diff --git a/components/atoms/static_atoms.txt b/components/atoms/static_atoms.txt index 8e28f90e04e..df9cd426984 100644 --- a/components/atoms/static_atoms.txt +++ b/components/atoms/static_atoms.txt @@ -65,3 +65,4 @@ characteristicvaluechanged fullscreenchange fullscreenerror gattserverdisconnected +onchange diff --git a/components/bluetooth/lib.rs b/components/bluetooth/lib.rs index a22e2ba6c91..a5f5f5837d7 100644 --- a/components/bluetooth/lib.rs +++ b/components/bluetooth/lib.rs @@ -256,6 +256,9 @@ impl BluetoothManager { BluetoothRequest::GetAvailability(sender) => { let _ = sender.send(self.get_availability()); }, + BluetoothRequest::MatchesFilter(id, filters, sender) => { + let _ = sender.send(self.device_matches_filter(&id, &filters)); + }, BluetoothRequest::Exit => { break }, @@ -425,6 +428,17 @@ impl BluetoothManager { self.cached_devices.contains_key(device_id) && self.address_to_id.values().any(|v| v == device_id) } + fn device_matches_filter(&mut self, + device_id: &str, + filters: &BluetoothScanfilterSequence) + -> BluetoothResult { + let mut adapter = try!(self.get_adapter()); + match self.get_device(&mut adapter, device_id) { + Some(ref device) => Ok(matches_filters(device, filters)), + None => Ok(false), + } + } + // Service fn get_and_cache_gatt_services(&mut self, @@ -561,6 +575,9 @@ impl BluetoothManager { -> BluetoothResponseResult { // Step 6. let mut adapter = try!(self.get_adapter()); + + // Step 7. + // Note: There are no requiredServiceUUIDS, we scan for all devices. if let Ok(ref session) = adapter.create_discovery_session() { if session.start_discovery().is_ok() { if !is_mock_adapter(&adapter) { @@ -570,8 +587,6 @@ impl BluetoothManager { let _ = session.stop_discovery(); } - // Step 7. - // Note: There are no requiredServiceUUIDS, we scan for all devices. let mut matched_devices = self.get_and_cache_devices(&mut adapter); // Step 8. @@ -582,8 +597,6 @@ impl BluetoothManager { } // Step 9. - // TODO: After the permission API implementation - // https://w3c.github.io/permissions/#prompt-the-user-to-choose if let Some(address) = self.select_device(matched_devices, &adapter) { let device_id = match self.address_to_id.get(&address) { Some(id) => id.clone(), @@ -602,7 +615,7 @@ impl BluetoothManager { return Ok(BluetoothResponse::RequestDevice(message)); } } - // TODO: Step 10 - 11: Implement the permission API. + // Step 10. return Err(BluetoothError::NotFound); // Step 12: Missing, because it is optional. } diff --git a/components/bluetooth_traits/lib.rs b/components/bluetooth_traits/lib.rs index 85421990f71..e54d256fe0c 100644 --- a/components/bluetooth_traits/lib.rs +++ b/components/bluetooth_traits/lib.rs @@ -12,7 +12,7 @@ pub mod blocklist; pub mod scanfilter; use ipc_channel::ipc::IpcSender; -use scanfilter::RequestDeviceoptions; +use scanfilter::{BluetoothScanfilterSequence, RequestDeviceoptions}; #[derive(Deserialize, Serialize)] pub enum BluetoothError { @@ -92,6 +92,7 @@ pub enum BluetoothRequest { SetRepresentedToNull(Vec, Vec, Vec), IsRepresentedDeviceNull(String, IpcSender), GetAvailability(IpcSender), + MatchesFilter(String, BluetoothScanfilterSequence, IpcSender>), Test(String, IpcSender>), Exit, } diff --git a/components/script/dom/bindings/codegen/CodegenRust.py b/components/script/dom/bindings/codegen/CodegenRust.py index 84bd4adf3d6..b1f188a7b45 100644 --- a/components/script/dom/bindings/codegen/CodegenRust.py +++ b/components/script/dom/bindings/codegen/CodegenRust.py @@ -1845,6 +1845,8 @@ class CGImports(CGWrapper): return type.flatMemberTypes if type.isDictionary(): return [type] + getTypesFromDictionary(type) + if type.isSequence(): + return componentTypes(type.inner) return [type] def isImportable(type): diff --git a/components/script/dom/bluetooth.rs b/components/script/dom/bluetooth.rs index 82c39782133..18ac71ad843 100644 --- a/components/script/dom/bluetooth.rs +++ b/components/script/dom/bluetooth.rs @@ -11,7 +11,11 @@ use core::clone::Clone; use dom::bindings::cell::DOMRefCell; use dom::bindings::codegen::Bindings::BluetoothBinding::{self, BluetoothDataFilterInit, BluetoothLEScanFilterInit}; use dom::bindings::codegen::Bindings::BluetoothBinding::{BluetoothMethods, RequestDeviceOptions}; +use dom::bindings::codegen::Bindings::BluetoothPermissionResultBinding::BluetoothPermissionDescriptor; +use dom::bindings::codegen::Bindings::BluetoothRemoteGATTServerBinding::BluetoothRemoteGATTServerBinding:: + BluetoothRemoteGATTServerMethods; use dom::bindings::codegen::Bindings::EventHandlerBinding::EventHandlerNonNull; +use dom::bindings::codegen::Bindings::PermissionStatusBinding::{PermissionName, PermissionState}; use dom::bindings::codegen::UnionTypes::StringOrUnsignedLong; use dom::bindings::error::Error::{self, Network, Security, Type}; use dom::bindings::error::Fallible; @@ -20,14 +24,19 @@ use dom::bindings::refcounted::{Trusted, TrustedPromise}; use dom::bindings::reflector::{DomObject, reflect_dom_object}; use dom::bindings::str::DOMString; use dom::bluetoothdevice::BluetoothDevice; +use dom::bluetoothpermissionresult::BluetoothPermissionResult; use dom::bluetoothuuid::{BluetoothServiceUUID, BluetoothUUID, UUID}; use dom::eventtarget::EventTarget; use dom::globalscope::GlobalScope; +use dom::permissions::{get_descriptor_permission_state, PermissionAlgorithm}; use dom::promise::Promise; use ipc_channel::ipc::{self, IpcSender}; use ipc_channel::router::ROUTER; -use js::jsapi::{JSAutoCompartment, JSContext}; +use js::conversions::ConversionResult; +use js::jsapi::{JSAutoCompartment, JSContext, JSObject}; +use js::jsval::{ObjectValue, UndefinedValue}; use script_thread::Runnable; +use std::cell::Ref; use std::collections::HashMap; use std::rc::Rc; use std::str::FromStr; @@ -46,6 +55,38 @@ const SERVICE_DATA_ERROR: &'static str = "'serviceData', if present, must be non const SERVICE_ERROR: &'static str = "'services', if present, must contain at least one service."; const OPTIONS_ERROR: &'static str = "Fields of 'options' conflict with each other. Either 'acceptAllDevices' member must be true, or 'filters' member must be set to a value."; +const BT_DESC_CONVERSION_ERROR: &'static str = "Can't convert to an IDL value of type BluetoothPermissionDescriptor"; + +#[derive(HeapSizeOf, JSTraceable)] +pub struct AllowedBluetoothDevice { + pub deviceId: DOMString, + pub mayUseGATT: bool, +} + +#[derive(HeapSizeOf, JSTraceable)] +pub struct BluetoothExtraPermissionData { + allowed_devices: DOMRefCell>, +} + +impl BluetoothExtraPermissionData { + pub fn new() -> BluetoothExtraPermissionData { + BluetoothExtraPermissionData { + allowed_devices: DOMRefCell::new(Vec::new()), + } + } + + pub fn add_new_allowed_device(&self, allowed_device: AllowedBluetoothDevice) { + self.allowed_devices.borrow_mut().push(allowed_device); + } + + fn get_allowed_devices(&self) -> Ref> { + self.allowed_devices.borrow() + } + + pub fn allowed_devices_contains_id(&self, id: DOMString) -> bool { + self.allowed_devices.borrow().iter().any(|d| d.deviceId == id) + } +} struct BluetoothContext { promise: Option, @@ -107,7 +148,8 @@ impl Bluetooth { fn request_bluetooth_devices(&self, p: &Rc, filters: &Option>, - optional_services: &Option>) { + optional_services: &Option>, + sender: IpcSender) { // TODO: Step 1: Triggered by user activation. // Step 2.2: There are no requiredServiceUUIDS, we scan for all devices. @@ -161,11 +203,13 @@ impl Bluetooth { let option = RequestDeviceoptions::new(BluetoothScanfilterSequence::new(uuid_filters), ServiceUUIDSequence::new(optional_services_uuids)); - // TODO: Step 3 - 5: Implement the permission API. + // Step 4 - 5. + if let PermissionState::Denied = get_descriptor_permission_state(PermissionName::Bluetooth, None) { + return p.reject_error(p.global().get_cx(), Error::NotFound); + } - // Note: Steps 6 - 8 are implemented in + // Note: Step 3, 6 - 8 are implemented in // components/net/bluetooth_thread.rs in request_device function. - let sender = response_async(p, self); self.get_bluetooth_thread().send(BluetoothRequest::RequestDevice(option, sender)).unwrap(); } } @@ -438,7 +482,8 @@ impl BluetoothMethods for Bluetooth { } // Step 2. - self.request_bluetooth_devices(&p, &option.filters, &option.optionalServices); + let sender = response_async(&p, self); + self.request_bluetooth_devices(&p, &option.filters, &option.optionalServices, sender); //Note: Step 3 - 4. in response function, Step 5. in handle_response function. return p; } @@ -463,7 +508,7 @@ impl AsyncBluetoothListener for Bluetooth { fn handle_response(&self, response: BluetoothResponse, promise_cx: *mut JSContext, promise: &Rc) { match response { // https://webbluetoothcg.github.io/web-bluetooth/#request-bluetooth-devices - // Step 13 - 14. + // Step 11, 13 - 14. BluetoothResponse::RequestDevice(device) => { let mut device_instance_map = self.device_instance_map.borrow_mut(); if let Some(existing_device) = device_instance_map.get(&device.id.clone()) { @@ -473,7 +518,14 @@ impl AsyncBluetoothListener for Bluetooth { DOMString::from(device.id.clone()), device.name.map(DOMString::from), &self); - device_instance_map.insert(device.id, JS::from_ref(&bt_device)); + device_instance_map.insert(device.id.clone(), JS::from_ref(&bt_device)); + + self.global().as_window().bluetooth_extra_permission_data().add_new_allowed_device( + AllowedBluetoothDevice { + deviceId: DOMString::from(device.id), + mayUseGATT: true, + } + ); // https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetooth-requestdevice // Step 5. promise.resolve_native(promise_cx, &bt_device); @@ -487,3 +539,143 @@ impl AsyncBluetoothListener for Bluetooth { } } } + +impl PermissionAlgorithm for Bluetooth { + type Descriptor = BluetoothPermissionDescriptor; + type Status = BluetoothPermissionResult; + + #[allow(unsafe_code)] + fn create_descriptor(cx: *mut JSContext, + permission_descriptor_obj: *mut JSObject) + -> Result { + rooted!(in(cx) let mut property = UndefinedValue()); + property.handle_mut().set(ObjectValue(permission_descriptor_obj)); + unsafe { + match BluetoothPermissionDescriptor::new(cx, property.handle()) { + Ok(ConversionResult::Success(descriptor)) => Ok(descriptor), + Ok(ConversionResult::Failure(error)) => Err(Error::Type(error.into_owned())), + Err(_) => Err(Error::Type(String::from(BT_DESC_CONVERSION_ERROR))), + } + } + } + + // https://webbluetoothcg.github.io/web-bluetooth/#query-the-bluetooth-permission + fn permission_query(cx: *mut JSContext, promise: &Rc, + descriptor: &BluetoothPermissionDescriptor, + status: &BluetoothPermissionResult) { + // Step 1: We are not using the `global` variable. + + // Step 2. + status.set_state(get_descriptor_permission_state(status.get_query(), None)); + + // Step 3. + if let PermissionState::Denied = status.get_state() { + status.set_devices(Vec::new()); + return promise.resolve_native(cx, status); + } + + // Step 4. + rooted_vec!(let mut matching_devices); + + // Step 5. + let global = status.global(); + let allowed_devices = global.as_window().bluetooth_extra_permission_data().get_allowed_devices(); + + let bluetooth = status.get_bluetooth(); + let device_map = bluetooth.get_device_map().borrow(); + + // Step 6. + for allowed_device in allowed_devices.iter() { + // Step 6.1. + if let Some(ref id) = descriptor.deviceId { + if &allowed_device.deviceId != id { + continue; + } + } + let device_id = String::from(allowed_device.deviceId.as_ref()); + + // Step 6.2. + if let Some(ref filters) = descriptor.filters { + let mut scan_filters: Vec = Vec::new(); + + // Step 6.2.1. + for filter in filters { + match canonicalize_filter(&filter) { + Ok(f) => scan_filters.push(f), + Err(error) => return promise.reject_error(cx, error), + } + } + + // Step 6.2.2. + // Instead of creating an internal slot we send an ipc message to the Bluetooth thread + // to check if one of the filters matches. + let (sender, receiver) = ipc::channel().unwrap(); + status.get_bluetooth_thread() + .send(BluetoothRequest::MatchesFilter(device_id.clone(), + BluetoothScanfilterSequence::new(scan_filters), + sender)).unwrap(); + + match receiver.recv().unwrap() { + Ok(true) => (), + Ok(false) => continue, + Err(error) => return promise.reject_error(cx, Error::from(error)), + }; + } + + // Step 6.3. + // TODO: Implement this correctly, not just using device ids here. + // https://webbluetoothcg.github.io/web-bluetooth/#get-the-bluetoothdevice-representing + if let Some(device) = device_map.get(&device_id) { + matching_devices.push(JS::from_ref(&**device)); + } + } + + // Step 7. + status.set_devices(matching_devices.drain(..).collect()); + + // https://w3c.github.io/permissions/#dom-permissions-query + // Step 7. + promise.resolve_native(cx, status); + } + + // https://webbluetoothcg.github.io/web-bluetooth/#request-the-bluetooth-permission + fn permission_request(cx: *mut JSContext, promise: &Rc, + descriptor: &BluetoothPermissionDescriptor, + status: &BluetoothPermissionResult) { + // Step 1. + if descriptor.filters.is_some() == descriptor.acceptAllDevices { + return promise.reject_error(cx, Error::Type(OPTIONS_ERROR.to_owned())); + } + + // Step 2. + let sender = response_async(promise, status); + let bluetooth = status.get_bluetooth(); + bluetooth.request_bluetooth_devices(promise, &descriptor.filters, &descriptor.optionalServices, sender); + + // NOTE: Step 3. is in BluetoothPermissionResult's `handle_response` function. + } + + #[allow(unrooted_must_root)] + // https://webbluetoothcg.github.io/web-bluetooth/#revoke-bluetooth-access + fn permission_revoke(_descriptor: &BluetoothPermissionDescriptor, status: &BluetoothPermissionResult) { + // Step 1. + let global = status.global(); + let allowed_devices = global.as_window().bluetooth_extra_permission_data().get_allowed_devices(); + // Step 2. + let bluetooth = status.get_bluetooth(); + let device_map = bluetooth.get_device_map().borrow(); + for (id, device) in device_map.iter() { + let id = DOMString::from(id.clone()); + // Step 2.1. + if allowed_devices.iter().any(|d| d.deviceId == id) && + !device.is_represented_device_null() { + // Note: We don't need to update the allowed_services, + // because we store it in the lower level + // where it is already up-to-date + continue; + } + // Step 2.2 - 2.4 + let _ = device.get_gatt().Disconnect(); + } + } +} diff --git a/components/script/dom/bluetoothdevice.rs b/components/script/dom/bluetoothdevice.rs index cd3d3dc07fa..d33aab9be97 100644 --- a/components/script/dom/bluetoothdevice.rs +++ b/components/script/dom/bluetoothdevice.rs @@ -74,6 +74,12 @@ impl BluetoothDevice { BluetoothDeviceBinding::Wrap) } + pub fn get_gatt(&self) -> Root { + self.gatt.or_init(|| { + BluetoothRemoteGATTServer::new(&self.global(), self) + }) + } + fn get_context(&self) -> Root { Root::from_ref(&self.context) } @@ -157,7 +163,7 @@ impl BluetoothDevice { #[allow(unrooted_must_root)] pub fn clean_up_disconnected_device(&self) { // Step 1. - self.Gatt().set_connected(false); + self.get_gatt().set_connected(false); // TODO: Step 2: Implement activeAlgorithms internal slot for BluetoothRemoteGATTServer. @@ -193,7 +199,7 @@ impl BluetoothDevice { for (id, device) in context.get_device_map().borrow().iter() { // Step 2.1 - 2.2. if id == &self.Id().to_string() { - if device.Gatt().Connected() { + if device.get_gatt().Connected() { return Ok(()); } // TODO: Step 2.3: Implement activeAlgorithms internal slot for BluetoothRemoteGATTServer. @@ -220,11 +226,14 @@ impl BluetoothDeviceMethods for BluetoothDevice { } // https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetoothdevice-gatt - fn Gatt(&self) -> Root { - // TODO: Step 1 - 2: Implement the Permission API. - self.gatt.or_init(|| { - BluetoothRemoteGATTServer::new(&self.global(), self) - }) + fn GetGatt(&self) -> Option> { + // Step 1. + if self.global().as_window().bluetooth_extra_permission_data() + .allowed_devices_contains_id(self.id.clone()) && !self.is_represented_device_null() { + return Some(self.get_gatt()) + } + // Step 2. + None } #[allow(unrooted_must_root)] diff --git a/components/script/dom/bluetoothpermissionresult.rs b/components/script/dom/bluetoothpermissionresult.rs new file mode 100644 index 00000000000..4f228e6c75c --- /dev/null +++ b/components/script/dom/bluetoothpermissionresult.rs @@ -0,0 +1,124 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use bluetooth_traits::{BluetoothRequest, BluetoothResponse}; +use dom::bindings::cell::DOMRefCell; +use dom::bindings::codegen::Bindings::BluetoothPermissionResultBinding::{self, BluetoothPermissionResultMethods}; +use dom::bindings::codegen::Bindings::NavigatorBinding::NavigatorBinding::NavigatorMethods; +use dom::bindings::codegen::Bindings::PermissionStatusBinding::{PermissionName, PermissionState}; +use dom::bindings::codegen::Bindings::PermissionStatusBinding::PermissionStatusBinding::PermissionStatusMethods; +use dom::bindings::codegen::Bindings::WindowBinding::WindowBinding::WindowMethods; +use dom::bindings::error::Error; +use dom::bindings::js::{JS, Root}; +use dom::bindings::reflector::{DomObject, reflect_dom_object}; +use dom::bindings::str::DOMString; +use dom::bluetooth::{AsyncBluetoothListener, Bluetooth, AllowedBluetoothDevice}; +use dom::bluetoothdevice::BluetoothDevice; +use dom::globalscope::GlobalScope; +use dom::permissionstatus::PermissionStatus; +use dom::promise::Promise; +use ipc_channel::ipc::IpcSender; +use js::jsapi::JSContext; +use std::rc::Rc; + +// https://webbluetoothcg.github.io/web-bluetooth/#bluetoothpermissionresult +#[dom_struct] +pub struct BluetoothPermissionResult { + status: PermissionStatus, + devices: DOMRefCell>>, +} + +impl BluetoothPermissionResult { + #[allow(unrooted_must_root)] + fn new_inherited(status: &PermissionStatus) -> BluetoothPermissionResult { + let result = BluetoothPermissionResult { + status: PermissionStatus::new_inherited(status.get_query()), + devices: DOMRefCell::new(Vec::new()), + }; + result.status.set_state(status.State()); + result + } + + pub fn new(global: &GlobalScope, status: &PermissionStatus) -> Root { + reflect_dom_object(box BluetoothPermissionResult::new_inherited(status), + global, + BluetoothPermissionResultBinding::Wrap) + } + + pub fn get_bluetooth(&self) -> Root { + self.global().as_window().Navigator().Bluetooth() + } + + pub fn get_bluetooth_thread(&self) -> IpcSender { + self.global().as_window().bluetooth_thread() + } + + pub fn get_query(&self) -> PermissionName { + self.status.get_query() + } + + pub fn set_state(&self, state: PermissionState) { + self.status.set_state(state) + } + + pub fn get_state(&self) -> PermissionState { + self.status.State() + } + + #[allow(unrooted_must_root)] + pub fn set_devices(&self, devices: Vec>) { + *self.devices.borrow_mut() = devices; + } +} + +impl BluetoothPermissionResultMethods for BluetoothPermissionResult { + // https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetoothpermissionresult-devices + fn Devices(&self) -> Vec> { + let device_vec: Vec> = + self.devices.borrow().iter().map(|d| Root::from_ref(&**d)).collect(); + device_vec + } +} + +impl AsyncBluetoothListener for BluetoothPermissionResult { + fn handle_response(&self, response: BluetoothResponse, promise_cx: *mut JSContext, promise: &Rc) { + match response { + // https://webbluetoothcg.github.io/web-bluetooth/#request-bluetooth-devices + // Step 3, 11, 13 - 14. + BluetoothResponse::RequestDevice(device) => { + self.set_state(PermissionState::Granted); + let bluetooth = self.get_bluetooth(); + let mut device_instance_map = bluetooth.get_device_map().borrow_mut(); + if let Some(ref existing_device) = device_instance_map.get(&device.id) { + // https://webbluetoothcg.github.io/web-bluetooth/#request-the-bluetooth-permission + // Step 3. + self.set_devices(vec!(JS::from_ref(&*existing_device))); + + // https://w3c.github.io/permissions/#dom-permissions-request + // Step 8. + return promise.resolve_native(promise_cx, self); + } + let bt_device = BluetoothDevice::new(&self.global(), + DOMString::from(device.id.clone()), + device.name.map(DOMString::from), + &bluetooth); + device_instance_map.insert(device.id.clone(), JS::from_ref(&bt_device)); + self.global().as_window().bluetooth_extra_permission_data().add_new_allowed_device( + AllowedBluetoothDevice { + deviceId: DOMString::from(device.id), + mayUseGATT: true, + } + ); + // https://webbluetoothcg.github.io/web-bluetooth/#request-the-bluetooth-permission + // Step 3. + self.set_devices(vec!(JS::from_ref(&bt_device))); + + // https://w3c.github.io/permissions/#dom-permissions-request + // Step 8. + promise.resolve_native(promise_cx, self); + }, + _ => promise.reject_error(promise_cx, Error::Type("Something went wrong...".to_owned())), + } + } +} diff --git a/components/script/dom/bluetoothremotegattcharacteristic.rs b/components/script/dom/bluetoothremotegattcharacteristic.rs index 636b9dc96f4..81210e4b170 100644 --- a/components/script/dom/bluetoothremotegattcharacteristic.rs +++ b/components/script/dom/bluetoothremotegattcharacteristic.rs @@ -7,7 +7,6 @@ use bluetooth_traits::blocklist::{Blocklist, uuid_is_blocklisted}; use dom::bindings::cell::DOMRefCell; use dom::bindings::codegen::Bindings::BluetoothCharacteristicPropertiesBinding:: BluetoothCharacteristicPropertiesMethods; -use dom::bindings::codegen::Bindings::BluetoothDeviceBinding::BluetoothDeviceMethods; use dom::bindings::codegen::Bindings::BluetoothRemoteGATTCharacteristicBinding; use dom::bindings::codegen::Bindings::BluetoothRemoteGATTCharacteristicBinding:: BluetoothRemoteGATTCharacteristicMethods; @@ -104,7 +103,7 @@ impl BluetoothRemoteGATTCharacteristicMethods for BluetoothRemoteGATTCharacteris // https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetoothremotegattcharacteristic-getdescriptor fn GetDescriptor(&self, descriptor: BluetoothDescriptorUUID) -> Rc { get_gatt_children(self, true, BluetoothUUID::descriptor, Some(descriptor), self.get_instance_id(), - self.Service().Device().Gatt().Connected(), GATTType::Descriptor) + self.Service().Device().get_gatt().Connected(), GATTType::Descriptor) } #[allow(unrooted_must_root)] @@ -113,7 +112,7 @@ impl BluetoothRemoteGATTCharacteristicMethods for BluetoothRemoteGATTCharacteris descriptor: Option) -> Rc { get_gatt_children(self, false, BluetoothUUID::descriptor, descriptor, self.get_instance_id(), - self.Service().Device().Gatt().Connected(), GATTType::Descriptor) + self.Service().Device().get_gatt().Connected(), GATTType::Descriptor) } // https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetoothremotegattcharacteristic-value @@ -134,7 +133,7 @@ impl BluetoothRemoteGATTCharacteristicMethods for BluetoothRemoteGATTCharacteris } // Step 2. - if !self.Service().Device().Gatt().Connected() { + if !self.Service().Device().get_gatt().Connected() { p.reject_error(p_cx, Network); return p; } @@ -174,7 +173,7 @@ impl BluetoothRemoteGATTCharacteristicMethods for BluetoothRemoteGATTCharacteris } // Step 4. - if !self.Service().Device().Gatt().Connected() { + if !self.Service().Device().get_gatt().Connected() { p.reject_error(p_cx, Network); return p; } @@ -210,7 +209,7 @@ impl BluetoothRemoteGATTCharacteristicMethods for BluetoothRemoteGATTCharacteris } // Step 2. - if !self.Service().Device().Gatt().Connected() { + if !self.Service().Device().get_gatt().Connected() { p.reject_error(p_cx, Network); return p; } diff --git a/components/script/dom/bluetoothremotegattdescriptor.rs b/components/script/dom/bluetoothremotegattdescriptor.rs index 5290c096d3a..c0ddd6dabe5 100644 --- a/components/script/dom/bluetoothremotegattdescriptor.rs +++ b/components/script/dom/bluetoothremotegattdescriptor.rs @@ -5,7 +5,6 @@ use bluetooth_traits::{BluetoothRequest, BluetoothResponse}; use bluetooth_traits::blocklist::{Blocklist, uuid_is_blocklisted}; use dom::bindings::cell::DOMRefCell; -use dom::bindings::codegen::Bindings::BluetoothDeviceBinding::BluetoothDeviceMethods; use dom::bindings::codegen::Bindings::BluetoothRemoteGATTCharacteristicBinding:: BluetoothRemoteGATTCharacteristicMethods; use dom::bindings::codegen::Bindings::BluetoothRemoteGATTDescriptorBinding; @@ -98,7 +97,7 @@ impl BluetoothRemoteGATTDescriptorMethods for BluetoothRemoteGATTDescriptor { } // Step 2. - if !self.Characteristic().Service().Device().Gatt().Connected() { + if !self.Characteristic().Service().Device().get_gatt().Connected() { p.reject_error(p_cx, Network); return p; } @@ -131,7 +130,7 @@ impl BluetoothRemoteGATTDescriptorMethods for BluetoothRemoteGATTDescriptor { } // Step 4. - if !self.Characteristic().Service().Device().Gatt().Connected() { + if !self.Characteristic().Service().Device().get_gatt().Connected() { p.reject_error(p_cx, Network); return p; } diff --git a/components/script/dom/bluetoothremotegattserver.rs b/components/script/dom/bluetoothremotegattserver.rs index 11e0528f5be..cd90b854907 100644 --- a/components/script/dom/bluetoothremotegattserver.rs +++ b/components/script/dom/bluetoothremotegattserver.rs @@ -103,17 +103,15 @@ impl BluetoothRemoteGATTServerMethods for BluetoothRemoteGATTServer { #[allow(unrooted_must_root)] // https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetoothremotegattserver-getprimaryservice fn GetPrimaryService(&self, service: BluetoothServiceUUID) -> Rc { - // TODO: Step 1: Implement the Permission API and the allowedServices BluetoothDevice internal slot. - // Step 2. + // Step 1 - 2. get_gatt_children(self, true, BluetoothUUID::service, Some(service), String::from(self.Device().Id()), - self.Device().Gatt().Connected(), GATTType::PrimaryService) + self.Device().get_gatt().Connected(), GATTType::PrimaryService) } #[allow(unrooted_must_root)] // https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetoothremotegattserver-getprimaryservices fn GetPrimaryServices(&self, service: Option) -> Rc { - // TODO: Step 1: Implement the Permission API and the allowedServices BluetoothDevice internal slot. - // Step 2. + // Step 1 - 2. get_gatt_children(self, false, BluetoothUUID::service, service, String::from(self.Device().Id()), self.Connected(), GATTType::PrimaryService) diff --git a/components/script/dom/bluetoothremotegattservice.rs b/components/script/dom/bluetoothremotegattservice.rs index da06678da0b..9b0ce5a3a3d 100644 --- a/components/script/dom/bluetoothremotegattservice.rs +++ b/components/script/dom/bluetoothremotegattservice.rs @@ -3,7 +3,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use bluetooth_traits::{BluetoothResponse, GATTType}; -use dom::bindings::codegen::Bindings::BluetoothDeviceBinding::BluetoothDeviceMethods; use dom::bindings::codegen::Bindings::BluetoothRemoteGATTServerBinding::BluetoothRemoteGATTServerMethods; use dom::bindings::codegen::Bindings::BluetoothRemoteGATTServiceBinding; use dom::bindings::codegen::Bindings::BluetoothRemoteGATTServiceBinding::BluetoothRemoteGATTServiceMethods; @@ -87,7 +86,7 @@ impl BluetoothRemoteGATTServiceMethods for BluetoothRemoteGATTService { characteristic: BluetoothCharacteristicUUID) -> Rc { get_gatt_children(self, true, BluetoothUUID::characteristic, Some(characteristic), self.get_instance_id(), - self.Device().Gatt().Connected(), GATTType::Characteristic) + self.Device().get_gatt().Connected(), GATTType::Characteristic) } #[allow(unrooted_must_root)] @@ -96,7 +95,7 @@ impl BluetoothRemoteGATTServiceMethods for BluetoothRemoteGATTService { characteristic: Option) -> Rc { get_gatt_children(self, false, BluetoothUUID::characteristic, characteristic, self.get_instance_id(), - self.Device().Gatt().Connected(), GATTType::Characteristic) + self.Device().get_gatt().Connected(), GATTType::Characteristic) } #[allow(unrooted_must_root)] @@ -105,7 +104,7 @@ impl BluetoothRemoteGATTServiceMethods for BluetoothRemoteGATTService { service: BluetoothServiceUUID) -> Rc { get_gatt_children(self, false, BluetoothUUID::service, Some(service), self.get_instance_id(), - self.Device().Gatt().Connected(), GATTType::IncludedService) + self.Device().get_gatt().Connected(), GATTType::IncludedService) } @@ -115,7 +114,7 @@ impl BluetoothRemoteGATTServiceMethods for BluetoothRemoteGATTService { service: Option) -> Rc { get_gatt_children(self, false, BluetoothUUID::service, service, self.get_instance_id(), - self.Device().Gatt().Connected(), GATTType::IncludedService) + self.Device().get_gatt().Connected(), GATTType::IncludedService) } // https://webbluetoothcg.github.io/web-bluetooth/#dom-serviceeventhandlers-onserviceadded @@ -151,12 +150,12 @@ impl AsyncBluetoothListener for BluetoothRemoteGATTService { // Step 7. BluetoothResponse::GetIncludedServices(services_vec, single) => { if single { - promise.resolve_native(promise_cx, &device.get_or_create_service(&services_vec[0], &device.Gatt())); - return; + return promise.resolve_native(promise_cx, + &device.get_or_create_service(&services_vec[0], &device.get_gatt())); } let mut services = vec!(); for service in services_vec { - let bt_service = device.get_or_create_service(&service, &device.Gatt()); + let bt_service = device.get_or_create_service(&service, &device.get_gatt()); services.push(bt_service); } promise.resolve_native(promise_cx, &services); diff --git a/components/script/dom/mod.rs b/components/script/dom/mod.rs index 5de594a5195..4b3c0c02cbb 100644 --- a/components/script/dom/mod.rs +++ b/components/script/dom/mod.rs @@ -223,6 +223,7 @@ pub mod bluetooth; pub mod bluetoothadvertisingevent; pub mod bluetoothcharacteristicproperties; pub mod bluetoothdevice; +pub mod bluetoothpermissionresult; pub mod bluetoothremotegattcharacteristic; pub mod bluetoothremotegattdescriptor; pub mod bluetoothremotegattserver; @@ -384,6 +385,8 @@ pub mod nodelist; pub mod pagetransitionevent; pub mod performance; pub mod performancetiming; +pub mod permissions; +pub mod permissionstatus; pub mod plugin; pub mod pluginarray; pub mod popstateevent; diff --git a/components/script/dom/navigator.rs b/components/script/dom/navigator.rs index dd9a1a789c6..27a1465e9d2 100644 --- a/components/script/dom/navigator.rs +++ b/components/script/dom/navigator.rs @@ -10,6 +10,7 @@ use dom::bindings::str::DOMString; use dom::bluetooth::Bluetooth; use dom::mimetypearray::MimeTypeArray; use dom::navigatorinfo; +use dom::permissions::Permissions; use dom::pluginarray::PluginArray; use dom::serviceworkercontainer::ServiceWorkerContainer; use dom::vr::VR; @@ -23,7 +24,8 @@ pub struct Navigator { plugins: MutNullableJS, mime_types: MutNullableJS, service_worker: MutNullableJS, - vr: MutNullableJS + vr: MutNullableJS, + permissions: MutNullableJS, } impl Navigator { @@ -35,6 +37,7 @@ impl Navigator { mime_types: Default::default(), service_worker: Default::default(), vr: Default::default(), + permissions: Default::default(), } } @@ -123,6 +126,11 @@ impl NavigatorMethods for Navigator { fn Vr(&self) -> Root { self.vr.or_init(|| VR::new(&self.global())) } + + // https://w3c.github.io/permissions/#navigator-and-workernavigator-extension + fn Permissions(&self) -> Root { + self.permissions.or_init(|| Permissions::new(&self.global())) + } } impl Navigator { diff --git a/components/script/dom/permissions.rs b/components/script/dom/permissions.rs new file mode 100644 index 00000000000..87d333f5bac --- /dev/null +++ b/components/script/dom/permissions.rs @@ -0,0 +1,351 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use dom::bindings::codegen::Bindings::PermissionStatusBinding::{PermissionDescriptor, PermissionName, PermissionState}; +use dom::bindings::codegen::Bindings::PermissionStatusBinding::PermissionStatusMethods; +use dom::bindings::codegen::Bindings::PermissionsBinding::{self, PermissionsMethods}; +use dom::bindings::error::Error; +use dom::bindings::js::Root; +use dom::bindings::reflector::{DomObject, Reflector, reflect_dom_object}; +use dom::bluetooth::Bluetooth; +use dom::bluetoothpermissionresult::BluetoothPermissionResult; +use dom::globalscope::GlobalScope; +use dom::permissionstatus::PermissionStatus; +use dom::promise::Promise; +use js::conversions::ConversionResult; +use js::jsapi::{JSContext, JSObject}; +use js::jsval::{ObjectValue, UndefinedValue}; +use servo_config::prefs::PREFS; +use std::rc::Rc; +#[cfg(target_os = "linux")] +use tinyfiledialogs::{self, MessageBoxIcon, YesNo}; + +#[cfg(target_os = "linux")] +const DIALOG_TITLE: &'static str = "Permission request dialog"; +const NONSECURE_DIALOG_MESSAGE: &'static str = "feature is only safe to use in secure context,\ + but servo can't guarantee\n that the current context is secure. Do you want to proceed and grant permission?"; +const REQUEST_DIALOG_MESSAGE: &'static str = "Do you want to grant permission for"; +const ROOT_DESC_CONVERSION_ERROR: &'static str = "Can't convert to an IDL value of type PermissionDescriptor"; + +pub trait PermissionAlgorithm { + type Descriptor; + type Status; + fn create_descriptor(cx: *mut JSContext, + permission_descriptor_obj: *mut JSObject) + -> Result; + fn permission_query(cx: *mut JSContext, promise: &Rc, + descriptor: &Self::Descriptor, status: &Self::Status); + fn permission_request(cx: *mut JSContext, promise: &Rc, + descriptor: &Self::Descriptor, status: &Self::Status); + fn permission_revoke(descriptor: &Self::Descriptor, status: &Self::Status); +} + +enum Operation { + Query, + Request, + Revoke, +} + +// https://w3c.github.io/permissions/#permissions +#[dom_struct] +pub struct Permissions { + reflector_: Reflector, +} + +impl Permissions { + pub fn new_inherited() -> Permissions { + Permissions { + reflector_: Reflector::new(), + } + } + + pub fn new(global: &GlobalScope) -> Root { + reflect_dom_object(box Permissions::new_inherited(), + global, + PermissionsBinding::Wrap) + } + + #[allow(unrooted_must_root)] + // https://w3c.github.io/permissions/#dom-permissions-query + // https://w3c.github.io/permissions/#dom-permissions-request + // https://w3c.github.io/permissions/#dom-permissions-revoke + fn manipulate(&self, + op: Operation, + cx: *mut JSContext, + permissionDesc: *mut JSObject, + promise: Option>) + -> Rc { + // (Query, Request) Step 3. + let p = match promise { + Some(promise) => promise, + None => Promise::new(&self.global()), + }; + + // (Query, Request, Revoke) Step 1. + let root_desc = match Permissions::create_descriptor(cx, permissionDesc) { + Ok(descriptor) => descriptor, + Err(error) => { + p.reject_error(cx, error); + return p; + }, + }; + + // (Query, Request) Step 5. + let status = PermissionStatus::new(&self.global(), &root_desc); + + // (Query, Request, Revoke) Step 2. + match root_desc.name { + PermissionName::Bluetooth => { + let bluetooth_desc = match Bluetooth::create_descriptor(cx, permissionDesc) { + Ok(descriptor) => descriptor, + Err(error) => { + p.reject_error(cx, error); + return p; + }, + }; + + // (Query, Request) Step 5. + let result = BluetoothPermissionResult::new(&self.global(), &status); + + match &op { + // (Request) Step 6 - 8. + &Operation::Request => Bluetooth::permission_request(cx, &p, &bluetooth_desc, &result), + + // (Query) Step 6 - 7. + &Operation::Query => Bluetooth::permission_query(cx, &p, &bluetooth_desc, &result), + + &Operation::Revoke => { + // (Revoke) Step 3. + let globalscope = self.global(); + globalscope.as_window() + .permission_state_invocation_results() + .borrow_mut() + .remove(&root_desc.name.to_string()); + + // (Revoke) Step 4. + Bluetooth::permission_revoke(&bluetooth_desc, &result) + }, + } + }, + _ => { + match &op { + &Operation::Request => { + // (Request) Step 6. + Permissions::permission_request(cx, &p, &root_desc, &status); + + // (Request) Step 7. The default algorithm always resolve + + // (Request) Step 8. + p.resolve_native(cx, &status); + }, + &Operation::Query => { + // (Query) Step 6. + Permissions::permission_query(cx, &p, &root_desc, &status); + + // (Query) Step 7. + p.resolve_native(cx, &status); + }, + + &Operation::Revoke => { + // (Revoke) Step 3. + let globalscope = self.global(); + globalscope.as_window() + .permission_state_invocation_results() + .borrow_mut() + .remove(&root_desc.name.to_string()); + + // (Revoke) Step 4. + Permissions::permission_revoke(&root_desc, &status); + }, + } + }, + }; + match op { + // (Revoke) Step 5. + Operation::Revoke => self.manipulate(Operation::Query, cx, permissionDesc, Some(p)), + + // (Query, Request) Step 4. + _ => p, + } + } +} + +impl PermissionsMethods for Permissions { + #[allow(unrooted_must_root)] + #[allow(unsafe_code)] + // https://w3c.github.io/permissions/#dom-permissions-query + unsafe fn Query(&self, cx: *mut JSContext, permissionDesc: *mut JSObject) -> Rc { + self.manipulate(Operation::Query, cx, permissionDesc, None) + } + + #[allow(unrooted_must_root)] + #[allow(unsafe_code)] + // https://w3c.github.io/permissions/#dom-permissions-request + unsafe fn Request(&self, cx: *mut JSContext, permissionDesc: *mut JSObject) -> Rc { + self.manipulate(Operation::Request, cx, permissionDesc, None) + } + + #[allow(unrooted_must_root)] + #[allow(unsafe_code)] + // https://w3c.github.io/permissions/#dom-permissions-revoke + unsafe fn Revoke(&self, cx: *mut JSContext, permissionDesc: *mut JSObject) -> Rc { + self.manipulate(Operation::Revoke, cx, permissionDesc, None) + } +} + +impl PermissionAlgorithm for Permissions { + type Descriptor = PermissionDescriptor; + type Status = PermissionStatus; + + #[allow(unsafe_code)] + fn create_descriptor(cx: *mut JSContext, + permission_descriptor_obj: *mut JSObject) + -> Result { + rooted!(in(cx) let mut property = UndefinedValue()); + property.handle_mut().set(ObjectValue(permission_descriptor_obj)); + unsafe { + match PermissionDescriptor::new(cx, property.handle()) { + Ok(ConversionResult::Success(descriptor)) => Ok(descriptor), + Ok(ConversionResult::Failure(error)) => Err(Error::Type(error.into_owned())), + Err(_) => Err(Error::Type(String::from(ROOT_DESC_CONVERSION_ERROR))), + } + } + } + + // https://w3c.github.io/permissions/#boolean-permission-query-algorithm + fn permission_query(_cx: *mut JSContext, + _promise: &Rc, + _descriptor: &PermissionDescriptor, + status: &PermissionStatus) { + // Step 1. + status.set_state(get_descriptor_permission_state(status.get_query(), None)); + } + + // https://w3c.github.io/permissions/#boolean-permission-request-algorithm + fn permission_request(cx: *mut JSContext, + promise: &Rc, + descriptor: &PermissionDescriptor, + status: &PermissionStatus) { + // Step 1. + Permissions::permission_query(cx, promise, descriptor, status); + + match status.State() { + // Step 3. + PermissionState::Prompt => { + let perm_name = status.get_query(); + // https://w3c.github.io/permissions/#request-permission-to-use (Step 3 - 4) + let state = + prompt_user(&format!("{} {} ?", REQUEST_DIALOG_MESSAGE, perm_name.clone())); + + let globalscope = GlobalScope::current(); + globalscope.as_window() + .permission_state_invocation_results() + .borrow_mut() + .insert(perm_name.to_string(), state); + }, + + // Step 2. + _ => return, + } + + // Step 4. + Permissions::permission_query(cx, promise, descriptor, status); + } + + fn permission_revoke(_descriptor: &PermissionDescriptor, _status: &PermissionStatus) {} +} + +// https://w3c.github.io/permissions/#permission-state +pub fn get_descriptor_permission_state(permission_name: PermissionName, + env_settings_obj: Option<&GlobalScope>) + -> PermissionState { + // Step 1. + let settings = match env_settings_obj { + Some(env_settings_obj) => Root::from_ref(env_settings_obj), + None => GlobalScope::current(), + }; + + // Step 2. + // TODO: The `is the environment settings object a non-secure context` check is missing. + // The current solution is a workaround with a message box to warn about this, + // if the feature is not allowed in non-secure contexcts, + // and let the user decide to grant the permission or not. + let state = match allowed_in_nonsecure_contexts(&permission_name) { + true => PermissionState::Prompt, + false => { + match PREFS.get("dom.permissions.testing.allowed_in_nonsecure_contexts").as_boolean().unwrap_or(false) { + true => PermissionState::Granted, + false => { + settings.as_window() + .permission_state_invocation_results() + .borrow_mut() + .remove(&permission_name.to_string()); + prompt_user(&format!("The {} {}", permission_name, NONSECURE_DIALOG_MESSAGE)) + }, + } + }, + }; + + // Step 3. + if let Some(prev_result) = settings.as_window() + .permission_state_invocation_results() + .borrow() + .get(&permission_name.to_string()) { + return prev_result.clone(); + } + + // Store the invocation result + settings.as_window() + .permission_state_invocation_results() + .borrow_mut() + .insert(permission_name.to_string(), state); + + // Step 4. + state +} + +#[cfg(target_os = "linux")] +fn prompt_user(message: &str) -> PermissionState { + match tinyfiledialogs::message_box_yes_no(DIALOG_TITLE, + message, + MessageBoxIcon::Question, + YesNo::No) { + YesNo::Yes => PermissionState::Granted, + YesNo::No => PermissionState::Denied, + } +} + +#[cfg(not(target_os = "linux"))] +fn prompt_user(_message: &str) -> PermissionState { + // TODO popup only supported on linux + PermissionState::Denied +} + +// https://w3c.github.io/permissions/#allowed-in-non-secure-contexts +fn allowed_in_nonsecure_contexts(permission_name: &PermissionName) -> bool { + match *permission_name { + // https://w3c.github.io/permissions/#dom-permissionname-geolocation + PermissionName::Geolocation => true, + // https://w3c.github.io/permissions/#dom-permissionname-notifications + PermissionName::Notifications => true, + // https://w3c.github.io/permissions/#dom-permissionname-push + PermissionName::Push => false, + // https://w3c.github.io/permissions/#dom-permissionname-midi + PermissionName::Midi => true, + // https://w3c.github.io/permissions/#dom-permissionname-camera + PermissionName::Camera => false, + // https://w3c.github.io/permissions/#dom-permissionname-microphone + PermissionName::Microphone => false, + // https://w3c.github.io/permissions/#dom-permissionname-speaker + PermissionName::Speaker => false, + // https://w3c.github.io/permissions/#dom-permissionname-device-info + PermissionName::Device_info => false, + // https://w3c.github.io/permissions/#dom-permissionname-background-sync + PermissionName::Background_sync => false, + // https://webbluetoothcg.github.io/web-bluetooth/#dom-permissionname-bluetooth + PermissionName::Bluetooth => false, + // https://storage.spec.whatwg.org/#dom-permissionname-persistent-storage + PermissionName::Persistent_storage => false, + } +} diff --git a/components/script/dom/permissionstatus.rs b/components/script/dom/permissionstatus.rs new file mode 100644 index 00000000000..0d060aa8e84 --- /dev/null +++ b/components/script/dom/permissionstatus.rs @@ -0,0 +1,62 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use dom::bindings::codegen::Bindings::EventHandlerBinding::EventHandlerNonNull; +use dom::bindings::codegen::Bindings::PermissionStatusBinding::{self, PermissionDescriptor, PermissionName}; +use dom::bindings::codegen::Bindings::PermissionStatusBinding::{PermissionNameValues, PermissionState}; +use dom::bindings::codegen::Bindings::PermissionStatusBinding::PermissionStatusMethods; +use dom::bindings::js::Root; +use dom::bindings::reflector::reflect_dom_object; +use dom::eventtarget::EventTarget; +use dom::globalscope::GlobalScope; +use std::cell::Cell; +use std::fmt::{self, Display, Formatter}; + +// https://w3c.github.io/permissions/#permissionstatus +#[dom_struct] +pub struct PermissionStatus { + eventtarget: EventTarget, + state: Cell, + query: Cell, +} + +impl PermissionStatus { + pub fn new_inherited(query: PermissionName) -> PermissionStatus { + PermissionStatus { + eventtarget: EventTarget::new_inherited(), + state: Cell::new(PermissionState::Denied), + query: Cell::new(query), + } + } + + pub fn new(global: &GlobalScope, query: &PermissionDescriptor) -> Root { + reflect_dom_object(box PermissionStatus::new_inherited(query.name), + global, + PermissionStatusBinding::Wrap) + } + + pub fn set_state(&self, state: PermissionState) { + self.state.set(state); + } + + pub fn get_query(&self) -> PermissionName { + self.query.get() + } +} + +impl PermissionStatusMethods for PermissionStatus { + // https://w3c.github.io/permissions/#dom-permissionstatus-state + fn State(&self) -> PermissionState { + self.state.get() + } + + // https://w3c.github.io/permissions/#dom-permissionstatus-onchange + event_handler!(onchange, GetOnchange, SetOnchange); +} + +impl Display for PermissionName { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{}", PermissionNameValues::strings[*self as usize].to_string()) + } +} diff --git a/components/script/dom/webidls/BluetoothDevice.webidl b/components/script/dom/webidls/BluetoothDevice.webidl index eac8b533392..1eb9f495ec0 100644 --- a/components/script/dom/webidls/BluetoothDevice.webidl +++ b/components/script/dom/webidls/BluetoothDevice.webidl @@ -8,7 +8,7 @@ interface BluetoothDevice : EventTarget { readonly attribute DOMString id; readonly attribute DOMString? name; - readonly attribute BluetoothRemoteGATTServer gatt; + readonly attribute BluetoothRemoteGATTServer? gatt; Promise watchAdvertisements(); void unwatchAdvertisements(); diff --git a/components/script/dom/webidls/BluetoothPermissionResult.webidl b/components/script/dom/webidls/BluetoothPermissionResult.webidl new file mode 100644 index 00000000000..3ac0685b3d0 --- /dev/null +++ b/components/script/dom/webidls/BluetoothPermissionResult.webidl @@ -0,0 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// https://webbluetoothcg.github.io/web-bluetooth/#bluetoothpermissionresult + +dictionary BluetoothPermissionDescriptor : PermissionDescriptor { + DOMString deviceId; + // These match RequestDeviceOptions. + sequence filters; + sequence optionalServices/* = []*/; + boolean acceptAllDevices = false; +}; + +[Pref="dom.bluetooth.enabled"] +interface BluetoothPermissionResult : PermissionStatus { + // attribute FrozenArray devices; + // Workaround until FrozenArray get implemented. + sequence devices(); +}; diff --git a/components/script/dom/webidls/Navigator.webidl b/components/script/dom/webidls/Navigator.webidl index a60a1541446..e04616ef40f 100644 --- a/components/script/dom/webidls/Navigator.webidl +++ b/components/script/dom/webidls/Navigator.webidl @@ -62,3 +62,9 @@ interface NavigatorCookies { partial interface Navigator { [SameObject, Pref="dom.webvr.enabled"] readonly attribute VR vr; }; + +// https://w3c.github.io/permissions/#navigator-and-workernavigator-extension +[Exposed=(Window)] +partial interface Navigator { + [Pref="dom.permissions.enabled"] readonly attribute Permissions permissions; +}; diff --git a/components/script/dom/webidls/PermissionStatus.webidl b/components/script/dom/webidls/PermissionStatus.webidl new file mode 100644 index 00000000000..9fb1a5ce966 --- /dev/null +++ b/components/script/dom/webidls/PermissionStatus.webidl @@ -0,0 +1,47 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + // https://w3c.github.io/permissions/#permissionstatus + +dictionary PermissionDescriptor { + required PermissionName name; +}; + +enum PermissionState { + "granted", + "denied", + "prompt", +}; + +enum PermissionName { + "geolocation", + "notifications", + "push", + "midi", + "camera", + "microphone", + "speaker", + "device-info", + "background-sync", + "bluetooth", + "persistent-storage", +}; + +[Pref="dom.permissions.enabled", Exposed=(Window,Worker)] +interface PermissionStatus : EventTarget { + readonly attribute PermissionState state; + attribute EventHandler onchange; +}; + +dictionary PushPermissionDescriptor : PermissionDescriptor { + boolean userVisibleOnly = false; +}; + +dictionary MidiPermissionDescriptor : PermissionDescriptor { + boolean sysex = false; +}; + +dictionary DevicePermissionDescriptor : PermissionDescriptor { + DOMString deviceId; +}; diff --git a/components/script/dom/webidls/Permissions.webidl b/components/script/dom/webidls/Permissions.webidl new file mode 100644 index 00000000000..56841956e8d --- /dev/null +++ b/components/script/dom/webidls/Permissions.webidl @@ -0,0 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// https://w3c.github.io/permissions/#permissions-interface + +[Pref="dom.permissions.enabled", Exposed=(Window,Worker)] +interface Permissions { + Promise query(object permissionDesc); + + Promise request(object permissionDesc); + + Promise revoke(object permissionDesc); +}; diff --git a/components/script/dom/webidls/WorkerNavigator.webidl b/components/script/dom/webidls/WorkerNavigator.webidl index 0661325b8be..c60eda76896 100644 --- a/components/script/dom/webidls/WorkerNavigator.webidl +++ b/components/script/dom/webidls/WorkerNavigator.webidl @@ -8,3 +8,10 @@ interface WorkerNavigator {}; WorkerNavigator implements NavigatorID; WorkerNavigator implements NavigatorLanguage; //WorkerNavigator implements NavigatorOnLine; + +// https://w3c.github.io/permissions/#navigator-and-workernavigator-extension + +[Exposed=(Worker)] +partial interface WorkerNavigator { + [Pref="dom.permissions.enabled"] readonly attribute Permissions permissions; +}; diff --git a/components/script/dom/window.rs b/components/script/dom/window.rs index fe9bab6a5e6..b68bd492fd0 100644 --- a/components/script/dom/window.rs +++ b/components/script/dom/window.rs @@ -13,6 +13,7 @@ use dom::bindings::codegen::Bindings::EventHandlerBinding::OnBeforeUnloadEventHa use dom::bindings::codegen::Bindings::EventHandlerBinding::OnErrorEventHandlerNonNull; use dom::bindings::codegen::Bindings::FunctionBinding::Function; use dom::bindings::codegen::Bindings::NodeBinding::NodeMethods; +use dom::bindings::codegen::Bindings::PermissionStatusBinding::PermissionState; use dom::bindings::codegen::Bindings::RequestBinding::RequestInit; use dom::bindings::codegen::Bindings::WindowBinding::{self, FrameRequestCallback, WindowMethods}; use dom::bindings::codegen::Bindings::WindowBinding::{ScrollBehavior, ScrollToOptions}; @@ -26,6 +27,7 @@ use dom::bindings::reflector::DomObject; use dom::bindings::str::DOMString; use dom::bindings::structuredclone::StructuredCloneData; use dom::bindings::utils::{GlobalStaticData, WindowProxyHandler}; +use dom::bluetooth::BluetoothExtraPermissionData; use dom::browsingcontext::BrowsingContext; use dom::crypto::Crypto; use dom::cssstyledeclaration::{CSSModificationAccess, CSSStyleDeclaration, CSSStyleOwner}; @@ -209,6 +211,8 @@ pub struct Window { #[ignore_heap_size_of = "channels are hard"] bluetooth_thread: IpcSender, + bluetooth_extra_permission_data: BluetoothExtraPermissionData, + /// An enlarged rectangle around the page contents visible in the viewport, used /// to prevent creating display list items for content that is far away from the viewport. page_clip_rect: Cell>, @@ -246,7 +250,10 @@ pub struct Window { /// A handle for communicating messages to the webvr thread, if available. #[ignore_heap_size_of = "channels are hard"] - webvr_thread: Option> + webvr_thread: Option>, + + /// A map for storing the previous permission state read results. + permission_state_invocation_results: DOMRefCell> } impl Window { @@ -313,6 +320,10 @@ impl Window { self.bluetooth_thread.clone() } + pub fn bluetooth_extra_permission_data(&self) -> &BluetoothExtraPermissionData { + &self.bluetooth_extra_permission_data + } + pub fn css_error_reporter(&self) -> Box { self.error_reporter.clone() } @@ -331,6 +342,10 @@ impl Window { pub fn webvr_thread(&self) -> Option> { self.webvr_thread.clone() } + + pub fn permission_state_invocation_results(&self) -> &DOMRefCell> { + &self.permission_state_invocation_results + } } #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] @@ -1678,6 +1693,7 @@ impl Window { dom_static: GlobalStaticData::new(), js_runtime: DOMRefCell::new(Some(runtime.clone())), bluetooth_thread: bluetooth_thread, + bluetooth_extra_permission_data: BluetoothExtraPermissionData::new(), page_clip_rect: Cell::new(max_rect()), resize_event: Cell::new(None), layout_chan: layout_chan, @@ -1696,7 +1712,8 @@ impl Window { scroll_offsets: DOMRefCell::new(HashMap::new()), media_query_lists: WeakMediaQueryListVec::new(), test_runner: Default::default(), - webvr_thread: webvr_thread + webvr_thread: webvr_thread, + permission_state_invocation_results: DOMRefCell::new(HashMap::new()), }; unsafe { diff --git a/components/script/dom/workernavigator.rs b/components/script/dom/workernavigator.rs index f6cd521634d..d2a5a7da7d5 100644 --- a/components/script/dom/workernavigator.rs +++ b/components/script/dom/workernavigator.rs @@ -4,22 +4,25 @@ use dom::bindings::codegen::Bindings::WorkerNavigatorBinding; use dom::bindings::codegen::Bindings::WorkerNavigatorBinding::WorkerNavigatorMethods; -use dom::bindings::js::Root; -use dom::bindings::reflector::{Reflector, reflect_dom_object}; +use dom::bindings::js::{MutNullableJS, Root}; +use dom::bindings::reflector::{DomObject, Reflector, reflect_dom_object}; use dom::bindings::str::DOMString; use dom::navigatorinfo; +use dom::permissions::Permissions; use dom::workerglobalscope::WorkerGlobalScope; // https://html.spec.whatwg.org/multipage/#workernavigator #[dom_struct] pub struct WorkerNavigator { reflector_: Reflector, + permissions: MutNullableJS, } impl WorkerNavigator { fn new_inherited() -> WorkerNavigator { WorkerNavigator { reflector_: Reflector::new(), + permissions: Default::default(), } } @@ -70,4 +73,9 @@ impl WorkerNavigatorMethods for WorkerNavigator { fn Language(&self) -> DOMString { navigatorinfo::Language() } + + // https://w3c.github.io/permissions/#navigator-and-workernavigator-extension + fn Permissions(&self) -> Root { + self.permissions.or_init(|| Permissions::new(&self.global())) + } } diff --git a/resources/prefs.json b/resources/prefs.json index 74c8fa7f9fe..278c5c4f8bf 100644 --- a/resources/prefs.json +++ b/resources/prefs.json @@ -4,6 +4,8 @@ "dom.forcetouch.enabled": false, "dom.mouseevent.which.enabled": false, "dom.mozbrowser.enabled": false, + "dom.permissions.enabled": false, + "dom.permissions.testing.allowed_in_nonsecure_contexts": false, "dom.serviceworker.timeout_seconds": 60, "dom.testable_crash.enabled": false, "dom.testbinding.enabled": false, diff --git a/tests/html/bluetooth-permission.html b/tests/html/bluetooth-permission.html new file mode 100644 index 00000000000..ebfe40e8e78 --- /dev/null +++ b/tests/html/bluetooth-permission.html @@ -0,0 +1,69 @@ + + +Bluetooth Permission Test + + + +

+    
+
+
diff --git a/tests/html/permission-test.html b/tests/html/permission-test.html
new file mode 100644
index 00000000000..fa1c9183f98
--- /dev/null
+++ b/tests/html/permission-test.html
@@ -0,0 +1,37 @@
+
+
+Permission Test
+
+    
+    
+    
+    
+    

+    
+
+
diff --git a/tests/wpt/mozilla/meta/MANIFEST.json b/tests/wpt/mozilla/meta/MANIFEST.json
index 86ef8cfc0ff..c73fdd4a390 100644
--- a/tests/wpt/mozilla/meta/MANIFEST.json
+++ b/tests/wpt/mozilla/meta/MANIFEST.json
@@ -24367,7 +24367,7 @@
    "testharness"
   ],
   "mozilla/bluetooth/connect/device-goes-out-of-range.html": [
-   "9e5dc423f92c3f73273d221e7fb7b8d905716db3",
+   "3ade30929f621b7c8c93b87e5392309729d68e42",
    "testharness"
   ],
   "mozilla/bluetooth/connect/get-same-gatt-server.html": [
diff --git a/tests/wpt/mozilla/meta/mozilla/bluetooth/__dir__.ini b/tests/wpt/mozilla/meta/mozilla/bluetooth/__dir__.ini
index ad2557a1798..c95b7c11c1d 100644
--- a/tests/wpt/mozilla/meta/mozilla/bluetooth/__dir__.ini
+++ b/tests/wpt/mozilla/meta/mozilla/bluetooth/__dir__.ini
@@ -1 +1 @@
-prefs: [dom.bluetooth.enabled:true, dom.bluetooth.testing.enabled:true]
+prefs: [dom.bluetooth.enabled:true, dom.bluetooth.testing.enabled:true, dom.permissions.testing.allowed_in_nonsecure_contexts:true]
diff --git a/tests/wpt/mozilla/meta/mozilla/bluetooth/interfaces.html.ini b/tests/wpt/mozilla/meta/mozilla/bluetooth/interfaces.html.ini
index 372cbf3def3..546d57e0b3d 100644
--- a/tests/wpt/mozilla/meta/mozilla/bluetooth/interfaces.html.ini
+++ b/tests/wpt/mozilla/meta/mozilla/bluetooth/interfaces.html.ini
@@ -51,18 +51,9 @@
   [BluetoothPermissionResult interface: existence and properties of interface object]
     expected: FAIL
 
-  [BluetoothPermissionResult interface object length]
-    expected: FAIL
-
-  [BluetoothPermissionResult interface object name]
-    expected: FAIL
-
   [BluetoothPermissionResult interface: existence and properties of interface prototype object]
     expected: FAIL
 
-  [BluetoothPermissionResult interface: existence and properties of interface prototype object's "constructor" property]
-    expected: FAIL
-
   [BluetoothPermissionResult interface: attribute devices]
     expected: FAIL
 
diff --git a/tests/wpt/mozilla/tests/mozilla/bluetooth/connect/device-goes-out-of-range.html b/tests/wpt/mozilla/tests/mozilla/bluetooth/connect/device-goes-out-of-range.html
index f189a9971f2..cb20839271d 100644
--- a/tests/wpt/mozilla/tests/mozilla/bluetooth/connect/device-goes-out-of-range.html
+++ b/tests/wpt/mozilla/tests/mozilla/bluetooth/connect/device-goes-out-of-range.html
@@ -10,8 +10,9 @@ promise_test(t => {
         filters: [{services: [heart_rate.name]}]
     })
     .then(device => {
+        var gatt_server = device.gatt;
         window.testRunner.setBluetoothMockDataSet(adapter_type.empty);
-        return promise_rejects(t, 'NetworkError', device.gatt.connect());
+        return promise_rejects(t, 'NetworkError', gatt_server.connect());
     });
 }, 'Device goes out of range. Reject with NetworkError.');