Rewild Your Web
18
fork

Configure Feed

Select the types of activity you want to include in your feed.

webtasks: local and remote providers

Signed-off-by: webbeef <me@webbeef.org>

webbeef e64c28bf f9f98022

+2527 -122
+1
Cargo.lock
··· 9048 9048 "petname", 9049 9049 "postcard", 9050 9050 "rand 0.9.2", 9051 + "regex", 9051 9052 "rustc-hash 2.1.2", 9052 9053 "serde", 9053 9054 "serde_json",
+5 -2
patches/components/constellation/Cargo.toml.patch
··· 17 17 keyboard-types = { workspace = true } 18 18 layout_api = { workspace = true } 19 19 log = { workspace = true } 20 - @@ -43,11 +46,14 @@ 20 + @@ -41,13 +44,17 @@ 21 + media = { workspace = true } 22 + net = { workspace = true } 21 23 net_traits = { workspace = true } 24 + +regex = { workspace = true } 22 25 paint_api = { workspace = true } 23 26 parking_lot = { workspace = true } 24 27 +petname = "2.0" ··· 32 35 servo-background-hang-monitor = { workspace = true } 33 36 servo-background-hang-monitor-api = { workspace = true } 34 37 servo-base = { workspace = true } 35 - @@ -61,6 +67,8 @@ 38 + @@ -61,6 +68,8 @@ 36 39 storage_traits = { workspace = true } 37 40 stylo = { workspace = true } 38 41 stylo_traits = { workspace = true }
+483 -40
patches/components/constellation/constellation.rs.patch
··· 8 8 use std::rc::{Rc, Weak}; 9 9 use std::sync::Arc; 10 10 use std::thread::JoinHandle; 11 - @@ -107,12 +108,12 @@ 11 + @@ -99,6 +100,7 @@ 12 + BackgroundHangMonitorControlMsg, BackgroundHangMonitorRegister, HangMonitorAlert, 13 + }; 14 + use content_security_policy::sandboxing_directive::SandboxingFlagSet; 15 + +use content_security_policy::url::Url; 16 + use crossbeam_channel::{Receiver, Select, Sender, unbounded}; 17 + use devtools_traits::{ 18 + ChromeToDevtoolsControlMsg, DevtoolsControlMsg, DevtoolsPageInfo, NavigationState, 19 + @@ -107,12 +109,12 @@ 12 20 use embedder_traits::resources::{self, Resource}; 13 21 use embedder_traits::user_contents::{UserContentManagerId, UserContents}; 14 22 use embedder_traits::{ ··· 27 35 }; 28 36 use euclid::Size2D; 29 37 use euclid::default::Size2D as UntypedSize2D; 30 - @@ -159,12 +160,14 @@ 38 + @@ -159,12 +161,14 @@ 31 39 use servo_canvas_traits::webgl::WebGLThreads; 32 40 use servo_config::{opts, pref}; 33 41 use servo_constellation_traits::{ ··· 48 56 }; 49 57 use servo_url::{Host, ImmutableOrigin, ServoUrl}; 50 58 use storage_traits::StorageThreads; 51 - @@ -178,6 +181,7 @@ 59 + @@ -178,6 +182,7 @@ 52 60 use webgpu_traits::{WebGPU, WebGPURequest}; 53 61 54 62 use super::embedder::ConstellationToEmbedderMsg; ··· 56 64 use crate::broadcastchannel::BroadcastChannels; 57 65 use crate::browsingcontext::{ 58 66 AllBrowsingContextsIterator, BrowsingContext, FullyActiveBrowsingContextsIterator, 59 - @@ -185,6 +189,7 @@ 67 + @@ -185,10 +190,12 @@ 60 68 }; 61 69 use crate::constellation_webview::ConstellationWebView; 62 70 use crate::event_loop::EventLoop; ··· 64 72 use crate::pipeline::Pipeline; 65 73 use crate::process_manager::ProcessManager; 66 74 use crate::serviceworker::ServiceWorkerUnprivilegedContent; 67 - @@ -213,6 +218,9 @@ 75 + use crate::session_history::{NeedsToReload, SessionHistoryChange, SessionHistoryDiff}; 76 + +use crate::tasks; 77 + 78 + type PendingApprovalNavigations = FxHashMap<PipelineId, (LoadData, NavigationHistoryBehavior)>; 79 + 80 + @@ -213,6 +220,12 @@ 68 81 /// While a completion failed, another global requested to complete the transfer. 69 82 /// We are still buffering messages, and awaiting the return of the buffer from the global who failed. 70 83 CompletionRequested(MessagePortRouterId, VecDeque<PortMessageTask>), 71 84 + /// The port is managed by a remote P2P peer. 72 85 + /// Messages routed to this port are serialized and sent over the P2P link. 73 86 + Remote(String), 87 + + /// The port is a virtual caller-side port for a web task delegation. 88 + + /// Messages routed to this port resolve the caller's promise via the stored callback. 89 + + Task(String), 74 90 } 75 91 76 92 #[derive(Debug)] 77 - @@ -514,6 +522,25 @@ 93 + @@ -514,6 +527,31 @@ 78 94 /// to the `UserContents` need to be forwared to all the `ScriptThread`s that host 79 95 /// the relevant `WebView`. 80 96 pub(crate) user_contents_for_manager_id: FxHashMap<UserContentManagerId, UserContents>, ··· 97 113 + 98 114 + /// The main process side of the ATProdo DOM API. 99 115 + at_proto: AtProtoManager, 116 + + 117 + + /// Registry of web task providers for the delegation system. 118 + + task_registry: tasks::TaskRegistry, 119 + + 120 + + /// Pending web task requests awaiting provider selection or result. 121 + + pending_task_requests: HashMap<String, tasks::PendingTaskRequest>, 100 122 } 101 123 102 124 /// State needed to construct a constellation. 103 - @@ -574,6 +601,9 @@ 125 + @@ -574,6 +612,9 @@ 104 126 105 127 /// The async runtime. 106 128 pub async_runtime: Box<dyn AsyncRuntime>, ··· 110 132 } 111 133 112 134 /// When we are exiting a pipeline, we can either force exiting or not. A normal exit 113 - @@ -683,7 +713,7 @@ 135 + @@ -683,7 +724,7 @@ 114 136 script_to_devtools_callback: Default::default(), 115 137 #[cfg(feature = "bluetooth")] 116 138 bluetooth_ipc_sender: state.bluetooth_thread, ··· 119 141 private_resource_threads: state.private_resource_threads, 120 142 public_storage_threads: state.public_storage_threads, 121 143 private_storage_threads: state.private_storage_threads, 122 - @@ -735,6 +765,13 @@ 144 + @@ -735,6 +776,15 @@ 123 145 pending_viewport_changes: Default::default(), 124 146 screenshot_readiness_requests: Vec::new(), 125 147 user_contents_for_manager_id: Default::default(), ··· 130 152 + at_proto: AtProtoManager::new( 131 153 + state.public_resource_threads.core_thread, 132 154 + ), 155 + + task_registry: tasks::TaskRegistry::new(state.config_dir.as_deref()), 156 + + pending_task_requests: HashMap::new(), 133 157 }; 134 158 135 159 constellation.run(); 136 - @@ -760,6 +797,18 @@ 160 + @@ -760,6 +810,18 @@ 137 161 fn clean_up_finished_script_event_loops(&mut self) { 138 162 self.event_loop_join_handles 139 163 .retain(|join_handle| !join_handle.is_finished()); ··· 152 176 self.event_loops 153 177 .retain(|event_loop| event_loop.upgrade().is_some()); 154 178 } 155 - @@ -1052,6 +1101,11 @@ 179 + @@ -1052,6 +1114,11 @@ 156 180 .get(&webview_id) 157 181 .and_then(|webview| webview.user_content_manager_id); 158 182 ··· 164 188 let new_pipeline_info = NewPipelineInfo { 165 189 parent_info: parent_pipeline_id, 166 190 new_pipeline_id, 167 - @@ -1062,6 +1116,13 @@ 191 + @@ -1062,6 +1129,13 @@ 168 192 viewport_details: initial_viewport_details, 169 193 user_content_manager_id, 170 194 theme, ··· 178 202 }; 179 203 let pipeline = match Pipeline::spawn(new_pipeline_info, event_loop, self, throttled) { 180 204 Ok(pipeline) => pipeline, 181 - @@ -1228,6 +1289,7 @@ 205 + @@ -1228,6 +1302,7 @@ 182 206 BackgroundHangMonitor(HangMonitorAlert), 183 207 Embedder(EmbedderToConstellationMessage), 184 208 FromSWManager(SWManagerMsg), ··· 186 210 RemoveProcess(usize), 187 211 } 188 212 // Get one incoming request. 189 - @@ -1248,6 +1310,15 @@ 213 + @@ -1248,6 +1323,15 @@ 190 214 sel.recv(&self.embedder_to_constellation_receiver); 191 215 sel.recv(&self.swmanager_receiver); 192 216 ··· 202 226 self.process_manager.register(&mut sel); 203 227 204 228 let request = { 205 - @@ -1276,9 +1347,13 @@ 229 + @@ -1276,9 +1360,13 @@ 206 230 .recv(&self.swmanager_receiver) 207 231 .expect("Unexpected SW channel panic in constellation") 208 232 .map(Request::FromSWManager), ··· 217 241 let _ = oper.recv(self.process_manager.receiver_at(process_index)); 218 242 Ok(Request::RemoveProcess(process_index)) 219 243 }, 220 - @@ -1304,6 +1379,9 @@ 244 + @@ -1304,6 +1392,9 @@ 221 245 Request::FromSWManager(message) => { 222 246 self.handle_request_from_swmanager(message); 223 247 }, ··· 227 251 Request::RemoveProcess(index) => self.process_manager.remove(index), 228 252 } 229 253 } 230 - @@ -1532,11 +1610,7 @@ 254 + @@ -1532,11 +1623,7 @@ 231 255 } 232 256 }, 233 257 EmbedderToConstellationMessage::PreferencesUpdated(updates) => { ··· 240 264 let _ = event_loop.send(ScriptThreadMessage::PreferencesUpdated( 241 265 updates 242 266 .iter() 243 - @@ -1563,6 +1637,18 @@ 267 + @@ -1563,6 +1650,18 @@ 244 268 EmbedderToConstellationMessage::SetAccessibilityActive(webview_id, active) => { 245 269 self.set_accessibility_active(webview_id, active); 246 270 }, ··· 259 283 } 260 284 } 261 285 262 - @@ -1760,7 +1846,13 @@ 286 + @@ -1760,7 +1859,13 @@ 263 287 return warn!("Attempt to add channel name from an unexpected origin."); 264 288 } 265 289 self.broadcast_channels ··· 274 298 }, 275 299 ScriptToConstellationMessage::RemoveBroadcastChannelNameInRouter( 276 300 router_id, 277 - @@ -1774,7 +1866,13 @@ 301 + @@ -1774,7 +1879,13 @@ 278 302 return warn!("Attempt to remove channel name from an unexpected origin."); 279 303 } 280 304 self.broadcast_channels ··· 289 313 }, 290 314 ScriptToConstellationMessage::RemoveBroadcastChannelRouter(router_id, origin) => { 291 315 if self 292 - @@ -1786,6 +1884,12 @@ 316 + @@ -1786,6 +1897,12 @@ 293 317 self.broadcast_channels 294 318 .remove_broadcast_channel_router(router_id); 295 319 }, ··· 302 326 ScriptToConstellationMessage::ScheduleBroadcast(router_id, message) => { 303 327 if self 304 328 .check_origin_against_pipeline(&source_pipeline_id, &message.origin) 305 - @@ -1795,8 +1899,15 @@ 329 + @@ -1795,8 +1912,15 @@ 306 330 "Attempt to schedule broadcast from an origin not matching the origin of the msg." 307 331 ); 308 332 } ··· 319 343 }, 320 344 ScriptToConstellationMessage::PipelineExited => { 321 345 self.handle_pipeline_exited(source_pipeline_id); 322 - @@ -1816,6 +1927,12 @@ 346 + @@ -1816,6 +1940,12 @@ 323 347 ScriptToConstellationMessage::CreateAuxiliaryWebView(load_info) => { 324 348 self.handle_script_new_auxiliary(load_info); 325 349 }, ··· 332 356 ScriptToConstellationMessage::ChangeRunningAnimationsState(animation_state) => { 333 357 self.handle_change_running_animations_state(source_pipeline_id, animation_state) 334 358 }, 335 - @@ -1989,6 +2106,29 @@ 359 + @@ -1862,7 +1992,7 @@ 360 + ScriptToConstellationMessage::SetFinalUrl(final_url) => { 361 + // The script may have finished loading after we already started shutting down. 362 + if let Some(ref mut pipeline) = self.pipelines.get_mut(&source_pipeline_id) { 363 + - pipeline.url = final_url; 364 + + pipeline.url = final_url.clone(); 365 + } else { 366 + warn!("constellation got set final url message for dead pipeline"); 367 + } 368 + @@ -1989,6 +2119,29 @@ 336 369 new_value, 337 370 ); 338 371 }, ··· 362 395 ScriptToConstellationMessage::MediaSessionEvent(pipeline_id, event) => { 363 396 // Unlikely at this point, but we may receive events coming from 364 397 // different media sessions, so we set the active media session based 365 - @@ -2008,7 +2148,12 @@ 398 + @@ -2008,7 +2161,12 @@ 366 399 } 367 400 self.active_media_session = Some(pipeline_id); 368 401 self.constellation_to_embedder_proxy.send( ··· 376 409 ); 377 410 }, 378 411 #[cfg(feature = "webgpu")] 379 - @@ -2063,9 +2208,412 @@ 412 + @@ -2063,7 +2221,769 @@ 380 413 let _ = event_loop.send(ScriptThreadMessage::TriggerGarbageCollection); 381 414 } 382 415 }, ··· 611 644 + ScriptToConstellationMessage::AtProto(request, response) => { 612 645 + self.at_proto.process_request(request, response); 613 646 + }, 614 - } 615 - } 616 - 647 + + ScriptToConstellationMessage::RequestTask( 648 + + task_name, 649 + + caller_data_json, 650 + + _display, 651 + + data, 652 + + provider_port_id, 653 + + callback, 654 + + ) => { 655 + + debug!("RequestTask: {task_name}"); 656 + + 657 + + // Parse caller data for filter matching. 658 + + let caller_data: HashMap<String, serde_json::Value> = 659 + + serde_json::from_str(&caller_data_json).unwrap_or_default(); 660 + + 661 + + // Look up matching providers in the registry. 662 + + let provider_infos = self 663 + + .task_registry 664 + + .get_provider_infos(&task_name, &caller_data); 665 + + let providers_json = 666 + + serde_json::to_string(&provider_infos).unwrap_or_else(|_| "[]".into()); 667 + + let request_id = format!("task-{webview_id:?}-{source_pipeline_id:?}"); 668 + + 669 + + // Register provider_port_id as a Task port in the constellation. 670 + + // When the provider posts on its entangled port, the message arrives here 671 + + // and gets routed through the callback to resolve the caller's promise. 672 + + self.message_ports.insert( 673 + + provider_port_id, 674 + + MessagePortInfo { 675 + + state: TransferState::Task(request_id.clone()), 676 + + entangled_with: None, 677 + + }, 678 + + ); 679 + + 680 + + // Store the pending request with data and callback. 681 + + self.pending_task_requests.insert( 682 + + request_id.clone(), 683 + + tasks::PendingTaskRequest { 684 + + task_name: task_name.clone(), 685 + + data, 686 + + callback, 687 + + provider: None, 688 + + provider_port_id, 689 + + provider_url: None, 690 + + dispatched: false, 691 + + provider_webview_id: None, 692 + + remote_caller_peer_id: None, 693 + + remote_providers: HashMap::new(), 694 + + }, 695 + + ); 696 + + 697 + + // Send to all script threads — the system UI will handle it 698 + + // via navigator.embedder.ontaskrequest. 699 + + for event_loop in self.event_loops() { 700 + + let _ = event_loop.send(ScriptThreadMessage::ShowTaskChooser( 701 + + request_id.clone(), 702 + + task_name.clone(), 703 + + providers_json.clone(), 704 + + )); 705 + + } 706 + + 707 + + // Broadcast TaskQuery to paired connected devices for remote provider discovery. 708 + + self.pairing.broadcast_message(&P2pMessage::TaskQuery { 709 + + request_id: request_id.clone(), 710 + + task_name: task_name.clone(), 711 + + caller_data_json: caller_data_json.clone(), 712 + + }); 713 + + }, 714 + + ScriptToConstellationMessage::RegisterTaskProvider( 715 + + task_name, 716 + + title, 717 + + description, 718 + + href, 719 + + filters_json, 720 + + returns_type, 721 + + display_mode, 722 + + icon, 723 + + ) => { 724 + + debug!("RegisterTaskProvider: {task_name} -> {href}"); 725 + + let display = match display_mode.as_str() { 726 + + "inline" => tasks::TaskDisplayMode::Inline, 727 + + _ => tasks::TaskDisplayMode::Window, 728 + + }; 729 + + let filters = serde_json::from_str(&filters_json).unwrap_or_default(); 730 + + if let Ok(url) = ServoUrl::parse(&href) { 731 + + self.task_registry.register(tasks::TaskProvider { 732 + + task_name, 733 + + title, 734 + + description, 735 + + href: url, 736 + + filters, 737 + + returns_type, 738 + + display, 739 + + icon, 740 + + }); 741 + + } 742 + + }, 743 + + ScriptToConstellationMessage::TaskProviderSelected(request_id, provider_id) => { 744 + + debug!("TaskProviderSelected: {request_id} -> {provider_id:?}"); 745 + + 746 + + let Some(mut pending) = self.pending_task_requests.remove(&request_id) else { 747 + + warn!("TaskProviderSelected: unknown request {request_id}"); 748 + + return; 749 + + }; 750 + + 751 + + let Some(provider_id) = provider_id else { 752 + + // User cancelled. 753 + + let _ = pending.callback.send(Err("Task cancelled".into())); 754 + + return; 755 + + }; 756 + + 757 + + // Check if this is a remote provider (ID starts with "remote:{peer_id}:"). 758 + + if provider_id.starts_with("remote:") { 759 + + let parts: Vec<&str> = provider_id.splitn(3, ':').collect(); 760 + + if parts.len() >= 3 { 761 + + let peer_id = parts[1]; 762 + + 763 + + // Look up the remote provider's href from stored info. 764 + + let provider_href = pending 765 + + .remote_providers 766 + + .get(&provider_id) 767 + + .map(|p| p.href.clone()) 768 + + .unwrap_or_default(); 769 + + 770 + + // Serialize the caller's data for P2P transfer. 771 + + let caller_data = pending 772 + + .data 773 + + .as_ref() 774 + + .and_then(|d| postcard::to_allocvec(d).ok()) 775 + + .unwrap_or_default(); 776 + + 777 + + self.pairing.send_message( 778 + + peer_id, 779 + + &P2pMessage::TaskExecute { 780 + + request_id: request_id.clone(), 781 + + task_name: pending.task_name.clone(), 782 + + provider_href, 783 + + caller_data, 784 + + }, 785 + + ); 786 + + 787 + + // Keep the pending request for when TaskResult arrives. 788 + + self.pending_task_requests 789 + + .insert(request_id.clone(), pending); 790 + + } 791 + + return; 792 + + } 793 + + 794 + + // Resolve the provider ID to a TaskProvider. 795 + + let Some(provider) = self.task_registry.resolve_provider(&provider_id).cloned() 796 + + else { 797 + + let _ = pending 798 + + .callback 799 + + .send(Err(format!("Unknown provider: {provider_id}"))); 800 + + return; 801 + + }; 802 + + 803 + + // Store the pending request back (with provider info) for when 804 + + // the provider webview opens and later completes. 805 + + pending.provider = Some(provider.clone()); 806 + + 807 + + // Build the provider URL with the request ID as a query param 808 + + // so the provider pipeline can be correlated. 809 + + let mut provider_url = provider.href.clone().into_url(); 810 + + provider_url 811 + + .query_pairs_mut() 812 + + .append_pair("taskRequestId", &request_id); 813 + + 814 + + pending.provider_url = Some(provider_url.to_string()); 815 + + self.pending_task_requests 816 + + .insert(request_id.clone(), pending); 817 + + 818 + + // Tell the system UI to open the provider webview. 819 + + for event_loop in self.event_loops() { 820 + + let _ = event_loop.send(ScriptThreadMessage::OpenTaskProvider( 821 + + request_id.clone(), 822 + + provider_url.to_string(), 823 + + provider.title.clone(), 824 + + )); 825 + + } 826 + + }, 827 + + ScriptToConstellationMessage::AcceptTask(callback) => { 828 + + // Find a pending task request whose provider URL matches this pipeline's URL. 829 + + let pipeline_url = self 830 + + .pipelines 831 + + .get(&source_pipeline_id) 832 + + .map(|p| p.url.as_str().to_string()); 833 + + 834 + + let matching_request = pipeline_url.and_then(|url| { 835 + + self.pending_task_requests 836 + + .iter() 837 + + .find(|(_, p)| p.provider_url.as_deref() == Some(&url) && !p.dispatched) 838 + + .map(|(id, _)| id.clone()) 839 + + }); 840 + + 841 + + if let Some(request_id) = matching_request { 842 + + if let Some(pending) = self.pending_task_requests.get_mut(&request_id) { 843 + + pending.dispatched = true; 844 + + pending.provider_webview_id = Some(webview_id); 845 + + let port_id_bytes = 846 + + postcard::to_allocvec(&pending.provider_port_id).unwrap_or_default(); 847 + + let task_name = pending.task_name.clone(); 848 + + let data = pending.data.take(); 849 + + 850 + + let _ = callback.send(Some((task_name, data, port_id_bytes))); 851 + + } else { 852 + + let _ = callback.send(None); 853 + + } 854 + + } else { 855 + + // No matching task request — this page is not a task provider. 856 + + let _ = callback.send(None); 857 + + } 858 + + }, 859 + + } 860 + + } 861 + + 617 862 + fn handle_pairing_event(&mut self, event: PairingEvent) { 618 863 + if let PairingEvent::MessageReceived { ref from, ref data } = event { 619 864 + debug!("P2P message received from {from}, {} bytes", data.len()); ··· 749 994 + self.message_ports.remove(&port_id); 750 995 + } 751 996 + }, 997 + + P2pMessage::TaskQuery { 998 + + ref request_id, 999 + + ref task_name, 1000 + + ref caller_data_json, 1001 + + } => { 1002 + + // Remote device is asking if we have matching providers. 1003 + + let caller_data: HashMap<String, serde_json::Value> = 1004 + + serde_json::from_str(caller_data_json).unwrap_or_default(); 1005 + + let provider_infos = self 1006 + + .task_registry 1007 + + .get_provider_infos(task_name, &caller_data); 1008 + + let providers_json = 1009 + + serde_json::to_string(&provider_infos).unwrap_or_else(|_| "[]".into()); 1010 + + let device_name = self.pairing.get_local_name_sync(); 1011 + + self.pairing.send_message( 1012 + + &from, 1013 + + &P2pMessage::TaskQueryResponse { 1014 + + request_id: request_id.clone(), 1015 + + providers_json, 1016 + + device_name, 1017 + + }, 1018 + + ); 1019 + + }, 1020 + + P2pMessage::TaskQueryResponse { 1021 + + ref request_id, 1022 + + ref providers_json, 1023 + + ref device_name, 1024 + + } => { 1025 + + // Remote device responded with matching providers. 1026 + + // Parse and send update to the system UI. 1027 + + if let Ok(mut remote_providers) = 1028 + + serde_json::from_str::<Vec<tasks::TaskProviderInfo>>(providers_json) 1029 + + { 1030 + + // Tag each provider with the remote device info. 1031 + + for p in &mut remote_providers { 1032 + + p.device_name = Some(device_name.clone()); 1033 + + p.remote_peer_id = Some(from.clone()); 1034 + + // Prefix the ID to avoid collisions with local providers. 1035 + + p.id = format!("remote:{}:{}", from, p.id); 1036 + + } 1037 + + if !remote_providers.is_empty() { 1038 + + // Store remote providers for later lookup when user selects one. 1039 + + if let Some(pending) = 1040 + + self.pending_task_requests.get_mut(request_id) 1041 + + { 1042 + + for p in &remote_providers { 1043 + + pending.remote_providers.insert(p.id.clone(), p.clone()); 1044 + + } 1045 + + } 1046 + + 1047 + + let update_json = serde_json::to_string(&remote_providers) 1048 + + .unwrap_or_else(|_| "[]".into()); 1049 + + // Send update to system UI via ScriptThreadMessage. 1050 + + for event_loop in self.event_loops() { 1051 + + let _ = 1052 + + event_loop.send(ScriptThreadMessage::TaskProvidersUpdate( 1053 + + request_id.clone(), 1054 + + update_json.clone(), 1055 + + )); 1056 + + } 1057 + + } 1058 + + } 1059 + + }, 1060 + + P2pMessage::TaskExecute { 1061 + + ref request_id, 1062 + + ref task_name, 1063 + + ref provider_href, 1064 + + ref caller_data, 1065 + + } => { 1066 + + // Remote device wants us to execute a task. 1067 + + debug!("TaskExecute from {from}: {task_name} -> {provider_href}"); 1068 + + let data: Option<StructuredSerializedData> = 1069 + + postcard::from_bytes(caller_data).ok(); 1070 + + let provider_port_id = MessagePortId::new(); 1071 + + 1072 + + // Register the provider port as a remote-task port. 1073 + + // When the provider posts a result, we'll send it back to the caller. 1074 + + self.message_ports.insert( 1075 + + provider_port_id, 1076 + + MessagePortInfo { 1077 + + state: TransferState::Task(request_id.clone()), 1078 + + entangled_with: None, 1079 + + }, 1080 + + ); 1081 + + 1082 + + // Create a no-op callback — the actual result will be sent 1083 + + // back via P2P in the TransferState::Task handler. 1084 + + let noop_callback = GenericCallback::new( 1085 + + move |_: Result<Result<StructuredSerializedData, String>, _>| {}, 1086 + + ) 1087 + + .expect("Could not create callback"); 1088 + + 1089 + + // Store the pending request with the remote caller's peer ID. 1090 + + let mut provider_url = Url::parse(provider_href) 1091 + + .unwrap_or_else(|_| Url::parse("about:blank").unwrap()); 1092 + + provider_url 1093 + + .query_pairs_mut() 1094 + + .append_pair("taskRequestId", request_id); 1095 + + 1096 + + self.pending_task_requests.insert( 1097 + + request_id.clone(), 1098 + + tasks::PendingTaskRequest { 1099 + + task_name: task_name.clone(), 1100 + + data, 1101 + + callback: noop_callback, 1102 + + provider: None, 1103 + + provider_port_id, 1104 + + provider_url: Some(provider_url.to_string()), 1105 + + dispatched: false, 1106 + + provider_webview_id: None, 1107 + + remote_caller_peer_id: Some(from.clone()), 1108 + + remote_providers: HashMap::new(), 1109 + + }, 1110 + + ); 1111 + + 1112 + + // Tell the system UI to open the provider webview. 1113 + + for event_loop in self.event_loops() { 1114 + + let _ = event_loop.send(ScriptThreadMessage::OpenTaskProvider( 1115 + + request_id.clone(), 1116 + + provider_url.to_string(), 1117 + + task_name.clone(), 1118 + + )); 1119 + + } 1120 + + }, 1121 + + P2pMessage::TaskResult { 1122 + + ref request_id, 1123 + + success, 1124 + + ref result_data, 1125 + + } => { 1126 + + // Remote device returned a task result. 1127 + + debug!("TaskResult from {from}: {request_id} success={success}"); 1128 + + if let Some(pending) = self.pending_task_requests.remove(request_id) { 1129 + + if success { 1130 + + if let Ok(data) = 1131 + + postcard::from_bytes::<StructuredSerializedData>(result_data) 1132 + + { 1133 + + let _ = pending.callback.send(Ok(data)); 1134 + + } else { 1135 + + let _ = pending.callback.send(Err( 1136 + + "Failed to deserialize remote task result".into(), 1137 + + )); 1138 + + } 1139 + + } else { 1140 + + let _ = pending.callback.send(Err("Remote task cancelled".into())); 1141 + + } 1142 + + } 1143 + + }, 752 1144 + _ => {}, 753 1145 + } 754 1146 + } ··· 758 1150 + // Handle peer disconnect: clean up remote channel state. 759 1151 + if let PairingEvent::PeerExpired { ref id } = event { 760 1152 + self.pairing.clear_remote_peer(id); 761 - + } 1153 + } 762 1154 + 763 1155 + // When a peer connects or reconnects, sync our open broadcast channels to it. 764 1156 + if let PairingEvent::PeerDiscovered { ref id, .. } | ··· 784 1176 + let _ = event_loop.send(ScriptThreadMessage::DispatchPairingEvent(event.clone())); 785 1177 + } 786 1178 + } 787 - + } 788 - + 1179 + } 1180 + 789 1181 /// Check the origin of a message against that of the pipeline it came from. 790 - /// Note: this is still limited as a security check, 791 - /// see <https://github.com/servo/servo/issues/11722> 792 - @@ -2382,6 +2930,29 @@ 1182 + @@ -2382,6 +3302,55 @@ 793 1183 TransferState::TransferInProgress(queue) => queue.push_back(task), 794 1184 TransferState::CompletionFailed(queue) => queue.push_back(task), 795 1185 TransferState::CompletionRequested(_, queue) => queue.push_back(task), ··· 816 1206 + }, 817 1207 + } 818 1208 + }, 1209 + + TransferState::Task(request_id) => { 1210 + + // The provider posted a result on its port. 1211 + + let request_id = request_id.clone(); 1212 + + if let Some(pending) = self.pending_task_requests.remove(&request_id) { 1213 + + if let Some(ref remote_peer_id) = pending.remote_caller_peer_id { 1214 + + // Remote task: send result back via P2P. 1215 + + let result_data = postcard::to_allocvec(&task.data).unwrap_or_default(); 1216 + + self.pairing.send_message( 1217 + + remote_peer_id, 1218 + + &P2pMessage::TaskResult { 1219 + + request_id: request_id.clone(), 1220 + + success: true, 1221 + + result_data, 1222 + + }, 1223 + + ); 1224 + + } else { 1225 + + // Local task: resolve the caller's promise directly. 1226 + + let _ = pending.callback.send(Ok(task.data)); 1227 + + } 1228 + + 1229 + + // Close the provider webview. 1230 + + if let Some(provider_webview_id) = pending.provider_webview_id { 1231 + + self.handle_close_top_level_browsing_context(provider_webview_id); 1232 + + } 1233 + + } 1234 + + }, 819 1235 } 820 1236 } 821 1237 822 - @@ -3273,6 +3844,13 @@ 1238 + @@ -3273,6 +4242,40 @@ 823 1239 /// <https://html.spec.whatwg.org/multipage/#destroy-a-top-level-traversable> 824 1240 fn handle_close_top_level_browsing_context(&mut self, webview_id: WebViewId) { 825 1241 debug!("{webview_id}: Closing"); 826 1242 + 1243 + + // If this is a task provider webview, reject the caller's promise. 1244 + + let task_request_id = self 1245 + + .pending_task_requests 1246 + + .iter() 1247 + + .find(|(_, p)| p.provider_webview_id == Some(webview_id)) 1248 + + .map(|(id, _)| id.clone()); 1249 + + if let Some(request_id) = task_request_id { 1250 + + if let Some(pending) = self.pending_task_requests.remove(&request_id) { 1251 + + if let Some(ref remote_peer_id) = pending.remote_caller_peer_id { 1252 + + // Remote task: send cancellation back via P2P. 1253 + + self.pairing.send_message( 1254 + + remote_peer_id, 1255 + + &P2pMessage::TaskResult { 1256 + + request_id: request_id.clone(), 1257 + + success: false, 1258 + + result_data: vec![], 1259 + + }, 1260 + + ); 1261 + + } else { 1262 + + // Local task: reject the caller's promise. 1263 + + let _ = pending 1264 + + .callback 1265 + + .send(Err("Task provider closed without completing".into())); 1266 + + } 1267 + + } 1268 + + } 1269 + + 827 1270 + // Notify embedded webview parent before closing (if this is an embedded webview) 828 1271 + self.handle_embedded_webview_notification(webview_id, EmbeddedWebViewEventType::Closed); 829 1272 + ··· 833 1276 let browsing_context_id = BrowsingContextId::from(webview_id); 834 1277 // Step 5. Remove traversable from the user agent's top-level traversable set. 835 1278 let browsing_context = 836 - @@ -3547,8 +4125,27 @@ 1279 + @@ -3547,8 +4550,27 @@ 837 1280 opener_webview_id, 838 1281 opener_pipeline_id, 839 1282 response_sender, ··· 861 1304 let Some((webview_id_sender, webview_id_receiver)) = generic_channel::channel() else { 862 1305 warn!("Failed to create channel"); 863 1306 let _ = response_sender.send(None); 864 - @@ -3649,6 +4246,397 @@ 1307 + @@ -3649,6 +4671,397 @@ 865 1308 }); 866 1309 } 867 1310 ··· 1259 1702 #[servo_tracing::instrument(skip_all)] 1260 1703 fn handle_refresh_cursor(&self, pipeline_id: PipelineId) { 1261 1704 let Some(pipeline) = self.pipelines.get(&pipeline_id) else { 1262 - @@ -4776,7 +5764,7 @@ 1705 + @@ -4776,7 +6189,7 @@ 1263 1706 } 1264 1707 1265 1708 #[servo_tracing::instrument(skip_all)] ··· 1268 1711 // Send a flat projection of the history to embedder. 1269 1712 // The final vector is a concatenation of the URLs of the past 1270 1713 // entries, the current entry and the future entries. 1271 - @@ -4880,9 +5868,22 @@ 1714 + @@ -4880,9 +6293,22 @@ 1272 1715 self.constellation_to_embedder_proxy 1273 1716 .send(ConstellationToEmbedderMsg::HistoryChanged( 1274 1717 webview_id,
+7 -1
patches/components/constellation/lib.rs.patch
··· 8 8 mod broadcastchannel; 9 9 mod browsingcontext; 10 10 mod constellation; 11 - @@ -14,6 +15,7 @@ 11 + @@ -14,11 +15,13 @@ 12 12 mod embedder; 13 13 mod event_loop; 14 14 mod logging; ··· 16 16 mod pipeline; 17 17 mod process_manager; 18 18 mod sandboxing; 19 + mod serviceworker; 20 + mod session_history; 21 + +mod tasks; 22 + 23 + pub use crate::constellation::{Constellation, InitialConstellationState}; 24 + pub use crate::embedder::ConstellationToEmbedderMsg;
+47 -1
patches/components/constellation/pairing.rs.patch
··· 1 1 --- original 2 2 +++ modified 3 - @@ -0,0 +1,816 @@ 3 + @@ -0,0 +1,862 @@ 4 4 +// SPDX-License-Identifier: AGPL-3.0-or-later 5 5 + 6 6 +//! P2P pairing service integration with the constellation. ··· 64 64 + }, 65 65 + /// Deny a port offer — the remote side refused the stream. 66 66 + PortOfferDenied { stream_id: String }, 67 + + 68 + + // Web Tasks P2P messages 69 + + /// Query remote device for matching task providers. 70 + + TaskQuery { 71 + + request_id: String, 72 + + task_name: String, 73 + + caller_data_json: String, 74 + + }, 75 + + /// Response with matching providers from the remote device. 76 + + TaskQueryResponse { 77 + + request_id: String, 78 + + providers_json: String, 79 + + device_name: String, 80 + + }, 81 + + /// Request remote device to execute a task with a specific provider. 82 + + TaskExecute { 83 + + request_id: String, 84 + + task_name: String, 85 + + provider_href: String, 86 + + caller_data: Vec<u8>, 87 + + }, 88 + + /// Result from a remotely executed task. 89 + + TaskResult { 90 + + request_id: String, 91 + + success: bool, 92 + + result_data: Vec<u8>, 93 + + }, 67 94 +} 68 95 + 69 96 +impl P2pMessage { ··· 96 123 + event_receiver: None, 97 124 + remote_channels: Default::default(), 98 125 + confirmed_peers: Default::default(), 126 + + } 127 + + } 128 + + 129 + + /// Get the local device name synchronously (best effort). 130 + + pub(crate) fn get_local_name_sync(&self) -> String { 131 + + let local_info = self.local_info.clone(); 132 + + match local_info.try_lock() { 133 + + Ok(guard) => guard 134 + + .as_ref() 135 + + .map(|info| info.name.clone()) 136 + + .unwrap_or_else(|| "Unknown device".to_string()), 137 + + Err(_) => "Unknown device".to_string(), 99 138 + } 100 139 + } 101 140 + ··· 622 661 + P2pMessage::PortClose { .. } | 623 662 + P2pMessage::PortOfferDenied { .. } => { 624 663 + // Return to constellation for port routing. 664 + + Some((from.to_owned(), message)) 665 + + }, 666 + + P2pMessage::TaskQuery { .. } | 667 + + P2pMessage::TaskQueryResponse { .. } | 668 + + P2pMessage::TaskExecute { .. } | 669 + + P2pMessage::TaskResult { .. } => { 670 + + // Return to constellation for task delegation handling. 625 671 + Some((from.to_owned(), message)) 626 672 + }, 627 673 + }
+518
patches/components/constellation/tasks.rs.patch
··· 1 + --- original 2 + +++ modified 3 + @@ -0,0 +1,515 @@ 4 + +// SPDX-License-Identifier: AGPL-3.0-or-later 5 + + 6 + +//! Task provider registry for the Web Tasks delegation system. 7 + +//! 8 + +//! The constellation maintains a registry of task providers. Providers can be 9 + +//! registered by privileged system pages or discovered from web page meta tags. 10 + + 11 + +use std::collections::HashMap; 12 + +use std::fs; 13 + +use std::path::{Path, PathBuf}; 14 + + 15 + +use log::{debug, error}; 16 + +use serde::{Deserialize, Serialize}; 17 + +use servo_base::generic_channel::GenericCallback; 18 + +use servo_base::id::{MessagePortId, WebViewId}; 19 + +use servo_constellation_traits::StructuredSerializedData; 20 + +use servo_url::ServoUrl; 21 + + 22 + +/// How a task provider should be displayed when launched. 23 + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] 24 + +pub enum TaskDisplayMode { 25 + + /// Shown as a webview overlay on top of the caller's webview. 26 + + Inline, 27 + + /// Opened in a new webview (full navigation). 28 + + Window, 29 + +} 30 + + 31 + +/// A filter value for task provider matching. 32 + +/// Follows the MozActivities filter spec. 33 + +#[derive(Clone, Debug, Deserialize, Serialize)] 34 + +#[serde(untagged)] 35 + +pub enum FilterValue { 36 + + /// A basic value: optional field, must equal this if present. 37 + + String(String), 38 + + /// A numeric value. 39 + + Number(f64), 40 + + /// An array of allowed values: optional field, must equal one if present. 41 + + StringArray(Vec<String>), 42 + + /// A filter definition object with detailed matching rules. 43 + + Object(FilterObject), 44 + +} 45 + + 46 + +/// Detailed filter rules for a single field. 47 + +#[derive(Clone, Debug, Deserialize, Serialize)] 48 + +pub struct FilterObject { 49 + + /// If true, the field must exist in the caller's data. 50 + + #[serde(default)] 51 + + pub required: bool, 52 + + /// Allowed value(s). Can be a single value or array. 53 + + #[serde(default)] 54 + + pub value: Option<FilterAllowedValues>, 55 + + /// Minimum numeric value (inclusive). 56 + + #[serde(default)] 57 + + pub min: Option<f64>, 58 + + /// Maximum numeric value (inclusive). 59 + + #[serde(default)] 60 + + pub max: Option<f64>, 61 + + /// Regex pattern the value must match. 62 + + #[serde(default)] 63 + + pub pattern: Option<String>, 64 + + /// Regex flags (e.g., "i" for case-insensitive). 65 + + #[serde(default, rename = "patternFlags")] 66 + + pub pattern_flags: Option<String>, 67 + +} 68 + + 69 + +/// Allowed values: a single value or an array. 70 + +#[derive(Clone, Debug, Deserialize, Serialize)] 71 + +#[serde(untagged)] 72 + +pub enum FilterAllowedValues { 73 + + Single(String), 74 + + Multiple(Vec<String>), 75 + +} 76 + + 77 + +/// A registered task provider. 78 + +#[derive(Clone, Debug, Deserialize, Serialize)] 79 + +pub struct TaskProvider { 80 + + /// The task name this provider handles (e.g., "pick-image"). 81 + + pub task_name: String, 82 + + /// Human-readable title shown in the chooser. 83 + + pub title: String, 84 + + /// Optional description for the chooser. 85 + + pub description: Option<String>, 86 + + /// URL of the handler page. 87 + + pub href: ServoUrl, 88 + + /// Filters for matching. Keys are field names from the caller's data. 89 + + pub filters: HashMap<String, FilterValue>, 90 + + /// What the provider returns: "blob", "text", "json", or "none". 91 + + pub returns_type: Option<String>, 92 + + /// How the provider should be displayed. 93 + + pub display: TaskDisplayMode, 94 + + /// Optional icon as a data: URL (base64 encoded). 95 + + pub icon: Option<String>, 96 + +} 97 + + 98 + +/// Serializable provider info sent to the embedder for the chooser UI. 99 + +#[derive(Clone, Debug, Deserialize, Serialize)] 100 + +pub struct TaskProviderInfo { 101 + + pub id: String, 102 + + pub title: String, 103 + + pub description: Option<String>, 104 + + pub display: TaskDisplayMode, 105 + + pub icon: Option<String>, 106 + + pub origin: String, 107 + + pub href: String, 108 + + pub device_name: Option<String>, 109 + + pub remote_peer_id: Option<String>, 110 + +} 111 + + 112 + +/// Registry of all known task providers. 113 + +pub struct TaskRegistry { 114 + + /// Map from task name → list of providers. 115 + + providers: HashMap<String, Vec<TaskProvider>>, 116 + + /// Path to persist providers. 117 + + config_path: Option<PathBuf>, 118 + +} 119 + + 120 + +impl Default for TaskRegistry { 121 + + fn default() -> Self { 122 + + Self { 123 + + providers: HashMap::new(), 124 + + config_path: None, 125 + + } 126 + + } 127 + +} 128 + + 129 + +impl TaskRegistry { 130 + + pub fn new(config_dir: Option<&Path>) -> Self { 131 + + let mut registry = Self { 132 + + providers: HashMap::new(), 133 + + config_path: config_dir.map(|d| d.join("task-providers.json")), 134 + + }; 135 + + registry.load(); 136 + + registry 137 + + } 138 + + 139 + + /// Register a new task provider. Uses (task_name, href) as the dedup key. 140 + + /// Re-registering with the same key updates the other fields. 141 + + pub fn register(&mut self, provider: TaskProvider) { 142 + + let providers = self 143 + + .providers 144 + + .entry(provider.task_name.clone()) 145 + + .or_default(); 146 + + let href_str = provider.href.as_str(); 147 + + 148 + + // Check for existing provider with same href. 149 + + if let Some(existing) = providers.iter_mut().find(|p| p.href.as_str() == href_str) { 150 + + // Update existing registration. 151 + + existing.title = provider.title; 152 + + existing.description = provider.description; 153 + + existing.filters = provider.filters; 154 + + existing.returns_type = provider.returns_type; 155 + + existing.display = provider.display; 156 + + existing.icon = provider.icon; 157 + + debug!( 158 + + "Updated task provider: {} -> {}", 159 + + existing.task_name, href_str 160 + + ); 161 + + } else { 162 + + debug!( 163 + + "Registered task provider: {} -> {}", 164 + + provider.task_name, href_str 165 + + ); 166 + + providers.push(provider); 167 + + } 168 + + 169 + + self.save(); 170 + + } 171 + + 172 + + /// Find all providers matching a task name and caller data. 173 + + /// The caller_data is a map of field names to values for filter matching. 174 + + pub fn find_providers( 175 + + &self, 176 + + task_name: &str, 177 + + caller_data: &HashMap<String, serde_json::Value>, 178 + + ) -> Vec<&TaskProvider> { 179 + + let Some(providers) = self.providers.get(task_name) else { 180 + + return vec![]; 181 + + }; 182 + + 183 + + providers 184 + + .iter() 185 + + .filter(|p| filters_match(&p.filters, caller_data)) 186 + + .collect() 187 + + } 188 + + 189 + + /// Get provider info suitable for sending to the embedder (chooser UI). 190 + + pub fn get_provider_infos( 191 + + &self, 192 + + task_name: &str, 193 + + caller_data: &HashMap<String, serde_json::Value>, 194 + + ) -> Vec<TaskProviderInfo> { 195 + + self.find_providers(task_name, caller_data) 196 + + .iter() 197 + + .enumerate() 198 + + .map(|(i, p)| { 199 + + let origin = p.href.origin().ascii_serialization(); 200 + + TaskProviderInfo { 201 + + id: format!("{}:{}", task_name, i), 202 + + title: p.title.clone(), 203 + + description: p.description.clone(), 204 + + display: p.display.clone(), 205 + + icon: p.icon.clone(), 206 + + origin, 207 + + href: p.href.as_str().to_string(), 208 + + device_name: None, 209 + + remote_peer_id: None, 210 + + } 211 + + }) 212 + + .collect() 213 + + } 214 + + 215 + + /// Resolve a provider ID (e.g., "share:0") to the actual TaskProvider. 216 + + pub fn resolve_provider(&self, provider_id: &str) -> Option<&TaskProvider> { 217 + + let (task_name, index_str) = provider_id.rsplit_once(':')?; 218 + + let index: usize = index_str.parse().ok()?; 219 + + let providers = self.providers.get(task_name)?; 220 + + providers.get(index) 221 + + } 222 + + 223 + + /// Save all providers to disk. 224 + + fn save(&self) { 225 + + let Some(path) = &self.config_path else { 226 + + return; 227 + + }; 228 + + // Collect all providers into a flat list for serialization. 229 + + let all_providers: Vec<&TaskProvider> = 230 + + self.providers.values().flat_map(|v| v.iter()).collect(); 231 + + match serde_json::to_string_pretty(&all_providers) { 232 + + Ok(json) => { 233 + + if let Err(e) = fs::write(path, json) { 234 + + error!("Failed to save task providers: {e}"); 235 + + } 236 + + }, 237 + + Err(e) => error!("Failed to serialize task providers: {e}"), 238 + + } 239 + + } 240 + + 241 + + /// Load providers from disk. 242 + + fn load(&mut self) { 243 + + let Some(path) = &self.config_path else { 244 + + return; 245 + + }; 246 + + let Ok(json) = fs::read_to_string(path) else { 247 + + return; // File doesn't exist yet, that's fine. 248 + + }; 249 + + match serde_json::from_str::<Vec<TaskProvider>>(&json) { 250 + + Ok(providers) => { 251 + + for provider in providers { 252 + + self.providers 253 + + .entry(provider.task_name.clone()) 254 + + .or_default() 255 + + .push(provider); 256 + + } 257 + + debug!( 258 + + "Loaded {} task providers from {}", 259 + + self.providers.values().map(|v| v.len()).sum::<usize>(), 260 + + path.display() 261 + + ); 262 + + }, 263 + + Err(e) => error!( 264 + + "Failed to parse task providers from {}: {e}", 265 + + path.display() 266 + + ), 267 + + } 268 + + } 269 + +} 270 + + 271 + +/// A pending task request waiting for provider selection or result. 272 + +pub struct PendingTaskRequest { 273 + + /// The task name. 274 + + pub task_name: String, 275 + + /// The caller's data (structured clone serialized). 276 + + pub data: Option<StructuredSerializedData>, 277 + + /// Callback to resolve the caller's promise with result data or error. 278 + + pub callback: GenericCallback<Result<StructuredSerializedData, String>>, 279 + + /// The selected provider (set after chooser selection). 280 + + pub provider: Option<TaskProvider>, 281 + + /// The virtual port ID for the provider side of the MessagePort pair. 282 + + pub provider_port_id: MessagePortId, 283 + + /// The URL the provider was opened with (set after provider selection). 284 + + /// Used to match new pipelines to pending task requests. 285 + + pub provider_url: Option<String>, 286 + + /// Whether the task data has been dispatched to the provider. 287 + + pub dispatched: bool, 288 + + /// The webview ID of the provider (set when the provider calls acceptTask). 289 + + pub provider_webview_id: Option<WebViewId>, 290 + + /// If this is a remote task, the peer ID of the requesting device. 291 + + /// When set, the result is sent back via P2P instead of resolving a local callback. 292 + + pub remote_caller_peer_id: Option<String>, 293 + + /// Remote providers discovered via P2P, keyed by provider ID. 294 + + pub remote_providers: HashMap<String, TaskProviderInfo>, 295 + +} 296 + + 297 + +/// Check if all provider filters are satisfied by the caller's data. 298 + +/// A provider with no filters matches everything. 299 + +fn filters_match( 300 + + filters: &HashMap<String, FilterValue>, 301 + + caller_data: &HashMap<String, serde_json::Value>, 302 + +) -> bool { 303 + + if filters.is_empty() { 304 + + return true; 305 + + } 306 + + 307 + + for (field_name, filter) in filters { 308 + + let caller_value = caller_data.get(field_name); 309 + + 310 + + match filter { 311 + + FilterValue::String(expected) => { 312 + + // Optional field, but if present must equal expected. 313 + + if let Some(val) = caller_value { 314 + + if val.as_str() != Some(expected.as_str()) { 315 + + return false; 316 + + } 317 + + } 318 + + }, 319 + + FilterValue::Number(expected) => { 320 + + if let Some(val) = caller_value { 321 + + if val.as_f64() != Some(*expected) { 322 + + return false; 323 + + } 324 + + } 325 + + }, 326 + + FilterValue::StringArray(allowed) => { 327 + + // Optional field, but if present must be one of allowed values. 328 + + if let Some(val) = caller_value { 329 + + let val_str = val.as_str().unwrap_or_default(); 330 + + if !allowed.iter().any(|a| a == val_str) { 331 + + return false; 332 + + } 333 + + } 334 + + }, 335 + + FilterValue::Object(obj) => { 336 + + if !filter_object_matches(obj, caller_value) { 337 + + return false; 338 + + } 339 + + }, 340 + + } 341 + + } 342 + + 343 + + true 344 + +} 345 + + 346 + +/// Check if a filter definition object matches a caller's value. 347 + +fn filter_object_matches(filter: &FilterObject, caller_value: Option<&serde_json::Value>) -> bool { 348 + + // Check required. 349 + + if filter.required && caller_value.is_none() { 350 + + return false; 351 + + } 352 + + 353 + + let Some(val) = caller_value else { 354 + + // Not required and not present — passes. 355 + + return true; 356 + + }; 357 + + 358 + + // Check value constraint. 359 + + if let Some(allowed) = &filter.value { 360 + + let val_str = val.as_str().unwrap_or_default(); 361 + + let matches = match allowed { 362 + + FilterAllowedValues::Single(s) => val_str == s, 363 + + FilterAllowedValues::Multiple(arr) => arr.iter().any(|a| a == val_str), 364 + + }; 365 + + if !matches { 366 + + return false; 367 + + } 368 + + } 369 + + 370 + + // Check min/max for numeric values. 371 + + if let Some(val_num) = val.as_f64() { 372 + + if let Some(min) = filter.min { 373 + + if val_num < min { 374 + + return false; 375 + + } 376 + + } 377 + + if let Some(max) = filter.max { 378 + + if val_num > max { 379 + + return false; 380 + + } 381 + + } 382 + + } 383 + + 384 + + // Check pattern. 385 + + if let Some(pattern) = &filter.pattern { 386 + + let val_str = val.as_str().unwrap_or_default(); 387 + + let flags = filter.pattern_flags.as_deref().unwrap_or(""); 388 + + let regex_str = if flags.contains('i') { 389 + + format!("(?i){pattern}") 390 + + } else { 391 + + pattern.clone() 392 + + }; 393 + + if let Ok(re) = regex::Regex::new(&regex_str) { 394 + + if !re.is_match(val_str) { 395 + + return false; 396 + + } 397 + + } 398 + + } 399 + + 400 + + true 401 + +} 402 + + 403 + +#[cfg(test)] 404 + +mod tests { 405 + + use super::*; 406 + + 407 + + #[test] 408 + + fn test_empty_filters_match_everything() { 409 + + let filters = HashMap::new(); 410 + + let data = HashMap::new(); 411 + + assert!(filters_match(&filters, &data)); 412 + + } 413 + + 414 + + #[test] 415 + + fn test_string_filter() { 416 + + let mut filters = HashMap::new(); 417 + + filters.insert("type".into(), FilterValue::String("url".into())); 418 + + 419 + + let mut data = HashMap::new(); 420 + + data.insert("type".into(), serde_json::json!("url")); 421 + + assert!(filters_match(&filters, &data)); 422 + + 423 + + data.insert("type".into(), serde_json::json!("text")); 424 + + assert!(!filters_match(&filters, &data)); 425 + + 426 + + // Field not present — passes (optional). 427 + + let empty_data = HashMap::new(); 428 + + assert!(filters_match(&filters, &empty_data)); 429 + + } 430 + + 431 + + #[test] 432 + + fn test_array_filter() { 433 + + let mut filters = HashMap::new(); 434 + + filters.insert( 435 + + "type".into(), 436 + + FilterValue::StringArray(vec!["url".into(), "text".into()]), 437 + + ); 438 + + 439 + + let mut data = HashMap::new(); 440 + + data.insert("type".into(), serde_json::json!("url")); 441 + + assert!(filters_match(&filters, &data)); 442 + + 443 + + data.insert("type".into(), serde_json::json!("image")); 444 + + assert!(!filters_match(&filters, &data)); 445 + + } 446 + + 447 + + #[test] 448 + + fn test_required_filter() { 449 + + let mut filters = HashMap::new(); 450 + + filters.insert( 451 + + "url".into(), 452 + + FilterValue::Object(FilterObject { 453 + + required: true, 454 + + value: None, 455 + + min: None, 456 + + max: None, 457 + + pattern: None, 458 + + pattern_flags: None, 459 + + }), 460 + + ); 461 + + 462 + + let empty_data = HashMap::new(); 463 + + assert!(!filters_match(&filters, &empty_data)); 464 + + 465 + + let mut data = HashMap::new(); 466 + + data.insert("url".into(), serde_json::json!("https://example.com")); 467 + + assert!(filters_match(&filters, &data)); 468 + + } 469 + + 470 + + #[test] 471 + + fn test_pattern_filter() { 472 + + let mut filters = HashMap::new(); 473 + + filters.insert( 474 + + "url".into(), 475 + + FilterValue::Object(FilterObject { 476 + + required: true, 477 + + value: None, 478 + + min: None, 479 + + max: None, 480 + + pattern: Some("^https://".into()), 481 + + pattern_flags: None, 482 + + }), 483 + + ); 484 + + 485 + + let mut data = HashMap::new(); 486 + + data.insert("url".into(), serde_json::json!("https://example.com")); 487 + + assert!(filters_match(&filters, &data)); 488 + + 489 + + data.insert("url".into(), serde_json::json!("http://example.com")); 490 + + assert!(!filters_match(&filters, &data)); 491 + + } 492 + + 493 + + #[test] 494 + + fn test_min_max_filter() { 495 + + let mut filters = HashMap::new(); 496 + + filters.insert( 497 + + "width".into(), 498 + + FilterValue::Object(FilterObject { 499 + + required: false, 500 + + value: None, 501 + + min: Some(100.0), 502 + + max: Some(1000.0), 503 + + pattern: None, 504 + + pattern_flags: None, 505 + + }), 506 + + ); 507 + + 508 + + let mut data = HashMap::new(); 509 + + data.insert("width".into(), serde_json::json!(500)); 510 + + assert!(filters_match(&filters, &data)); 511 + + 512 + + data.insert("width".into(), serde_json::json!(50)); 513 + + assert!(!filters_match(&filters, &data)); 514 + + 515 + + data.insert("width".into(), serde_json::json!(1500)); 516 + + assert!(!filters_match(&filters, &data)); 517 + + } 518 + +}
+5 -1
patches/components/constellation/tracing.rs.patch
··· 36 36 Self::ActivateDocument => target!("ActivateDocument"), 37 37 Self::SetDocumentState(..) => target!("SetDocumentState"), 38 38 Self::SetFinalUrl(..) => target!("SetFinalUrl"), 39 - @@ -187,6 +195,50 @@ 39 + @@ -187,6 +195,54 @@ 40 40 target!("RespondToScreenshotReadinessRequest") 41 41 }, 42 42 Self::TriggerGarbageCollection => target!("TriggerGarbageCollection"), ··· 84 84 + Self::CreatePeerStream(..) => target!("CreatePeerStream"), 85 85 + Self::PeerStreamResponse(..) => target!("PeerStreamResponse"), 86 86 + Self::AtProto(..) => target!("AtProto"), 87 + + Self::RequestTask(..) => target!("RequestTask"), 88 + + Self::RegisterTaskProvider(..) => target!("RegisterTaskProvider"), 89 + + Self::TaskProviderSelected(..) => target!("TaskProviderSelected"), 90 + + Self::AcceptTask(..) => target!("AcceptTask"), 87 91 } 88 92 } 89 93 }
+19
patches/components/devtools/lib.rs.patch
··· 1 + --- original 2 + +++ modified 3 + @@ -237,11 +237,11 @@ 4 + continue; 5 + }; 6 + // connection succeeded and accepted 7 + - sender 8 + - .send(DevtoolsControlMsg::FromChrome( 9 + - ChromeToDevtoolsControlMsg::AddClient(stream), 10 + - )) 11 + - .unwrap(); 12 + + if let Err(err) = sender.send(DevtoolsControlMsg::FromChrome( 13 + + ChromeToDevtoolsControlMsg::AddClient(stream), 14 + + )) { 15 + + eprintln!("Failed to send new client message to devtools control: {err}"); 16 + + } 17 + } 18 + }) 19 + .expect("Thread spawning failed");
+237 -2
patches/components/script/dom/embedder.rs.patch
··· 1 1 --- original 2 2 +++ modified 3 - @@ -0,0 +1,277 @@ 3 + @@ -0,0 +1,512 @@ 4 4 +/* SPDX Id: AGPL-3.0-or-later */ 5 5 + 6 6 +//! The `Embedder` interface provides communication between web content and the embedder. ··· 24 24 +use servo_url::ServoUrl; 25 25 + 26 26 +use crate::dom::bindings::codegen::Bindings::CustomEventBinding::CustomEventMethods; 27 - +use crate::dom::bindings::codegen::Bindings::EmbedderBinding::EmbedderMethods; 27 + +use crate::dom::bindings::codegen::Bindings::EmbedderBinding::{ 28 + + EmbedderMethods, TaskProviderDescriptor, 29 + +}; 28 30 +use crate::dom::bindings::inheritance::Castable; 29 31 +use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object}; 30 32 +use crate::dom::bindings::root::{DomRoot, MutNullableDom}; ··· 188 190 + .fire(self.upcast::<EventTarget>(), can_gc); 189 191 + } 190 192 + 193 + + /// Dispatch a taskrequest event with the given task name and providers. 194 + + /// The system UI should call `respondToTaskRequest(requestId, providerId)` to respond. 195 + + #[expect(unsafe_code)] 196 + + pub(crate) fn dispatch_task_request( 197 + + &self, 198 + + request_id: &str, 199 + + task_name: &str, 200 + + providers_json: &str, 201 + + can_gc: CanGc, 202 + + ) { 203 + + let cx = GlobalScope::get_cx(); 204 + + rooted!(in(*cx) let mut detail = UndefinedValue()); 205 + + 206 + + unsafe { 207 + + rooted!(in(*cx) let detail_obj = JS_NewObject(*cx, ptr::null())); 208 + + if !detail_obj.get().is_null() { 209 + + // Set requestId property 210 + + rooted!(in(*cx) let mut id_val = UndefinedValue()); 211 + + request_id.safe_to_jsval(cx, id_val.handle_mut(), can_gc); 212 + + JS_DefineProperty( 213 + + *cx, 214 + + detail_obj.handle(), 215 + + c"requestId".as_ptr(), 216 + + id_val.handle(), 217 + + JSPROP_ENUMERATE as u32, 218 + + ); 219 + + 220 + + // Set taskName property 221 + + rooted!(in(*cx) let mut name_val = UndefinedValue()); 222 + + task_name.safe_to_jsval(cx, name_val.handle_mut(), can_gc); 223 + + JS_DefineProperty( 224 + + *cx, 225 + + detail_obj.handle(), 226 + + c"taskName".as_ptr(), 227 + + name_val.handle(), 228 + + JSPROP_ENUMERATE as u32, 229 + + ); 230 + + 231 + + // Set providers property (JSON string for now) 232 + + rooted!(in(*cx) let mut providers_val = UndefinedValue()); 233 + + providers_json.safe_to_jsval(cx, providers_val.handle_mut(), can_gc); 234 + + JS_DefineProperty( 235 + + *cx, 236 + + detail_obj.handle(), 237 + + c"providers".as_ptr(), 238 + + providers_val.handle(), 239 + + JSPROP_ENUMERATE as u32, 240 + + ); 241 + + 242 + + detail.set(ObjectValue(detail_obj.get())); 243 + + } 244 + + } 245 + + 246 + + let global = self.global(); 247 + + let custom_event = CustomEvent::new_uninitialized(&global, can_gc); 248 + + custom_event.InitCustomEvent( 249 + + cx, 250 + + DOMString::from("taskrequest"), 251 + + false, 252 + + false, 253 + + detail.handle(), 254 + + ); 255 + + 256 + + custom_event 257 + + .upcast::<Event>() 258 + + .fire(self.upcast::<EventTarget>(), can_gc); 259 + + } 260 + + 261 + + /// Dispatch an opentaskprovider event so the system UI opens a provider webview. 262 + + #[expect(unsafe_code)] 263 + + pub(crate) fn dispatch_open_task_provider( 264 + + &self, 265 + + request_id: &str, 266 + + url: &str, 267 + + title: &str, 268 + + can_gc: CanGc, 269 + + ) { 270 + + let cx = GlobalScope::get_cx(); 271 + + rooted!(in(*cx) let mut detail = UndefinedValue()); 272 + + 273 + + unsafe { 274 + + rooted!(in(*cx) let detail_obj = JS_NewObject(*cx, ptr::null())); 275 + + if !detail_obj.get().is_null() { 276 + + rooted!(in(*cx) let mut val = UndefinedValue()); 277 + + 278 + + request_id.safe_to_jsval(cx, val.handle_mut(), can_gc); 279 + + JS_DefineProperty( 280 + + *cx, 281 + + detail_obj.handle(), 282 + + c"requestId".as_ptr(), 283 + + val.handle(), 284 + + JSPROP_ENUMERATE as u32, 285 + + ); 286 + + 287 + + url.safe_to_jsval(cx, val.handle_mut(), can_gc); 288 + + JS_DefineProperty( 289 + + *cx, 290 + + detail_obj.handle(), 291 + + c"url".as_ptr(), 292 + + val.handle(), 293 + + JSPROP_ENUMERATE as u32, 294 + + ); 295 + + 296 + + title.safe_to_jsval(cx, val.handle_mut(), can_gc); 297 + + JS_DefineProperty( 298 + + *cx, 299 + + detail_obj.handle(), 300 + + c"title".as_ptr(), 301 + + val.handle(), 302 + + JSPROP_ENUMERATE as u32, 303 + + ); 304 + + 305 + + detail.set(ObjectValue(detail_obj.get())); 306 + + } 307 + + } 308 + + 309 + + let global = self.global(); 310 + + let custom_event = CustomEvent::new_uninitialized(&global, can_gc); 311 + + custom_event.InitCustomEvent( 312 + + cx, 313 + + DOMString::from("opentaskprovider"), 314 + + false, 315 + + false, 316 + + detail.handle(), 317 + + ); 318 + + 319 + + custom_event 320 + + .upcast::<Event>() 321 + + .fire(self.upcast::<EventTarget>(), can_gc); 322 + + } 323 + + 324 + + /// Dispatch a taskprovidersupdate event with additional remote providers. 325 + + #[expect(unsafe_code)] 326 + + pub(crate) fn dispatch_task_providers_update( 327 + + &self, 328 + + request_id: &str, 329 + + providers_json: &str, 330 + + can_gc: CanGc, 331 + + ) { 332 + + let cx = GlobalScope::get_cx(); 333 + + rooted!(in(*cx) let mut detail = UndefinedValue()); 334 + + 335 + + unsafe { 336 + + rooted!(in(*cx) let detail_obj = JS_NewObject(*cx, ptr::null())); 337 + + if !detail_obj.get().is_null() { 338 + + rooted!(in(*cx) let mut val = UndefinedValue()); 339 + + 340 + + request_id.safe_to_jsval(cx, val.handle_mut(), can_gc); 341 + + JS_DefineProperty( 342 + + *cx, 343 + + detail_obj.handle(), 344 + + c"requestId".as_ptr(), 345 + + val.handle(), 346 + + JSPROP_ENUMERATE as u32, 347 + + ); 348 + + 349 + + providers_json.safe_to_jsval(cx, val.handle_mut(), can_gc); 350 + + JS_DefineProperty( 351 + + *cx, 352 + + detail_obj.handle(), 353 + + c"providers".as_ptr(), 354 + + val.handle(), 355 + + JSPROP_ENUMERATE as u32, 356 + + ); 357 + + 358 + + detail.set(ObjectValue(detail_obj.get())); 359 + + } 360 + + } 361 + + 362 + + let global = self.global(); 363 + + let custom_event = CustomEvent::new_uninitialized(&global, can_gc); 364 + + custom_event.InitCustomEvent( 365 + + cx, 366 + + DOMString::from("taskprovidersupdate"), 367 + + false, 368 + + false, 369 + + detail.handle(), 370 + + ); 371 + + 372 + + custom_event 373 + + .upcast::<Event>() 374 + + .fire(self.upcast::<EventTarget>(), can_gc); 375 + + } 376 + + 191 377 + pub(crate) fn is_allowed_to_embed_for_url(url: &ServoUrl) -> bool { 192 378 + // TODO: better permission mechanism with finer granularity 193 379 + url.scheme() == "beaver" ··· 263 449 + .send(EmbedderMsg::StartWindowResize(webview_id)); 264 450 + } 265 451 + 452 + + /// Respond to a web task request with the selected provider id, or null to cancel. 453 + + fn RespondToTaskRequest(&self, request_id: DOMString, provider_id: Option<DOMString>) { 454 + + let global = self.global(); 455 + + let _ = global.script_to_constellation_chan().send( 456 + + ScriptToConstellationMessage::TaskProviderSelected( 457 + + request_id.to_string(), 458 + + provider_id.map(|s| s.to_string()), 459 + + ), 460 + + ); 461 + + } 462 + + 463 + + /// Register a task provider from a privileged page. 464 + + fn RegisterTaskProvider(&self, descriptor: &TaskProviderDescriptor) { 465 + + let global = self.global(); 466 + + let base_url = global.api_base_url(); 467 + + 468 + + // Resolve href relative to the page's base URL. 469 + + let href = match ServoUrl::parse_with_base(Some(&base_url), &descriptor.href) { 470 + + Ok(url) => url.to_string(), 471 + + Err(_) => { 472 + + log::warn!("registerTaskProvider: invalid href '{}'", &*descriptor.href); 473 + + return; 474 + + }, 475 + + }; 476 + + 477 + + let _ = global.script_to_constellation_chan().send( 478 + + ScriptToConstellationMessage::RegisterTaskProvider( 479 + + descriptor.name.to_string(), 480 + + descriptor.title.to_string(), 481 + + descriptor.description.as_ref().map(|s| s.to_string()), 482 + + href, 483 + + descriptor.filters.to_string(), 484 + + descriptor.returns.as_ref().map(|s| s.to_string()), 485 + + descriptor.display.to_string(), 486 + + descriptor.icon.as_ref().map(|s| s.to_string()), 487 + + ), 488 + + ); 489 + + } 490 + + 266 491 + fn Pairing(&self, can_gc: CanGc) -> DomRoot<Pairing> { 267 492 + self.pairing 268 493 + .or_init(|| Pairing::new(&self.global(), can_gc)) ··· 276 501 + preferencechanged, 277 502 + GetOnpreferencechanged, 278 503 + SetOnpreferencechanged 504 + + ); 505 + + 506 + + // Event handler for web task request events 507 + + event_handler!(taskrequest, GetOntaskrequest, SetOntaskrequest); 508 + + 509 + + // Event handler for task providers update (remote providers discovered) 510 + + event_handler!( 511 + + taskprovidersupdate, 512 + + GetOntaskprovidersupdate, 513 + + SetOntaskprovidersupdate 279 514 + ); 280 515 +}
+36 -43
patches/components/script/dom/html/htmliframeelement.rs.patch
··· 108 108 }; 109 109 110 110 self.pipeline_id.set(Some(new_pipeline_id)); 111 - @@ -597,6 +636,148 @@ 111 + @@ -597,6 +636,128 @@ 112 112 ); 113 113 } 114 114 ··· 160 160 + load_data.destination = Destination::IFrame; 161 161 + load_data.policy_container = Some(window.as_global_scope().policy_container()); 162 162 + 163 - + // Clone load_data for spawning the pipeline later 164 - + let load_data_for_spawn = load_data.clone(); 165 - + let theme = window.theme(); 166 - + 167 163 + // Get the iframe's size to use as the viewport for the embedded webview. 168 164 + // We use border_box which gives us the size in CSS pixels. 169 165 + let hidpi_scale_factor = window.device_pixel_ratio(); ··· 188 184 + ipc::channel().expect("Failed to create IPC channel for embedded webview"); 189 185 + 190 186 + let hide_focus = self.has_hide_focus(); 187 + + let theme = window.theme(); 191 188 + let request = EmbeddedWebViewCreationRequest { 192 189 + load_data, 193 190 + parent_pipeline_id: pipeline_id, ··· 220 217 + self.pipeline_id.set(Some(response.new_pipeline_id)); 221 218 + self.webview_id.set(Some(response.new_webview_id)); 222 219 + 223 - + // Spawn the pipeline in the script thread 224 - + // Embedded webviews are top-level, so parent_info is None 225 - + let new_pipeline_info = NewPipelineInfo { 226 - + parent_info: None, 227 - + new_pipeline_id: response.new_pipeline_id, 228 - + browsing_context_id: response.new_browsing_context_id, 229 - + webview_id: response.new_webview_id, 230 - + opener: None, 231 - + load_data: load_data_for_spawn, 232 - + viewport_details, 233 - + user_content_manager_id: None, 234 - + theme, 235 - + is_embedded_webview: true, 236 - + hide_focus, 237 - + }; 238 - + 239 - + with_script_thread(|script_thread| { 240 - + script_thread.spawn_pipeline(new_pipeline_info); 241 - + }); 220 + + // The constellation already spawns the pipeline via Pipeline::spawn() 221 + + // in handle_create_embedded_webview, so we don't need to spawn it here. 242 222 + }, 243 223 + Ok(None) => { 244 224 + warn!("Embedded webview creation was rejected by embedder"); ··· 257 237 fn destroy_nested_browsing_context(&self) { 258 238 self.pipeline_id.set(None); 259 239 self.pending_pipeline_id.set(None); 260 - @@ -659,6 +840,13 @@ 240 + @@ -659,6 +820,13 @@ 261 241 lazy_load_resumption_steps: Default::default(), 262 242 pending_navigation: Default::default(), 263 243 already_fired_synchronous_load_event: Default::default(), ··· 271 251 } 272 252 } 273 253 274 - @@ -694,7 +882,158 @@ 254 + @@ -694,6 +862,157 @@ 275 255 self.webview_id.get() 276 256 } 277 257 ··· 376 356 + 377 357 + /// Returns true if this iframe is hosting an embedded webview (created with "embed" attribute). 378 358 + /// Embedded webviews have their own top-level WebViewId and window.parent === window.self. 379 - #[inline] 359 + + #[inline] 380 360 + pub(crate) fn is_embedded_webview(&self) -> bool { 381 361 + self.is_embedded_webview.get() 382 362 + } ··· 426 406 + self.page_zoom.set(zoom); 427 407 + } 428 408 + 429 - + #[inline] 409 + #[inline] 430 410 pub(crate) fn sandboxing_flag_set(&self) -> SandboxingFlagSet { 431 411 self.sandboxing_flag_set 432 - .get() 433 - @@ -1078,6 +1417,89 @@ 412 + @@ -1078,6 +1397,89 @@ 434 413 435 414 // https://html.spec.whatwg.org/multipage/#dom-iframe-longdesc 436 415 make_url_setter!(SetLongDesc, "longdesc"); ··· 520 499 } 521 500 522 501 impl VirtualMethods for HTMLIFrameElement { 523 - @@ -1134,10 +1556,40 @@ 502 + @@ -1133,9 +1535,54 @@ 503 + // may be in a different script thread. Instead, we check to see if the parent 524 504 // is in a document tree and has a browsing context, which is what causes 525 505 // the child browsing context to be created. 526 - if self.upcast::<Node>().is_connected_with_browsing_context() { 527 - - debug!("iframe src set while in browsing context."); 528 - - self.process_the_iframe_attributes(ProcessingMode::NotFirstTime, cx); 506 + + 507 + + // Only process if the value actually changed (not just re-set to the same value). 508 + + let value_changed = match mutation { 509 + + AttributeMutation::Set(old_value, _) => { 510 + + old_value.map_or(true, |old| **old != **attr.value()) 511 + + }, 512 + + AttributeMutation::Removed => true, 513 + + }; 514 + + 515 + + if value_changed && self.upcast::<Node>().is_connected_with_browsing_context() { 529 516 + // For embedded webviews, navigate using the load() method instead of 530 517 + // processing iframe attributes (which is for regular nested iframes). 531 518 + if self.is_embedded_webview.get() { 519 + + // Skip the initial src set (old=None) since create_embedded_webview 520 + + // already navigated. Only re-navigate on actual src changes. 521 + + let is_initial_set = matches!(mutation, AttributeMutation::Set(None, _)); 522 + + if is_initial_set { 523 + + return; 524 + + } 532 525 + if let Some(webview_id) = self.embedded_webview_id.get() { 533 526 + let url = self 534 527 + .shared_attribute_processing_steps_for_iframe_and_frame_elements( ··· 548 541 + debug!("iframe src set while in browsing context."); 549 542 + self.process_the_iframe_attributes(ProcessingMode::NotFirstTime, cx); 550 543 + } 551 - } 552 - }, 544 + + } 545 + + }, 553 546 + local_name!("embed") => { 554 547 + // The embed attribute determines whether this iframe hosts an embedded webview. 555 548 + // Warn if it's changed after the iframe is already connected, as this is not supported. 556 - + if self.upcast::<Node>().is_connected_with_browsing_context() { 549 + if self.upcast::<Node>().is_connected_with_browsing_context() { 550 + - debug!("iframe src set while in browsing context."); 551 + - self.process_the_iframe_attributes(ProcessingMode::NotFirstTime, cx); 557 552 + warn!( 558 553 + "The 'embed' attribute on iframe should not be changed after insertion. \ 559 554 + The iframe mode (nested vs embedded webview) is determined at insertion time." 560 555 + ); 561 - + } 562 - + }, 556 + } 557 + }, 563 558 local_name!("loading") => { 564 - // https://html.spec.whatwg.org/multipage/#attr-iframe-loading 565 - // > When the loading attribute's state is changed to the Eager state, the user agent must run these steps: 566 - @@ -1200,6 +1652,23 @@ 559 + @@ -1200,6 +1647,23 @@ 567 560 568 561 debug!("<iframe> running post connection steps"); 569 562 ··· 587 580 // Step 1. Create a new child navigable for insertedNode. 588 581 self.create_nested_browsing_context(cx); 589 582 590 - @@ -1223,11 +1692,25 @@ 583 + @@ -1223,11 +1687,25 @@ 591 584 fn unbind_from_tree(&self, context: &UnbindContext, can_gc: CanGc) { 592 585 self.super_type().unwrap().unbind_from_tree(context, can_gc); 593 586
+46 -1
patches/components/script/dom/html/htmllinkelement.rs.patch
··· 1 1 --- original 2 2 +++ modified 3 - @@ -754,7 +754,7 @@ 3 + @@ -466,6 +466,10 @@ 4 + self.fetch_and_process_prefetch_link(&href); 5 + } 6 + 7 + + if relations.contains(LinkRelations::WEBTASKS) { 8 + + self.fetch_and_process_webtasks_link(&href); 9 + + } 10 + + 11 + if relations.contains(LinkRelations::PRELOAD) { 12 + self.handle_preload_url(); 13 + } 14 + @@ -660,6 +664,33 @@ 15 + document.fetch_background(request, fetch_context); 16 + } 17 + 18 + + /// Fetch and process a `<link rel="webtasks">` manifest. 19 + + fn fetch_and_process_webtasks_link(&self, href: &str) { 20 + + if href.is_empty() { 21 + + return; 22 + + } 23 + + 24 + + let mut options = self.processing_options(); 25 + + options.destination = Destination::None; 26 + + 27 + + let Some(request) = options.create_link_request(self.owner_window().webview_id()) else { 28 + + return; 29 + + }; 30 + + let url = request.url.clone(); 31 + + 32 + + let document = self.upcast::<Node>().owner_doc(); 33 + + let fetch_context = LinkFetchContext { 34 + + url, 35 + + link: Some(Trusted::new(self)), 36 + + document: Trusted::new(&document), 37 + + global: Trusted::new(&document.global()), 38 + + type_: LinkFetchContextType::WebTasks, 39 + + response_body: vec![], 40 + + }; 41 + + 42 + + document.fetch_background(request, fetch_context); 43 + + } 44 + + 45 + /// <https://html.spec.whatwg.org/multipage/#concept-link-obtain> 46 + fn handle_stylesheet_url(&self) { 47 + let document = self.owner_document(); 48 + @@ -754,7 +785,7 @@ 4 49 if !window.is_top_level() { 5 50 return; 6 51 }
+215 -9
patches/components/script/dom/navigator.rs.patch
··· 8 8 use std::sync::LazyLock; 9 9 10 10 use dom_struct::dom_struct; 11 + @@ -12,7 +13,7 @@ 12 + use embedder_traits::{EmbedderMsg, ProtocolHandlerUpdateRegistration, RegisterOrUnregister}; 13 + use headers::HeaderMap; 14 + use http::header::{self, HeaderValue}; 15 + -use js::rust::MutableHandleValue; 16 + +use js::rust::{HandleValue, MutableHandleValue}; 17 + use net_traits::request::{ 18 + CredentialsMode, Destination, RequestBuilder, RequestId, RequestMode, 19 + is_cors_safelisted_request_content_type, 11 20 @@ -20,10 +21,13 @@ 12 21 use net_traits::{FetchMetadata, NetworkError, ResourceFetchTiming}; 13 22 use regex::Regex; 14 23 use servo_base::generic_channel; 15 24 +use servo_base::id::MessagePortId; 16 25 use servo_config::pref; 17 - +use servo_constellation_traits::ScriptToConstellationMessage; 26 + +use servo_constellation_traits::{ScriptToConstellationMessage, StructuredSerializedData}; 18 27 use servo_url::ServoUrl; 19 28 20 29 use crate::body::Extractable; ··· 22 31 #[cfg(feature = "gamepad")] 23 32 use crate::dom::bindings::cell::DomRefCell; 24 33 use crate::dom::bindings::codegen::Bindings::NavigatorBinding::NavigatorMethods; 25 - @@ -40,6 +44,7 @@ 34 + @@ -34,6 +38,7 @@ 35 + use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object}; 36 + use crate::dom::bindings::root::{DomRoot, MutNullableDom}; 37 + use crate::dom::bindings::str::{DOMString, USVString}; 38 + +use crate::dom::bindings::structuredclone; 39 + use crate::dom::bindings::utils::to_frozen_array; 40 + #[cfg(feature = "bluetooth")] 41 + use crate::dom::bluetooth::Bluetooth; 42 + @@ -40,6 +45,7 @@ 26 43 use crate::dom::clipboard::Clipboard; 27 44 use crate::dom::credentialmanagement::credentialscontainer::CredentialsContainer; 28 45 use crate::dom::csp::{GlobalCspReporting, Violation}; ··· 30 47 #[cfg(feature = "gamepad")] 31 48 use crate::dom::gamepad::Gamepad; 32 49 #[cfg(feature = "gamepad")] 33 - @@ -46,13 +51,16 @@ 50 + @@ -46,13 +52,16 @@ 34 51 use crate::dom::gamepad::gamepadevent::GamepadEventType; 35 52 use crate::dom::geolocation::Geolocation; 36 53 use crate::dom::globalscope::GlobalScope; ··· 47 64 use crate::dom::serviceworkercontainer::ServiceWorkerContainer; 48 65 use crate::dom::servointernals::ServoInternals; 49 66 use crate::dom::types::UserActivation; 50 - @@ -63,6 +71,7 @@ 67 + @@ -63,6 +72,8 @@ 51 68 use crate::dom::xrsystem::XRSystem; 52 69 use crate::fetch::RequestWithGlobalScope; 53 70 use crate::network_listener::{FetchResponseListener, ResourceTimingListener, submit_timing}; 54 71 +use crate::realms::InRealm; 72 + +use crate::routed_promise::{RoutedPromiseListener, callback_promise}; 55 73 use crate::script_runtime::{CanGc, JSContext}; 56 74 57 75 pub(super) fn hardware_concurrency() -> u64 { 58 - @@ -132,6 +141,9 @@ 76 + @@ -132,6 +143,9 @@ 59 77 has_gamepad_gesture: Cell<bool>, 60 78 servo_internals: MutNullableDom<ServoInternals>, 61 79 user_activation: MutNullableDom<UserActivation>, ··· 65 83 } 66 84 67 85 impl Navigator { 68 - @@ -158,6 +170,9 @@ 86 + @@ -158,6 +172,9 @@ 69 87 has_gamepad_gesture: Cell::new(false), 70 88 servo_internals: Default::default(), 71 89 user_activation: Default::default(), ··· 75 93 } 76 94 } 77 95 78 - @@ -170,6 +185,11 @@ 96 + @@ -170,6 +187,11 @@ 79 97 self.xr.get() 80 98 } 81 99 ··· 87 105 #[cfg(feature = "gamepad")] 88 106 pub(crate) fn get_gamepad(&self, index: usize) -> Option<DomRoot<Gamepad>> { 89 107 self.gamepads.borrow().get(index).and_then(|g| g.get()) 90 - @@ -561,6 +581,18 @@ 108 + @@ -561,6 +583,18 @@ 91 109 .or_init(|| ServoInternals::new(&self.global(), CanGc::note())) 92 110 } 93 111 ··· 106 124 /// <https://html.spec.whatwg.org/multipage/#dom-navigator-registerprotocolhandler> 107 125 fn RegisterProtocolHandler(&self, scheme: DOMString, url: USVString) -> Fallible<()> { 108 126 // Step 1. Let (normalizedScheme, normalizedURLString) be the result of 109 - @@ -604,6 +636,62 @@ 127 + @@ -604,6 +638,214 @@ 110 128 self.user_activation 111 129 .or_init(|| UserActivation::new(&self.global(), can_gc)) 112 130 } ··· 161 179 + Ok(promise) 162 180 + } 163 181 + 182 + + fn RequestTask( 183 + + &self, 184 + + cx: JSContext, 185 + + name: DOMString, 186 + + options: HandleValue, 187 + + comp: InRealm, 188 + + can_gc: CanGc, 189 + + ) -> Fallible<Rc<Promise>> { 190 + + let global = self.global(); 191 + + let safe_cx = GlobalScope::get_cx(); 192 + + let promise = Promise::new_in_current_realm(comp, can_gc); 193 + + let task_source = global.task_manager().dom_manipulation_task_source(); 194 + + 195 + + let display: Option<String> = None; // TODO: extract from options if needed 196 + + 197 + + // Serialize options via structured clone (for passing to the provider). 198 + + let data = if options.is_undefined() || options.is_null() { 199 + + None 200 + + } else { 201 + + rooted!(in(*safe_cx) let mut rooted_data = options.get()); 202 + + match structuredclone::write(safe_cx, rooted_data.handle(), None) { 203 + + Ok(serialized) => Some(serialized), 204 + + Err(e) => { 205 + + promise.reject_error(e, can_gc); 206 + + return Ok(promise); 207 + + }, 208 + + } 209 + + }; 210 + + 211 + + // Extract primitive properties (string, number, boolean) from the options 212 + + // object for filter matching in the constellation. Complex values (blobs, objects) 213 + + // are ignored for matching but still passed to the provider via structured clone. 214 + + let caller_data_json = if options.is_object() { 215 + + #[expect(unsafe_code)] 216 + + unsafe { 217 + + use std::ptr::NonNull; 218 + + 219 + + use js::jsapi::{ 220 + + GetPropertyKeys, JS_GetProperty, JS_TypeOfValue, JSITER_OWNONLY, JSType, 221 + + }; 222 + + use js::jsval::UndefinedValue; 223 + + use js::rust::IdVector; 224 + + 225 + + let mut map = serde_json::Map::new(); 226 + + let obj = options.to_object(); 227 + + rooted!(in(*safe_cx) let rooted_obj = obj); 228 + + 229 + + let mut ids = IdVector::new(*safe_cx); 230 + + if GetPropertyKeys( 231 + + *safe_cx, 232 + + rooted_obj.handle().into(), 233 + + JSITER_OWNONLY, 234 + + ids.handle_mut(), 235 + + ) { 236 + + for id in &*ids { 237 + + // Convert property key to string. 238 + + rooted!(in(*safe_cx) let rooted_id = *id); 239 + + rooted!(in(*safe_cx) let mut id_val = UndefinedValue()); 240 + + if !js::jsapi::JS_IdToValue( 241 + + *safe_cx, 242 + + rooted_id.get(), 243 + + id_val.handle_mut().into(), 244 + + ) { 245 + + continue; 246 + + } 247 + + let key_jsstr = js::rust::ToString(*safe_cx, id_val.handle().into()); 248 + + if key_jsstr.is_null() { 249 + + continue; 250 + + } 251 + + let name_str = js::conversions::jsstr_to_string( 252 + + *safe_cx, 253 + + NonNull::new_unchecked(key_jsstr), 254 + + ); 255 + + 256 + + // Get the property value. 257 + + let c_name = match std::ffi::CString::new(name_str.clone()) { 258 + + Ok(c) => c, 259 + + Err(_) => continue, 260 + + }; 261 + + rooted!(in(*safe_cx) let mut val = UndefinedValue()); 262 + + if !JS_GetProperty( 263 + + *safe_cx, 264 + + rooted_obj.handle().into(), 265 + + c_name.as_ptr(), 266 + + val.handle_mut().into(), 267 + + ) { 268 + + continue; 269 + + } 270 + + 271 + + // Only keep primitive types. 272 + + let js_type = JS_TypeOfValue(*safe_cx, val.handle().into()); 273 + + match js_type { 274 + + JSType::JSTYPE_STRING => { 275 + + let jsstr = val.get().to_string(); 276 + + if !jsstr.is_null() { 277 + + let s = js::conversions::jsstr_to_string( 278 + + *safe_cx, 279 + + NonNull::new_unchecked(jsstr), 280 + + ); 281 + + map.insert(name_str, serde_json::Value::String(s)); 282 + + } 283 + + }, 284 + + JSType::JSTYPE_NUMBER => { 285 + + let n = val.get().to_number(); 286 + + if let Some(num) = serde_json::Number::from_f64(n) { 287 + + map.insert(name_str, serde_json::Value::Number(num)); 288 + + } 289 + + }, 290 + + JSType::JSTYPE_BOOLEAN => { 291 + + map.insert( 292 + + name_str, 293 + + serde_json::Value::Bool(val.get().to_boolean()), 294 + + ); 295 + + }, 296 + + _ => { 297 + + // Skip complex types (objects, blobs, functions, etc.) 298 + + }, 299 + + } 300 + + } 301 + + } 302 + + 303 + + serde_json::to_string(&map).unwrap_or_else(|_| "{}".into()) 304 + + } 305 + + } else { 306 + + "{}".to_string() 307 + + }; 308 + + 309 + + // Create a virtual port ID for the provider side. 310 + + // The constellation will set this up as a Task port that intercepts 311 + + // messages and routes the result data through the callback. 312 + + let provider_port_id = MessagePortId::new(); 313 + + 314 + + let callback = callback_promise(&promise, self, task_source); 315 + + 316 + + let chan = global.script_to_constellation_chan(); 317 + + if chan 318 + + .send(ScriptToConstellationMessage::RequestTask( 319 + + name.to_string(), 320 + + caller_data_json, 321 + + display, 322 + + data, 323 + + provider_port_id, 324 + + callback, 325 + + )) 326 + + .is_err() 327 + + { 328 + + promise.reject_error(script_bindings::error::Error::Operation(None), can_gc); 329 + + } 330 + + 331 + + Ok(promise) 332 + + } 333 + + 164 334 + /// <https://webbeef.org/atproto> 165 335 + fn Atproto(&self) -> DomRoot<AtProto> { 166 336 + self.at_proto ··· 169 339 } 170 340 171 341 struct BeaconFetchListener { 342 + @@ -659,3 +901,35 @@ 343 + self.global.root() 344 + } 345 + } 346 + + 347 + +/// Handle the async response from the constellation for `navigator.requestTask()`. 348 + +impl RoutedPromiseListener<Result<StructuredSerializedData, String>> for Navigator { 349 + + fn handle_response( 350 + + &self, 351 + + response: Result<StructuredSerializedData, String>, 352 + + promise: &Rc<Promise>, 353 + + can_gc: CanGc, 354 + + ) { 355 + + match response { 356 + + Ok(data) => { 357 + + let global = self.global(); 358 + + let cx = GlobalScope::get_cx(); 359 + + rooted!(in(*cx) let mut result_val = js::jsval::UndefinedValue()); 360 + + match structuredclone::read(&global, data, result_val.handle_mut(), can_gc) { 361 + + Ok(_ports) => { 362 + + promise.resolve_native(&result_val.handle(), can_gc); 363 + + }, 364 + + Err(e) => { 365 + + promise.reject_error(e, can_gc); 366 + + }, 367 + + } 368 + + }, 369 + + Err(e) => { 370 + + promise.reject_error( 371 + + script_bindings::error::Error::Operation(Some(e.into())), 372 + + can_gc, 373 + + ); 374 + + }, 375 + + } 376 + + } 377 + +}
+95
patches/components/script/dom/processingoptions.rs.patch
··· 1 + --- original 2 + +++ modified 3 + @@ -436,6 +436,7 @@ 4 + pub(crate) enum LinkFetchContextType { 5 + Prefetch, 6 + Preload(PreloadKey), 7 + + WebTasks, 8 + } 9 + 10 + impl From<LinkFetchContextType> for InitiatorType { 11 + @@ -479,7 +480,10 @@ 12 + _: RequestId, 13 + mut chunk: Vec<u8>, 14 + ) { 15 + - if matches!(self.type_, LinkFetchContextType::Preload(..)) { 16 + + if matches!( 17 + + self.type_, 18 + + LinkFetchContextType::Preload(..) | LinkFetchContextType::WebTasks 19 + + ) { 20 + self.response_body.append(&mut chunk); 21 + } 22 + } 23 + @@ -524,6 +528,11 @@ 24 + 25 + submit_timing(cx, &self, &response_result, &timing); 26 + 27 + + // Process web tasks manifest if this was a webtasks link fetch. 28 + + if matches!(self.type_, LinkFetchContextType::WebTasks) && response_result.is_ok() { 29 + + Self::process_webtasks_manifest(&self.response_body, &self.url, &self.global.root()); 30 + + } 31 + + 32 + // Step 11.6. If processResponse is given, then call processResponse with response. 33 + // 34 + // Part of Preload 35 + @@ -543,6 +552,60 @@ 36 + } 37 + } 38 + 39 + +impl LinkFetchContext { 40 + + /// Parse a web tasks manifest and register each provider with the constellation. 41 + + fn process_webtasks_manifest(body: &[u8], base_url: &ServoUrl, global: &GlobalScope) { 42 + + use servo_constellation_traits::ScriptToConstellationMessage; 43 + + 44 + + #[derive(serde::Deserialize)] 45 + + struct TaskDescriptor { 46 + + name: String, 47 + + title: String, 48 + + #[serde(default)] 49 + + description: Option<String>, 50 + + href: String, 51 + + #[serde(default)] 52 + + filters: serde_json::Value, 53 + + #[serde(default)] 54 + + returns: Option<String>, 55 + + #[serde(default = "default_display")] 56 + + display: String, 57 + + #[serde(default)] 58 + + icon: Option<String>, 59 + + } 60 + + fn default_display() -> String { 61 + + "window".to_string() 62 + + } 63 + + 64 + + let Ok(descriptors) = serde_json::from_slice::<Vec<TaskDescriptor>>(body) else { 65 + + log::warn!("Failed to parse webtasks manifest from {base_url}"); 66 + + return; 67 + + }; 68 + + 69 + + let chan = global.script_to_constellation_chan(); 70 + + for desc in descriptors { 71 + + let href = match ServoUrl::parse_with_base(Some(base_url), &desc.href) { 72 + + Ok(url) => url.to_string(), 73 + + Err(_) => { 74 + + log::warn!("Invalid href in webtasks manifest: {}", desc.href); 75 + + continue; 76 + + }, 77 + + }; 78 + + 79 + + let _ = chan.send(ScriptToConstellationMessage::RegisterTaskProvider( 80 + + desc.name, 81 + + desc.title, 82 + + desc.description, 83 + + href, 84 + + serde_json::to_string(&desc.filters).unwrap_or_else(|_| "{}".into()), 85 + + desc.returns, 86 + + desc.display, 87 + + desc.icon, 88 + + )); 89 + + } 90 + + } 91 + +} 92 + + 93 + impl ResourceTimingListener for LinkFetchContext { 94 + fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) { 95 + (self.type_.clone().into(), self.url.clone())
+152 -5
patches/components/script/dom/window.rs.patch
··· 9 9 }; 10 10 use euclid::default::Rect as UntypedRect; 11 11 use euclid::{Point2D, Rect, Scale, Size2D, Vector2D}; 12 + @@ -190,7 +190,7 @@ 13 + use crate::messaging::{MainThreadScriptMsg, ScriptEventLoopReceiver, ScriptEventLoopSender}; 14 + use crate::microtask::{Microtask, UserMicrotask}; 15 + use crate::network_listener::{ResourceTimingListener, submit_timing}; 16 + -use crate::realms::enter_realm; 17 + +use crate::realms::{InRealm, enter_realm}; 18 + use crate::script_runtime::{CanGc, JSContext as SafeJSContext, Runtime}; 19 + use crate::script_thread::ScriptThread; 20 + use crate::script_window_proxies::ScriptWindowProxies; 12 21 @@ -1145,12 +1145,22 @@ 13 22 14 23 let (sender, receiver) = ··· 83 92 // Step 6: Let userPromptHandler be WebDriver BiDi user prompt opened with this, 84 93 // "prompt", and message. 85 94 // TODO: Add support for WebDriver BiDi. 86 - @@ -1669,6 +1697,9 @@ 95 + @@ -1669,6 +1697,26 @@ 87 96 // https://html.spec.whatwg.org/multipage/#windoweventhandlers 88 97 window_event_handlers!(); 89 98 90 99 + // PeerStream event handler 91 100 + event_handler!(peerstream, GetOnpeerstream, SetOnpeerstream); 92 101 + 102 + + fn AcceptTask(&self, comp: InRealm, can_gc: CanGc) -> Fallible<Rc<Promise>> { 103 + + let global = self.upcast::<GlobalScope>(); 104 + + let promise = Promise::new_in_current_realm(comp, can_gc); 105 + + let task_source = global.task_manager().dom_manipulation_task_source(); 106 + + let callback = crate::routed_promise::callback_promise(&promise, self, task_source); 107 + + 108 + + let chan = global.script_to_constellation_chan(); 109 + + if chan 110 + + .send(ScriptToConstellationMessage::AcceptTask(callback)) 111 + + .is_err() 112 + + { 113 + + promise.reject_error(script_bindings::error::Error::Operation(None), can_gc); 114 + + } 115 + + 116 + + Ok(promise) 117 + + } 118 + + 93 119 /// <https://developer.mozilla.org/en-US/docs/Web/API/Window/screen> 94 120 fn Screen(&self, can_gc: CanGc) -> DomRoot<Screen> { 95 121 self.screen.or_init(|| Screen::new(self, can_gc)) 96 - @@ -3040,9 +3071,33 @@ 122 + @@ -3040,9 +3088,33 @@ 97 123 &self, 98 124 input_event: &ConstellationInputEvent, 99 125 ) -> Option<HitTestResult> { ··· 130 156 } 131 157 132 158 #[expect(unsafe_code)] 133 - @@ -3061,8 +3116,25 @@ 159 + @@ -3061,8 +3133,25 @@ 134 160 // SAFETY: This is safe because `Window::query_elements_from_point` has ensured that 135 161 // layout has run and any OpaqueNodes that no longer refer to real nodes are gone. 136 162 let address = UntrustedNodeAddress(result.node.0 as *const c_void); ··· 157 183 cursor: result.cursor, 158 184 point_in_node: result.point_in_target, 159 185 point_in_frame, 160 - @@ -3605,6 +3677,8 @@ 186 + @@ -3605,6 +3694,8 @@ 161 187 player_context: WindowGLContext, 162 188 #[cfg(feature = "webgpu")] gpu_id_hub: Arc<IdentityHub>, 163 189 inherited_secure_context: Option<bool>, ··· 166 192 theme: Theme, 167 193 weak_script_thread: Weak<ScriptThread>, 168 194 ) -> DomRoot<Self> { 169 - @@ -3631,6 +3705,8 @@ 195 + @@ -3631,6 +3722,8 @@ 170 196 gpu_id_hub, 171 197 inherited_secure_context, 172 198 unminify_js, ··· 175 201 Some(font_context), 176 202 ), 177 203 ongoing_navigation: Default::default(), 204 + @@ -3906,3 +3999,120 @@ 205 + Self::create_named_properties_object(cx, proto, object) 206 + } 207 + } 208 + + 209 + +/// Handle the AcceptTask response from the constellation. 210 + +impl 211 + + crate::routed_promise::RoutedPromiseListener< 212 + + Option<(String, Option<StructuredSerializedData>, Vec<u8>)>, 213 + + > for Window 214 + +{ 215 + + #[expect(unsafe_code)] 216 + + fn handle_response( 217 + + &self, 218 + + response: Option<(String, Option<StructuredSerializedData>, Vec<u8>)>, 219 + + promise: &Rc<Promise>, 220 + + can_gc: CanGc, 221 + + ) { 222 + + use js::jsapi::{JS_NewObject, JSPROP_ENUMERATE}; 223 + + use js::jsval::{ObjectValue, UndefinedValue}; 224 + + use js::rust::wrappers::JS_DefineProperty; 225 + + use script_bindings::conversions::SafeToJSValConvertible; 226 + + 227 + + let Some((task_name, data, port_id_bytes)) = response else { 228 + + promise.reject_error( 229 + + script_bindings::error::Error::Operation(Some( 230 + + "No task assigned to this page".into(), 231 + + )), 232 + + can_gc, 233 + + ); 234 + + return; 235 + + }; 236 + + 237 + + let global = self.upcast::<GlobalScope>(); 238 + + 239 + + // Deserialize the provider port ID. 240 + + let Ok(provider_port_id) = 241 + + postcard::from_bytes::<servo_base::id::MessagePortId>(&port_id_bytes) 242 + + else { 243 + + promise.reject_error( 244 + + script_bindings::error::Error::Operation(Some("Invalid port ID".into())), 245 + + can_gc, 246 + + ); 247 + + return; 248 + + }; 249 + + 250 + + // Create a local port entangled with the provider_port_id. 251 + + let local_port = crate::dom::messageport::MessagePort::new(global, can_gc); 252 + + global.track_message_port(&local_port, None); 253 + + global.set_port_entanglement(*local_port.message_port_id(), provider_port_id); 254 + + 255 + + // Tell the constellation to set bidirectional entanglement. 256 + + let _ = global.script_to_constellation_chan().send( 257 + + ScriptToConstellationMessage::EntanglePorts( 258 + + *local_port.message_port_id(), 259 + + provider_port_id, 260 + + ), 261 + + ); 262 + + 263 + + // Deserialize the task data. 264 + + let cx = GlobalScope::get_cx(); 265 + + rooted!(in(*cx) let mut data_val = UndefinedValue()); 266 + + if let Some(data) = data { 267 + + let _ = crate::dom::bindings::structuredclone::read( 268 + + global, 269 + + data, 270 + + data_val.handle_mut(), 271 + + can_gc, 272 + + ); 273 + + } 274 + + 275 + + // Build a plain JS object: { taskName, data, port } 276 + + unsafe { 277 + + rooted!(in(*cx) let result_obj = JS_NewObject(*cx, std::ptr::null())); 278 + + if result_obj.get().is_null() { 279 + + promise.reject_error( 280 + + script_bindings::error::Error::Operation(Some( 281 + + "Failed to create result".into(), 282 + + )), 283 + + can_gc, 284 + + ); 285 + + return; 286 + + } 287 + + 288 + + rooted!(in(*cx) let mut val = UndefinedValue()); 289 + + 290 + + // name 291 + + task_name.safe_to_jsval(cx, val.handle_mut(), can_gc); 292 + + JS_DefineProperty( 293 + + *cx, 294 + + result_obj.handle(), 295 + + c"name".as_ptr(), 296 + + val.handle(), 297 + + JSPROP_ENUMERATE as u32, 298 + + ); 299 + + 300 + + // data 301 + + JS_DefineProperty( 302 + + *cx, 303 + + result_obj.handle(), 304 + + c"data".as_ptr(), 305 + + data_val.handle(), 306 + + JSPROP_ENUMERATE as u32, 307 + + ); 308 + + 309 + + // port 310 + + rooted!(in(*cx) let mut port_val = UndefinedValue()); 311 + + local_port.safe_to_jsval(cx, port_val.handle_mut(), can_gc); 312 + + JS_DefineProperty( 313 + + *cx, 314 + + result_obj.handle(), 315 + + c"port".as_ptr(), 316 + + port_val.handle(), 317 + + JSPROP_ENUMERATE as u32, 318 + + ); 319 + + 320 + + rooted!(in(*cx) let result_val = ObjectValue(result_obj.get())); 321 + + promise.resolve_native(&result_val.handle(), can_gc); 322 + + } 323 + + } 324 + +}
+30 -1
patches/components/script/links.rs.patch
··· 1 1 --- original 2 2 +++ modified 3 - @@ -434,7 +434,7 @@ 3 + @@ -110,6 +110,9 @@ 4 + 5 + /// <https://html.spec.whatwg.org/multipage/#link-type-terms-of-service> 6 + const TERMS_OF_SERVICE = 1 << 26; 7 + + 8 + + /// Custom: web tasks provider manifest. 9 + + const WEBTASKS = 1 << 27; 10 + } 11 + } 12 + 13 + @@ -136,7 +139,8 @@ 14 + .union(Self::PRIVACY_POLICY) 15 + .union(Self::SEARCH) 16 + .union(Self::STYLESHEET) 17 + - .union(Self::TERMS_OF_SERVICE); 18 + + .union(Self::TERMS_OF_SERVICE) 19 + + .union(Self::WEBTASKS); 20 + 21 + /// The set of allowed relations for [`<a>`] and [`<area>`] elements 22 + /// 23 + @@ -283,6 +287,8 @@ 24 + Self::TAG 25 + } else if keyword.eq_ignore_ascii_case("terms-of-service") { 26 + Self::TERMS_OF_SERVICE 27 + + } else if keyword.eq_ignore_ascii_case("webtasks") { 28 + + Self::WEBTASKS 29 + } else { 30 + Self::empty() 31 + } 32 + @@ -434,7 +440,7 @@ 4 33 let source = document.browsing_context().unwrap(); 5 34 let (maybe_chosen, history_handling) = match target_attribute_value { 6 35 Some(name) => {
+4 -1
patches/components/script/messaging.rs.patch
··· 1 1 --- original 2 2 +++ modified 3 - @@ -109,6 +109,10 @@ 3 + @@ -109,6 +109,13 @@ 4 4 ScriptThreadMessage::UpdatePinchZoomInfos(id, _) => Some(*id), 5 5 ScriptThreadMessage::SetAccessibilityActive(..) => None, 6 6 ScriptThreadMessage::TriggerGarbageCollection => None, ··· 8 8 + ScriptThreadMessage::DispatchServoError(..) => None, 9 9 + ScriptThreadMessage::DispatchPairingEvent(..) => None, 10 10 + ScriptThreadMessage::DispatchPeerStream(..) => None, 11 + + ScriptThreadMessage::ShowTaskChooser(..) => None, 12 + + ScriptThreadMessage::OpenTaskProvider(..) => None, 13 + + ScriptThreadMessage::TaskProvidersUpdate(..) => None, 11 14 }, 12 15 MixedMessage::FromScript(inner_msg) => match inner_msg { 13 16 MainThreadScriptMsg::Common(CommonScriptMsg::Task(_, _, pipeline_id, _)) => {
+80 -10
patches/components/script/script_thread.rs.patch
··· 54 54 use crate::dom::servoparser::{ParserContext, ServoParser}; 55 55 use crate::dom::types::DebuggerGlobalScope; 56 56 #[cfg(feature = "webgpu")] 57 - @@ -1921,11 +1928,22 @@ 57 + @@ -1921,12 +1928,45 @@ 58 58 self.handle_refresh_cursor(pipeline_id); 59 59 }, 60 60 ScriptThreadMessage::PreferencesUpdated(updates) => { ··· 79 79 + // Dispatch preferencechanged events to all Embedder instances 80 80 + self.dispatch_preference_changed_to_embedders(&updates, CanGc::from_cx(cx)); 81 81 }, 82 + + ScriptThreadMessage::ShowTaskChooser(request_id, task_name, providers_json) => { 83 + + // Dispatch taskrequest event to all Embedder instances. 84 + + // Only the system UI will have a handler registered. 85 + + self.dispatch_task_request_to_embedders( 86 + + &request_id, 87 + + &task_name, 88 + + &providers_json, 89 + + CanGc::from_cx(cx), 90 + + ); 91 + + }, 92 + + ScriptThreadMessage::OpenTaskProvider(request_id, url, title) => { 93 + + // Dispatch an embedder event so the system UI opens the provider webview. 94 + + self.dispatch_open_task_provider(&request_id, &url, &title, CanGc::from_cx(cx)); 95 + + }, 96 + + ScriptThreadMessage::TaskProvidersUpdate(request_id, providers_json) => { 97 + + // Dispatch an embedder event to update the chooser with remote providers. 98 + + self.dispatch_task_providers_update( 99 + + &request_id, 100 + + &providers_json, 101 + + CanGc::from_cx(cx), 102 + + ); 103 + + }, 82 104 ScriptThreadMessage::ForwardKeyboardScroll(pipeline_id, scroll) => { 83 105 if let Some(document) = self.documents.borrow().find_document(pipeline_id) { 84 - @@ -1966,6 +1984,35 @@ 106 + document.event_handler().do_keyboard_scroll(scroll); 107 + @@ -1966,6 +2006,35 @@ 85 108 ScriptThreadMessage::TriggerGarbageCollection => unsafe { 86 109 JS_GC(*GlobalScope::get_cx(), GCReason::API); 87 110 }, ··· 117 140 } 118 141 } 119 142 120 - @@ -3007,6 +3054,9 @@ 143 + @@ -3007,6 +3076,9 @@ 121 144 .documents 122 145 .borrow() 123 146 .find_iframe(parent_pipeline_id, browsing_context_id); ··· 127 150 if let Some(frame_element) = frame_element { 128 151 frame_element.update_pipeline_id(new_pipeline_id, reason, cx); 129 152 } 130 - @@ -3026,6 +3076,7 @@ 153 + @@ -3026,6 +3098,7 @@ 131 154 // is no need to pass along existing opener information that 132 155 // will be discarded. 133 156 None, ··· 135 158 ); 136 159 } 137 160 } 138 - @@ -3304,6 +3355,155 @@ 161 + @@ -3304,6 +3377,155 @@ 139 162 } 140 163 } 141 164 ··· 291 314 fn ask_constellation_for_top_level_info( 292 315 &self, 293 316 sender_webview_id: WebViewId, 294 - @@ -3422,7 +3622,13 @@ 317 + @@ -3422,7 +3644,13 @@ 295 318 self.senders.pipeline_to_embedder_sender.clone(), 296 319 self.senders.constellation_sender.clone(), 297 320 incomplete.pipeline_id, ··· 306 329 incomplete.viewport_details, 307 330 origin.clone(), 308 331 final_url.clone(), 309 - @@ -3444,6 +3650,8 @@ 332 + @@ -3444,6 +3672,8 @@ 310 333 #[cfg(feature = "webgpu")] 311 334 self.gpu_id_hub.clone(), 312 335 incomplete.load_data.inherited_secure_context, ··· 315 338 incomplete.theme, 316 339 self.this.clone(), 317 340 ); 318 - @@ -3467,6 +3675,7 @@ 341 + @@ -3467,6 +3697,7 @@ 319 342 incomplete.webview_id, 320 343 incomplete.parent_info, 321 344 incomplete.opener, ··· 323 346 ); 324 347 if window_proxy.parent().is_some() { 325 348 // https://html.spec.whatwg.org/multipage/#navigating-across-documents:delaying-load-events-mode-2 326 - @@ -4287,6 +4496,24 @@ 349 + @@ -4287,10 +4518,71 @@ 327 350 document.event_handler().handle_refresh_cursor(); 328 351 } 329 352 ··· 345 368 + } 346 369 + } 347 370 + 371 + + /// Dispatch taskrequest events to all Embedder instances in this script thread. 372 + + /// Only the system UI will typically have a handler for this event. 373 + + fn dispatch_task_request_to_embedders( 374 + + &self, 375 + + request_id: &str, 376 + + task_name: &str, 377 + + providers_json: &str, 378 + + can_gc: CanGc, 379 + + ) { 380 + + for (_, document) in self.documents.borrow().iter() { 381 + + if let Some(embedder) = document.window().Navigator().get_embedder() { 382 + + let _ac = enter_realm(&*embedder); 383 + + embedder.dispatch_task_request(request_id, task_name, providers_json, can_gc); 384 + + } 385 + + } 386 + + } 387 + + 348 388 pub(crate) fn is_servo_privileged(url: ServoUrl) -> bool { 349 389 with_script_thread(|script_thread| script_thread.privileged_urls.contains(&url)) 350 390 } 351 - @@ -4331,7 +4558,7 @@ 391 + 392 + + /// Dispatch an opentaskprovider event to all Embedder instances. 393 + + /// The system UI will open a new webview with the provider URL. 394 + + fn dispatch_open_task_provider(&self, request_id: &str, url: &str, title: &str, can_gc: CanGc) { 395 + + for (_, document) in self.documents.borrow().iter() { 396 + + if let Some(embedder) = document.window().Navigator().get_embedder() { 397 + + let _ac = enter_realm(&*embedder); 398 + + embedder.dispatch_open_task_provider(request_id, url, title, can_gc); 399 + + } 400 + + } 401 + + } 402 + + 403 + + /// Dispatch a taskprovidersupdate event to all Embedder instances. 404 + + fn dispatch_task_providers_update( 405 + + &self, 406 + + request_id: &str, 407 + + providers_json: &str, 408 + + can_gc: CanGc, 409 + + ) { 410 + + for (_, document) in self.documents.borrow().iter() { 411 + + if let Some(embedder) = document.window().Navigator().get_embedder() { 412 + + let _ac = enter_realm(&*embedder); 413 + + embedder.dispatch_task_providers_update(request_id, providers_json, can_gc); 414 + + } 415 + + } 416 + + } 417 + + 418 + fn handle_request_screenshot_readiness( 419 + &self, 420 + webview_id: WebViewId, 421 + @@ -4331,7 +4623,7 @@ 352 422 can_gc: CanGc, 353 423 ) { 354 424 let Some(window) = self.documents.borrow().find_window(pipeline_id) else {
+12 -2
patches/components/script_bindings/codegen/Bindings.conf.patch
··· 30 30 'Navigator': { 31 31 - 'inRealms': ['GetVRDisplays'], 32 32 - 'canGc': ['Languages', 'SendBeacon', 'UserActivation'], 33 - + 'inRealms': ['GetVRDisplays', 'CreatePeerStream'], 34 - + 'canGc': ['Languages', 'SendBeacon', 'UserActivation', 'CreatePeerStream'], 33 + + 'inRealms': ['GetVRDisplays', 'CreatePeerStream', 'RequestTask'], 34 + + 'canGc': ['Languages', 'SendBeacon', 'UserActivation', 'CreatePeerStream', 'RequestTask'], 35 35 }, 36 36 37 37 'Node': { ··· 47 47 'PerformanceObserver': { 48 48 'canGc': ['SupportedEntryTypes'], 49 49 }, 50 + @@ -842,7 +857,8 @@ 51 + }, 52 + 53 + 'Window': { 54 + - 'canGc': ['CookieStore', 'FetchLater', 'GetVisualViewport', 'ReportError', 'Screen', 'StructuredClone'], 55 + + 'canGc': ['CookieStore', 'FetchLater', 'GetVisualViewport', 'ReportError', 'Screen', 'StructuredClone', 'AcceptTask'], 56 + + 'inRealms': ['AcceptTask'], 57 + 'additionalTraits': ['crate::interfaces::WindowHelpers'], 58 + 'realm': ['CreateImageBitmap', 'CreateImageBitmap_', 'WebdriverCallback', 'GetOpener', 'Fetch'], 59 + 'cx': ['Location', 'Open', 'PostMessage', 'PostMessage_', 'SetInterval', 'SetTimeout', 'Stop', 'TrustedTypes', 'WebdriverException']
+26 -1
patches/components/script_bindings/webidls/Embedder.webidl.patch
··· 1 1 --- original 2 2 +++ modified 3 - @@ -0,0 +1,37 @@ 3 + @@ -0,0 +1,62 @@ 4 4 +/* SPDX Id: AGPL-3.0-or-later */ 5 5 + 6 6 +// Servo-specific API for communication between web content and the embedder. ··· 25 25 + // This allows the window to be resized from web content. 26 26 + undefined startWindowResize(); 27 27 + 28 + + // Respond to a web task request with the selected provider id, or null to cancel. 29 + + undefined respondToTaskRequest(DOMString requestId, DOMString? providerId); 30 + + 31 + + // Register a task provider. Only available to privileged pages. 32 + + undefined registerTaskProvider(TaskProviderDescriptor descriptor); 33 + + 28 34 + // Event fired when Servo encounters an error. 29 35 + // The event is a CustomEvent with detail: { errorType: string, message: string } 30 36 + attribute EventHandler onservoerror; ··· 32 38 + // Event fired when a preference changes. 33 39 + // The event is a CustomEvent with detail: { name: string, value: any } 34 40 + attribute EventHandler onpreferencechanged; 41 + + 42 + + // Event fired when a page requests a web task delegation. 43 + + // The event is a CustomEvent with detail: { taskName: string, providers: array, respond: function } 44 + + attribute EventHandler ontaskrequest; 45 + + 46 + + // Event fired when additional remote task providers are discovered. 47 + + // The event is a CustomEvent with detail: { requestId: string, providers: string (JSON) } 48 + + attribute EventHandler ontaskprovidersupdate; 35 49 +}; 36 50 + 37 51 +partial interface Navigator { 38 52 + [Func="Embedder::is_allowed_to_embed"] 39 53 + readonly attribute Embedder embedder; 40 54 +}; 55 + + 56 + +dictionary TaskProviderDescriptor { 57 + + required DOMString name; 58 + + required DOMString title; 59 + + DOMString description; 60 + + required USVString href; 61 + + DOMString filters = "{}"; 62 + + DOMString returns; 63 + + DOMString display = "window"; 64 + + USVString icon; 65 + +};
+29
patches/components/script_bindings/webidls/WebTasks.webidl.patch
··· 1 + --- original 2 + +++ modified 3 + @@ -0,0 +1,26 @@ 4 + +/* SPDX-License-Identifier: AGPL-3.0-or-later */ 5 + + 6 + +/// Web Tasks API — allows any page to request a task and have the system 7 + +/// find and launch a suitable provider. 8 + +/// 9 + +/// The options parameter is a plain JS object whose properties are: 10 + +/// - Matched against provider filters for provider selection 11 + +/// - Passed as-is to the provider via acceptTask() 12 + +/// 13 + +/// Example: navigator.requestTask("share", { type: "url", url: "https://...", text: "Hi" }) 14 + + 15 + +partial interface Navigator { 16 + + [Throws] Promise<any> requestTask(DOMString name, optional any options); 17 + +}; 18 + + 19 + +/// Data returned by window.acceptTask(). 20 + +dictionary TaskStartData { 21 + + required DOMString name; 22 + + any data; 23 + + required MessagePort port; 24 + +}; 25 + + 26 + +partial interface Window { 27 + + /// Called by a task provider to accept and receive the task. 28 + + [Throws] Promise<TaskStartData> acceptTask(); 29 + +};
+31 -1
patches/components/shared/constellation/from_script_message.rs.patch
··· 209 209 /// Mark a new document as active 210 210 ActivateDocument, 211 211 /// Set the document state for a pipeline (used by screenshot / reftests) 212 - @@ -722,6 +855,79 @@ 212 + @@ -722,6 +855,109 @@ 213 213 RespondToScreenshotReadinessRequest(ScreenshotReadinessResponse), 214 214 /// Request the constellation to force garbage collection in all `ScriptThread`'s. 215 215 TriggerGarbageCollection, ··· 286 286 + PeerStreamResponse(String, String, bool), 287 287 + /// ATProto api message. 288 288 + AtProto(AtProtoRequest, GenericCallback<AtProtoResult>), 289 + + /// Request a web task delegation. The constellation will find matching providers, 290 + + /// show a chooser, launch the selected provider, and return the result. 291 + + /// Args: task_name, caller_data_json (for filter matching), display_preference, data (structured clone), provider_port_id, callback. 292 + + RequestTask( 293 + + String, 294 + + String, 295 + + Option<String>, 296 + + Option<StructuredSerializedData>, 297 + + MessagePortId, 298 + + GenericCallback<Result<StructuredSerializedData, String>>, 299 + + ), 300 + + /// Register a task provider. 301 + + /// Args: task_name, title, description, href, filters_json, returns_type, display_mode, icon. 302 + + RegisterTaskProvider( 303 + + String, 304 + + String, 305 + + Option<String>, 306 + + String, 307 + + String, 308 + + Option<String>, 309 + + String, 310 + + Option<String>, 311 + + ), 312 + + /// User selected a task provider (or cancelled). Sent by the system UI's 313 + + /// respondToTaskRequest(). Args: request_id, provider_id (None = cancelled). 314 + + TaskProviderSelected(String, Option<String>), 315 + + /// A task provider page calls window.acceptTask() to receive its task. 316 + + /// Constellation responds with the task data via callback. 317 + + /// Args: callback(task_name, data, provider_port_id_bytes). 318 + + AcceptTask(GenericCallback<Option<(String, Option<StructuredSerializedData>, Vec<u8>)>>), 289 319 } 290 320 291 321 impl fmt::Debug for ScriptToConstellationMessage {
+17 -1
patches/components/shared/script/lib.rs.patch
··· 35 35 } 36 36 37 37 /// When a pipeline is closed, should its browsing context be discarded too? 38 - @@ -322,6 +330,24 @@ 38 + @@ -286,6 +294,15 @@ 39 + SendImageKeysBatch(PipelineId, Vec<ImageKey>), 40 + /// Preferences were updated in the parent process. 41 + PreferencesUpdated(Vec<(String, PrefValue)>), 42 + + /// A web task request needs to be shown to the system UI. 43 + + /// Fields: request_id, task_name, providers_json. 44 + + ShowTaskChooser(String, String, String), 45 + + /// Tell the system UI to open a task provider webview. 46 + + /// Fields: request_id, provider_url, provider_title. 47 + + OpenTaskProvider(String, String, String), 48 + + /// Update the system UI with additional remote task providers. 49 + + /// Fields: request_id, providers_json. 50 + + TaskProvidersUpdate(String, String), 51 + /// Notify the `ScriptThread` that the Servo renderer is no longer waiting on 52 + /// asynchronous image uploads for the given `Pipeline`. These are mainly used 53 + /// by canvas to perform uploads while the display list is being built. 54 + @@ -322,6 +339,24 @@ 39 55 SetAccessibilityActive(PipelineId, bool), 40 56 /// Force a garbage collection in this script thread. 41 57 TriggerGarbageCollection,
+1
ui/shared/search/utils.js
··· 26 26 if ( 27 27 !url.startsWith("about:") && 28 28 !url.startsWith("at:") && 29 + !url.startsWith("beaver:") && 29 30 !url.startsWith("tile:") && 30 31 !url.startsWith("data:") && 31 32 !url.startsWith("file://") &&
+3
ui/system/index.html
··· 4 4 <head> 5 5 <title>Beaver</title> 6 6 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" /> 7 + <link rel="webtasks" href="tasks.json" /> 7 8 <link rel="stylesheet" href="beaver://theme/index.css" /> 8 9 <link rel="stylesheet" href="beaver://shared/fonts/fonts.css" /> 9 10 <link rel="stylesheet" href="beaver://shared/third_party/lucide/lucide.css" /> ··· 16 17 <script type="module" src="notification_panel.js"></script> 17 18 <script type="module" src="media_control.js"></script> 18 19 <script type="module" src="toasts.js"></script> 20 + <script type="module" src="task_chooser.js"></script> 19 21 <script type="module" src="index.js"></script> 20 22 </head> 21 23 <body> ··· 42 44 <div id="root"></div> 43 45 </main> 44 46 <toast-manager id="toast-manager"></toast-manager> 47 + <task-chooser></task-chooser> 45 48 <footer id="footer"> 46 49 </footer> 47 50 </body>
+17
ui/system/index.js
··· 1177 1177 } 1178 1178 }); 1179 1179 1180 + // Open a task provider in a new webview (window mode). 1181 + document.body.addEventListener("open-task-provider", (e) => { 1182 + const { url, title } = e.detail; 1183 + console.log(`[WebTasks] Opening provider: ${title} (${url})`); 1184 + openView(url, title); 1185 + }); 1186 + 1187 + // Show an inline task provider overlay on the active webview. 1188 + document.body.addEventListener("show-inline-provider", (e) => { 1189 + const { url, title } = e.detail; 1190 + console.log(`[WebTasks] Showing inline provider: ${title} (${url})`); 1191 + const entry = layoutManager.webviews.get(layoutManager.activeWebviewId); 1192 + if (entry) { 1193 + entry.webview.showInlineProvider(url, title); 1194 + } 1195 + }); 1196 + 1180 1197 // Send current page URL to a peer when "Open in <peer>" is selected. 1181 1198 document.getElementById("root").addEventListener("p2p-open-in", async (e) => { 1182 1199 const { peerId, url } = e.detail;
+297
ui/system/task_chooser.js
··· 1 + // SPDX-License-Identifier: AGPL-3.0-or-later 2 + 3 + // Task chooser UI component. 4 + // Listens for taskrequest events on navigator.embedder, displays a list of 5 + // matching task providers, and responds with the user's selection. 6 + // Handles both window mode (new webview) and inline mode (overlay dialog). 7 + 8 + import { 9 + LitElement, 10 + html, 11 + css, 12 + } from "beaver://shared/third_party/lit/lit-all.min.js"; 13 + 14 + export class TaskChooser extends LitElement { 15 + static properties = { 16 + open: { type: Boolean, reflect: true }, 17 + _taskName: { state: true }, 18 + _providers: { state: true }, 19 + _requestId: { state: true }, 20 + }; 21 + 22 + static styles = css` 23 + :host { 24 + display: none; 25 + position: fixed; 26 + inset: 0; 27 + z-index: var(--z-modal, 8000); 28 + } 29 + 30 + :host([open]) { 31 + display: flex; 32 + } 33 + 34 + .overlay { 35 + position: absolute; 36 + inset: 0; 37 + background: var(--color-backdrop, rgba(0, 0, 0, 0.4)); 38 + display: flex; 39 + align-items: flex-end; 40 + justify-content: center; 41 + } 42 + 43 + .panel { 44 + background: var(--bg-surface, #fff); 45 + border-radius: var(--radius-md, 12px) var(--radius-md, 12px) 0 0; 46 + padding: 1em; 47 + width: 100%; 48 + max-width: 400px; 49 + max-height: 60vh; 50 + overflow-y: auto; 51 + box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15); 52 + font-family: var(--font-family-base, system-ui, sans-serif); 53 + } 54 + 55 + .title { 56 + font-size: 0.95em; 57 + font-weight: 600; 58 + color: var(--color-text, #333); 59 + margin-bottom: 0.75em; 60 + } 61 + 62 + .provider { 63 + display: flex; 64 + align-items: center; 65 + gap: 0.75em; 66 + padding: 0.6em 0.5em; 67 + border-radius: var(--radius-sm, 8px); 68 + cursor: pointer; 69 + transition: background 0.15s; 70 + } 71 + 72 + .provider:hover { 73 + background: var(--bg-hover, #f0f0f0); 74 + } 75 + 76 + .provider-icon { 77 + width: 24px; 78 + height: 24px; 79 + border-radius: 4px; 80 + object-fit: contain; 81 + flex-shrink: 0; 82 + } 83 + 84 + .provider-title { 85 + font-size: 0.9em; 86 + font-weight: 500; 87 + color: var(--color-text, #333); 88 + } 89 + 90 + .provider-description { 91 + font-size: 0.8em; 92 + color: var(--color-text-secondary, #666); 93 + } 94 + 95 + .provider-origin { 96 + font-size: 0.75em; 97 + color: var(--color-text-secondary, #999); 98 + } 99 + 100 + .cancel { 101 + display: block; 102 + width: 100%; 103 + margin-top: 0.75em; 104 + padding: 0.6em; 105 + border: 1px solid var(--color-border, #ddd); 106 + border-radius: var(--radius-sm, 8px); 107 + background: var(--bg-surface, #fff); 108 + color: var(--color-text, #333); 109 + font-size: 0.9em; 110 + cursor: pointer; 111 + text-align: center; 112 + } 113 + 114 + .cancel:hover { 115 + background: var(--bg-hover, #f0f0f0); 116 + } 117 + `; 118 + 119 + constructor() { 120 + super(); 121 + this.open = false; 122 + this._taskName = ""; 123 + this._providers = []; 124 + this._requestId = ""; 125 + this._selectedDisplay = null; 126 + } 127 + 128 + connectedCallback() { 129 + super.connectedCallback(); 130 + if (navigator.embedder) { 131 + this._onTaskRequest = (e) => this._handleTaskRequest(e); 132 + navigator.embedder.addEventListener("taskrequest", this._onTaskRequest); 133 + 134 + this._onOpenProvider = (e) => this._handleOpenProvider(e); 135 + navigator.embedder.addEventListener( 136 + "opentaskprovider", 137 + this._onOpenProvider, 138 + ); 139 + 140 + this._onProvidersUpdate = (e) => this._handleProvidersUpdate(e); 141 + navigator.embedder.addEventListener( 142 + "taskprovidersupdate", 143 + this._onProvidersUpdate, 144 + ); 145 + } 146 + } 147 + 148 + disconnectedCallback() { 149 + super.disconnectedCallback(); 150 + if (navigator.embedder) { 151 + if (this._onTaskRequest) { 152 + navigator.embedder.removeEventListener( 153 + "taskrequest", 154 + this._onTaskRequest, 155 + ); 156 + } 157 + if (this._onOpenProvider) { 158 + navigator.embedder.removeEventListener( 159 + "opentaskprovider", 160 + this._onOpenProvider, 161 + ); 162 + } 163 + if (this._onProvidersUpdate) { 164 + navigator.embedder.removeEventListener( 165 + "taskprovidersupdate", 166 + this._onProvidersUpdate, 167 + ); 168 + } 169 + } 170 + } 171 + 172 + _handleProvidersUpdate(e) { 173 + const { requestId, providers } = e.detail; 174 + if (requestId !== this._requestId) return; 175 + 176 + try { 177 + const newProviders = 178 + typeof providers === "string" ? JSON.parse(providers) : providers; 179 + if (newProviders.length > 0) { 180 + console.log( 181 + `[TaskChooser] Remote providers update: +${newProviders.length}`, 182 + ); 183 + this._providers = [...this._providers, ...newProviders]; 184 + } 185 + } catch { 186 + // Ignore parse errors. 187 + } 188 + } 189 + 190 + _handleOpenProvider(e) { 191 + const { requestId, url, title } = e.detail; 192 + console.log(`[TaskChooser] Opening provider: ${title} (${url})`); 193 + 194 + if (this._selectedDisplay === "inline") { 195 + // Dispatch event to show inline provider on the caller's webview. 196 + this.dispatchEvent( 197 + new CustomEvent("show-inline-provider", { 198 + bubbles: true, 199 + composed: true, 200 + detail: { requestId, url, title }, 201 + }), 202 + ); 203 + } else { 204 + // Dispatch event to index.js to open the provider in a new webview. 205 + this.dispatchEvent( 206 + new CustomEvent("open-task-provider", { 207 + bubbles: true, 208 + composed: true, 209 + detail: { requestId, url, title }, 210 + }), 211 + ); 212 + } 213 + } 214 + 215 + _handleTaskRequest(e) { 216 + const { requestId, taskName, providers } = e.detail; 217 + console.log("[TaskChooser] Task request received:", taskName, requestId); 218 + 219 + this._requestId = requestId; 220 + this._taskName = taskName; 221 + this._selectedDisplay = null; 222 + 223 + try { 224 + this._providers = 225 + typeof providers === "string" ? JSON.parse(providers) : providers; 226 + } catch { 227 + this._providers = []; 228 + } 229 + 230 + if (this._providers.length === 0) { 231 + console.log("[TaskChooser] No providers, cancelling"); 232 + this._respond(null); 233 + return; 234 + } 235 + 236 + this.open = true; 237 + } 238 + 239 + _selectProvider(provider) { 240 + console.log("[TaskChooser] Provider selected:", provider.id, provider.title); 241 + // Store display mode for when opentaskprovider arrives. 242 + this._selectedDisplay = provider.display === "Inline" ? "inline" : "window"; 243 + this._respond(provider.id); 244 + this.open = false; 245 + } 246 + 247 + _cancel() { 248 + console.log("[TaskChooser] Cancelled"); 249 + this._respond(null); 250 + this.open = false; 251 + } 252 + 253 + _respond(providerId) { 254 + if (this._requestId && navigator.embedder) { 255 + navigator.embedder.respondToTaskRequest(this._requestId, providerId); 256 + this._requestId = ""; 257 + } 258 + } 259 + 260 + render() { 261 + if (!this.open) return html``; 262 + 263 + // Provider chooser. 264 + return html` 265 + <div class="overlay" @click=${this._cancel}> 266 + <div class="panel" @click=${(e) => e.stopPropagation()}> 267 + <div class="title">${this._taskName} with...</div> 268 + ${this._providers.map( 269 + (p) => html` 270 + <div class="provider" @click=${() => this._selectProvider(p)}> 271 + ${p.icon 272 + ? html`<img class="provider-icon" src=${p.icon} alt="" />` 273 + : ""} 274 + <div> 275 + <div class="provider-title">${p.title}</div> 276 + ${p.description 277 + ? html`<div class="provider-description"> 278 + ${p.description} 279 + </div>` 280 + : ""} 281 + <div class="provider-origin"> 282 + ${p.origin}${p.device_name 283 + ? html` <span class="device-name">(${p.device_name})</span>` 284 + : ""} 285 + </div> 286 + </div> 287 + </div> 288 + `, 289 + )} 290 + <button class="cancel" @click=${this._cancel}>Cancel</button> 291 + </div> 292 + </div> 293 + `; 294 + } 295 + } 296 + 297 + customElements.define("task-chooser", TaskChooser);
+13
ui/system/tasks.json
··· 1 + [ 2 + { 3 + "name": "share", 4 + "title": "Share", 5 + "description": "Share content", 6 + "href": "beaver://system/share_provider.html", 7 + "filters": { 8 + "type": ["url", "text"] 9 + }, 10 + "display": "window", 11 + "icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXNoYXJlLWljb24gbHVjaWRlLXNoYXJlIj48cGF0aCBkPSJNMTIgMnYxMyIvPjxwYXRoIGQ9Im0xNiA2LTQtNC00IDQiLz48cGF0aCBkPSJNNCAxMnY4YTIgMiAwIDAgMCAyIDJoMTJhMiAyIDAgMCAwIDItMnYtOCIvPjwvc3ZnPg==" 12 + } 13 + ]
+61
ui/system/web_view.css
··· 283 283 :host(.mobile-mode) .iframe-container { 284 284 border-radius: 0; 285 285 } 286 + 287 + /* Inline task provider overlay */ 288 + :host .inline-provider-overlay { 289 + position: absolute; 290 + inset: 0; 291 + background: var(--color-backdrop, rgba(0, 0, 0, 0.4)); 292 + display: flex; 293 + align-items: center; 294 + justify-content: center; 295 + z-index: var(--z-dialog, 1000); 296 + } 297 + 298 + :host .inline-provider-dialog { 299 + background: var(--bg-surface, #fff); 300 + border-radius: var(--radius-md, 12px); 301 + width: 90%; 302 + max-width: 500px; 303 + height: 70%; 304 + max-height: 600px; 305 + display: flex; 306 + flex-direction: column; 307 + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); 308 + overflow: hidden; 309 + } 310 + 311 + :host .inline-provider-header { 312 + display: flex; 313 + align-items: center; 314 + gap: 0.5em; 315 + padding: var(--spacing-xs, 0.5em) var(--spacing-sm, 0.75em); 316 + background: var(--bg-surface, #f5f5f5); 317 + border-bottom: 1px solid var(--color-border, #ddd); 318 + font-family: var(--font-family-base, system-ui, sans-serif); 319 + } 320 + 321 + :host .inline-provider-title { 322 + flex: 1; 323 + font-size: 0.85em; 324 + font-weight: 600; 325 + color: var(--color-text, #333); 326 + } 327 + 328 + :host .inline-provider-close { 329 + background: none; 330 + border: none; 331 + font-size: 1.2em; 332 + cursor: pointer; 333 + padding: 0.2em 0.4em; 334 + color: var(--color-text, #666); 335 + border-radius: 4px; 336 + } 337 + 338 + :host .inline-provider-close:hover { 339 + background: var(--bg-hover, #e0e0e0); 340 + } 341 + 342 + :host .inline-provider-dialog iframe { 343 + flex: 1; 344 + border: none; 345 + width: 100%; 346 + }
+40
ui/system/web_view.js
··· 43 43 44 44 // Load status for progress indicator 45 45 this.loadStatus = "idle"; 46 + 47 + // Inline task provider state 48 + this.inlineProvider = null; 46 49 } 47 50 48 51 handleMenuAction(e) { ··· 136 139 selectControl: { state: true }, 137 140 colorPicker: { state: true }, 138 141 loadStatus: { state: true }, 142 + inlineProvider: { state: true }, 139 143 }; 140 144 141 145 ensureIframe() { ··· 666 670 } 667 671 } 668 672 673 + // Show an inline task provider overlay within this webview. 674 + showInlineProvider(url, title) { 675 + this.inlineProvider = { url, title }; 676 + } 677 + 678 + // Close the inline task provider overlay. 679 + closeInlineProvider() { 680 + this.inlineProvider = null; 681 + } 682 + 669 683 // Dismiss the pending context menu without showing it 670 684 dismissPendingContextMenu() { 671 685 if (this.pendingContextMenu) { ··· 838 852 </color-picker>`; 839 853 } 840 854 855 + renderInlineProvider() { 856 + if (!this.inlineProvider) { 857 + return ""; 858 + } 859 + 860 + return html` 861 + <div class="inline-provider-overlay" @click=${() => this.closeInlineProvider()}> 862 + <div class="inline-provider-dialog" @click=${(e) => e.stopPropagation()}> 863 + <div class="inline-provider-header"> 864 + <span class="inline-provider-title">${this.inlineProvider.title}</span> 865 + <button class="inline-provider-close" @click=${() => this.closeInlineProvider()}> 866 + &times; 867 + </button> 868 + </div> 869 + <iframe 870 + embed 871 + src="${this.inlineProvider.url}" 872 + @embedclosed=${() => this.closeInlineProvider()} 873 + ></iframe> 874 + </div> 875 + </div> 876 + `; 877 + } 878 + 841 879 renderPermissionPrompt() { 842 880 if (!this.currentPermission) { 843 881 return ""; ··· 1173 1211 @embednotificationshow=${this.onnotificationshow} 1174 1212 @embedloadstatuschange=${this.onloadstatuschange} 1175 1213 @embedmediasessionevent=${this.onmediasessionevent} 1214 + @embedclosed=${this.close} 1176 1215 ></iframe> 1177 1216 ${this.renderDialog()} ${this.renderPermissionPrompt()} 1178 1217 <select-control ··· 1188 1227 ></select-control> 1189 1228 1190 1229 ${this.renderColorPicker()} 1230 + ${this.renderInlineProvider()} 1191 1231 </div> 1192 1232 </div> 1193 1233 `;