diff --git a/components/config/prefs.rs b/components/config/prefs.rs index 8b3c5e86788..271bc6ec8e7 100644 --- a/components/config/prefs.rs +++ b/components/config/prefs.rs @@ -452,7 +452,7 @@ pub enum UserAgentPlatform { impl UserAgentPlatform { /// Return the default `UserAgentPlatform` for this platform. This is /// not an implementation of `Default` so that it can be `const`. - const fn default() -> Self { + pub const fn default() -> Self { if cfg!(target_os = "android") { Self::Android } else if cfg!(target_env = "ohos") { diff --git a/components/constellation/constellation.rs b/components/constellation/constellation.rs index e898933ada8..9ec81cdfc93 100644 --- a/components/constellation/constellation.rs +++ b/components/constellation/constellation.rs @@ -482,6 +482,9 @@ pub struct Constellation { /// When in single-process mode, join handles for script-threads. script_join_handles: HashMap>, + + /// A list of URLs that can access privileged internal APIs. + privileged_urls: Vec, } /// State needed to construct a constellation. @@ -535,6 +538,9 @@ pub struct InitialConstellationState { /// User content manager pub user_content_manager: UserContentManager, + /// A list of URLs that can access privileged internal APIs. + pub privileged_urls: Vec, + /// The async runtime. pub async_runtime: Box, } @@ -732,6 +738,7 @@ where process_manager: ProcessManager::new(state.mem_profiler_chan), async_runtime: state.async_runtime, script_join_handles: Default::default(), + privileged_urls: state.privileged_urls, }; constellation.run(); @@ -1003,6 +1010,7 @@ where player_context: WindowGLContext::get(), rippy_data: self.rippy_data.clone(), user_content_manager: self.user_content_manager.clone(), + privileged_urls: self.privileged_urls.clone(), }); let pipeline = match result { diff --git a/components/constellation/pipeline.rs b/components/constellation/pipeline.rs index 83db415e84c..78924513bbe 100644 --- a/components/constellation/pipeline.rs +++ b/components/constellation/pipeline.rs @@ -204,6 +204,9 @@ pub struct InitialPipelineState { /// User content manager pub user_content_manager: UserContentManager, + + /// A list of URLs that can access privileged internal APIs. + pub privileged_urls: Vec, } pub struct NewPipeline { @@ -305,6 +308,7 @@ impl Pipeline { rippy_data: state.rippy_data, user_content_manager: state.user_content_manager, lifeline_sender: None, + privileged_urls: state.privileged_urls, }; // Spawn the child process. @@ -497,6 +501,7 @@ pub struct UnprivilegedPipelineContent { rippy_data: Vec, user_content_manager: UserContentManager, lifeline_sender: Option>, + privileged_urls: Vec, } impl UnprivilegedPipelineContent { @@ -543,6 +548,7 @@ impl UnprivilegedPipelineContent { player_context: self.player_context.clone(), inherited_secure_context: self.load_data.inherited_secure_context, user_content_manager: self.user_content_manager, + privileged_urls: self.privileged_urls, }, layout_factory, Arc::new(self.system_font_service.to_proxy()), diff --git a/components/net/http_cache.rs b/components/net/http_cache.rs index 94a6331bb30..8ff8e808d82 100644 --- a/components/net/http_cache.rs +++ b/components/net/http_cache.rs @@ -514,6 +514,10 @@ impl HttpCache { request: &Request, done_chan: &mut DoneChannel, ) -> Option { + if pref!(network_http_cache_disabled) { + return None; + } + // TODO: generate warning headers as appropriate debug!("trying to construct cache response for {:?}", request.url()); if request.method != Method::GET { diff --git a/components/net/protocols/mod.rs b/components/net/protocols/mod.rs index 6dc58ceab64..a4d420bf2a5 100644 --- a/components/net/protocols/mod.rs +++ b/components/net/protocols/mod.rs @@ -30,6 +30,13 @@ use file::FileProtocolHander; static FORBIDDEN_SCHEMES: [&str; 4] = ["http", "https", "chrome", "about"]; pub trait ProtocolHandler: Send + Sync { + /// A list of schema-less URLs that can be resolved against this handler's + /// scheme. These URLs will be granted access to the `navigator.servo` + /// interface to perform privileged operations that manipulate Servo internals. + fn privileged_paths(&self) -> &'static [&'static str] { + &[] + } + /// Triggers the load of a resource for this protocol and returns a future /// that will produce a Response. Even if the protocol is not backed by a /// http endpoint, it is recommended to a least provide: @@ -132,6 +139,18 @@ impl ProtocolRegistry { .get(scheme) .is_some_and(|handler| handler.is_secure()) } + + pub fn privileged_urls(&self) -> Vec { + self.handlers + .iter() + .flat_map(|(scheme, handler)| { + let paths = handler.privileged_paths(); + paths + .iter() + .filter_map(move |path| ServoUrl::parse(&format!("{scheme}:{path}")).ok()) + }) + .collect() + } } /// Test if the URL is potentially trustworthy or the custom protocol is registered as secure diff --git a/components/script/dom/servointernals.rs b/components/script/dom/servointernals.rs index b104238a8af..58b572a3eb6 100644 --- a/components/script/dom/servointernals.rs +++ b/components/script/dom/servointernals.rs @@ -8,11 +8,13 @@ use constellation_traits::ScriptToConstellationMessage; use dom_struct::dom_struct; use js::rust::HandleObject; use profile_traits::mem::MemoryReportResult; +use script_bindings::error::{Error, Fallible}; use script_bindings::interfaces::ServoInternalsHelpers; use script_bindings::script_runtime::JSContext; +use script_bindings::str::USVString; +use servo_config::prefs::{self, PrefValue}; use crate::dom::bindings::codegen::Bindings::ServoInternalsBinding::ServoInternalsMethods; -use crate::dom::bindings::error::Error; use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object}; use crate::dom::bindings::root::DomRoot; use crate::dom::globalscope::GlobalScope; @@ -20,6 +22,7 @@ use crate::dom::promise::Promise; use crate::realms::{AlreadyInRealm, InRealm}; use crate::routed_promise::{RoutedPromiseListener, route_promise}; use crate::script_runtime::CanGc; +use crate::script_thread::ScriptThread; #[dom_struct] pub(crate) struct ServoInternals { @@ -55,6 +58,51 @@ impl ServoInternalsMethods for ServoInternals { } promise } + + /// + fn GetBoolPreference(&self, name: USVString) -> Fallible { + if let PrefValue::Bool(b) = prefs::get().get_value(&name) { + return Ok(b); + } + Err(Error::TypeMismatch) + } + + /// + fn GetIntPreference(&self, name: USVString) -> Fallible { + if let PrefValue::Int(i) = prefs::get().get_value(&name) { + return Ok(i); + } + Err(Error::TypeMismatch) + } + + /// + fn GetStringPreference(&self, name: USVString) -> Fallible { + if let PrefValue::Str(s) = prefs::get().get_value(&name) { + return Ok(s.into()); + } + Err(Error::TypeMismatch) + } + + /// + fn SetBoolPreference(&self, name: USVString, value: bool) { + let mut current_prefs = prefs::get().clone(); + current_prefs.set_value(&name, value.into()); + prefs::set(current_prefs); + } + + /// + fn SetIntPreference(&self, name: USVString, value: i64) { + let mut current_prefs = prefs::get().clone(); + current_prefs.set_value(&name, value.into()); + prefs::set(current_prefs); + } + + /// + fn SetStringPreference(&self, name: USVString, value: USVString) { + let mut current_prefs = prefs::get().clone(); + current_prefs.set_value(&name, value.0.into()); + prefs::set(current_prefs); + } } impl RoutedPromiseListener for ServoInternals { @@ -66,14 +114,16 @@ impl RoutedPromiseListener for ServoInternals { } impl ServoInternalsHelpers for ServoInternals { - /// The navigator.servo api is only exposed to about: pages except about:blank + /// The navigator.servo api is exposed to about: pages except about:blank, as + /// well as any URLs provided by embedders that register new protocol handlers. #[allow(unsafe_code)] fn is_servo_internal(cx: JSContext, _global: HandleObject) -> bool { unsafe { let in_realm_proof = AlreadyInRealm::assert_for_cx(cx); let global_scope = GlobalScope::from_context(*cx, InRealm::Already(&in_realm_proof)); let url = global_scope.get_url(); - url.scheme() == "about" && url.as_str() != "about:blank" + (url.scheme() == "about" && url.as_str() != "about:blank") || + ScriptThread::is_servo_privileged(url) } } } diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs index 2d98acfbea9..9e2388d2b5a 100644 --- a/components/script/script_thread.rs +++ b/components/script/script_thread.rs @@ -346,6 +346,10 @@ pub struct ScriptThread { needs_rendering_update: Arc, debugger_global: Dom, + + /// A list of URLs that can access privileged internal APIs. + #[no_trace] + privileged_urls: Vec, } struct BHMExitSignal { @@ -1016,6 +1020,7 @@ impl ScriptThread { scheduled_update_the_rendering: Default::default(), needs_rendering_update: Arc::new(AtomicBool::new(false)), debugger_global: debugger_global.as_traced(), + privileged_urls: state.privileged_urls, } } @@ -4030,6 +4035,10 @@ impl ScriptThread { }; document.event_handler().handle_refresh_cursor(); } + + pub(crate) fn is_servo_privileged(url: ServoUrl) -> bool { + with_script_thread(|script_thread| script_thread.privileged_urls.contains(&url)) + } } impl Drop for ScriptThread { diff --git a/components/script_bindings/webidls/ServoInternals.webidl b/components/script_bindings/webidls/ServoInternals.webidl index 609d49180e4..94bb8b286c7 100644 --- a/components/script_bindings/webidls/ServoInternals.webidl +++ b/components/script_bindings/webidls/ServoInternals.webidl @@ -11,6 +11,13 @@ Func="ServoInternals::is_servo_internal"] interface ServoInternals { Promise reportMemory(); + + [Throws] USVString getStringPreference(USVString name); + [Throws] long long getIntPreference(USVString name); + [Throws] boolean getBoolPreference(USVString name); + undefined setStringPreference(USVString name, USVString value); + undefined setIntPreference(USVString name, long long value); + undefined setBoolPreference(USVString name, boolean value); }; partial interface Navigator { diff --git a/components/servo/lib.rs b/components/servo/lib.rs index 1ab513151f6..eef2b8813cd 100644 --- a/components/servo/lib.rs +++ b/components/servo/lib.rs @@ -1149,6 +1149,8 @@ fn create_constellation( let bluetooth_thread: IpcSender = BluetoothThreadFactory::new(embedder_proxy.clone()); + let privileged_urls = protocols.privileged_urls(); + let (public_resource_threads, private_resource_threads, async_runtime) = new_resource_threads( devtools_sender.clone(), time_profiler_chan.clone(), @@ -1191,6 +1193,7 @@ fn create_constellation( wgpu_image_map, user_content_manager, async_runtime, + privileged_urls, }; let layout_factory = Arc::new(LayoutFactoryImpl()); diff --git a/components/shared/script/lib.rs b/components/shared/script/lib.rs index 6e03d0cbb38..e5051a069af 100644 --- a/components/shared/script/lib.rs +++ b/components/shared/script/lib.rs @@ -354,6 +354,8 @@ pub struct InitialScriptState { pub player_context: WindowGLContext, /// User content manager pub user_content_manager: UserContentManager, + /// A list of URLs that can access privileged internal APIs. + pub privileged_urls: Vec, } /// Errors from executing a paint worklet diff --git a/ports/servoshell/desktop/protocols/servo.rs b/ports/servoshell/desktop/protocols/servo.rs index 465d71a9e08..c6ad9aa0733 100644 --- a/ports/servoshell/desktop/protocols/servo.rs +++ b/ports/servoshell/desktop/protocols/servo.rs @@ -4,22 +4,37 @@ //! Loads resources using a mapping from well-known shortcuts to resource: urls. //! Recognized shortcuts: +//! - servo:default-user-agent +//! - servo:experimental-preferences //! - servo:newtab +//! - servo:preferences use std::future::Future; use std::pin::Pin; +use headers::{ContentType, HeaderMapExt}; use net::fetch::methods::{DoneChannel, FetchContext}; use net::protocols::ProtocolHandler; +use net_traits::ResourceFetchTiming; use net_traits::request::Request; -use net_traits::response::Response; +use net_traits::response::{Response, ResponseBody}; +use servo::config::prefs::UserAgentPlatform; use crate::desktop::protocols::resource::ResourceProtocolHandler; +use crate::prefs::EXPERIMENTAL_PREFS; #[derive(Default)] pub struct ServoProtocolHandler {} impl ProtocolHandler for ServoProtocolHandler { + fn privileged_paths(&self) -> &'static [&'static str] { + &["preferences"] + } + + fn is_fetchable(&self) -> bool { + true + } + fn load( &self, request: &mut Request, @@ -35,9 +50,44 @@ impl ProtocolHandler for ServoProtocolHandler { context, "/newtab.html", ), + + "preferences" => ResourceProtocolHandler::response_for_path( + request, + done_chan, + context, + "/preferences.html", + ), + + "experimental-preferences" => { + let pref_list = EXPERIMENTAL_PREFS + .iter() + .map(|pref| format!("\"{pref}\"")) + .collect::>() + .join(","); + json_response(request, format!("[{pref_list}]")) + }, + + "default-user-agent" => { + let user_agent = UserAgentPlatform::default().to_user_agent_string(); + json_response(request, format!("\"{user_agent}\"")) + }, + _ => Box::pin(std::future::ready(Response::network_internal_error( "Invalid shortcut", ))), } } } + +fn json_response( + request: &Request, + body: String, +) -> Pin + Send>> { + let mut response = Response::new( + request.current_url(), + ResourceFetchTiming::new(request.timing_type()), + ); + response.headers.typed_insert(ContentType::json()); + *response.body.lock().unwrap() = ResponseBody::Done(body.into_bytes()); + Box::pin(std::future::ready(response)) +} diff --git a/ports/servoshell/prefs.rs b/ports/servoshell/prefs.rs index 9c312331c32..4311562074c 100644 --- a/ports/servoshell/prefs.rs +++ b/ports/servoshell/prefs.rs @@ -20,6 +20,25 @@ use servo::servo_geometry::DeviceIndependentPixel; use servo::servo_url::ServoUrl; use url::Url; +pub(crate) static EXPERIMENTAL_PREFS: &[&str] = &[ + "dom_async_clipboard_enabled", + "dom_fontface_enabled", + "dom_intersection_observer_enabled", + "dom_mouse_event_which_enabled", + "dom_navigator_sendbeacon_enabled", + "dom_notification_enabled", + "dom_offscreen_canvas_enabled", + "dom_permissions_enabled", + "dom_resize_observer_enabled", + "dom_trusted_types_enabled", + "dom_webgl2_enabled", + "dom_webgpu_enabled", + "dom_xpath_enabled", + "layout_columns_enabled", + "layout_container_queries_enabled", + "layout_grid_enabled", +]; + #[cfg_attr(any(target_os = "android", target_env = "ohos"), allow(dead_code))] #[derive(Clone)] pub(crate) struct ServoShellPreferences { @@ -587,26 +606,9 @@ pub(crate) fn parse_command_line_arguments(args: Vec) -> ArgumentParsing .collect(); if opt_match.opt_present("enable-experimental-web-platform-features") { - vec![ - "dom_async_clipboard_enabled", - "dom_fontface_enabled", - "dom_intersection_observer_enabled", - "dom_mouse_event_which_enabled", - "dom_navigator_sendbeacon_enabled", - "dom_notification_enabled", - "dom_offscreen_canvas_enabled", - "dom_permissions_enabled", - "dom_resize_observer_enabled", - "dom_trusted_types_enabled", - "dom_webgl2_enabled", - "dom_webgpu_enabled", - "dom_xpath_enabled", - "layout_columns_enabled", - "layout_container_queries_enabled", - "layout_grid_enabled", - ] - .iter() - .for_each(|pref| preferences.set_value(pref, PrefValue::Bool(true))); + for pref in EXPERIMENTAL_PREFS { + preferences.set_value(pref, PrefValue::Bool(true)); + } } // Handle all command-line preferences overrides. diff --git a/resources/resource_protocol/preferences.html b/resources/resource_protocol/preferences.html new file mode 100644 index 00000000000..65ce6f73bba --- /dev/null +++ b/resources/resource_protocol/preferences.html @@ -0,0 +1,123 @@ + + + + servo:preferences + + + +

Preferences

+ +
Loading...
+ + +