manager.rs 47 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541
  1. // Copyright 2019-2022 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use std::{
  5. borrow::Cow,
  6. collections::{HashMap, HashSet},
  7. fmt,
  8. fs::create_dir_all,
  9. sync::{Arc, Mutex, MutexGuard},
  10. };
  11. use serde::Serialize;
  12. use serde_json::Value as JsonValue;
  13. use serialize_to_javascript::{default_template, DefaultTemplate, Template};
  14. use url::Url;
  15. use tauri_macros::default_runtime;
  16. use tauri_utils::debug_eprintln;
  17. #[cfg(feature = "isolation")]
  18. use tauri_utils::pattern::isolation::RawIsolationPayload;
  19. use tauri_utils::{
  20. assets::{AssetKey, CspHash},
  21. config::{Csp, CspDirectiveSources},
  22. html::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
  23. };
  24. #[cfg(feature = "isolation")]
  25. use crate::hooks::IsolationJavascript;
  26. use crate::pattern::PatternJavascript;
  27. use crate::{
  28. app::{AppHandle, GlobalWindowEvent, GlobalWindowEventListener},
  29. event::{assert_event_name_is_valid, Event, EventHandler, Listeners},
  30. hooks::{InvokeHandler, InvokePayload, InvokeResponder, OnPageLoad, PageLoadPayload},
  31. plugin::PluginStore,
  32. runtime::{
  33. http::{
  34. MimeType, Request as HttpRequest, Response as HttpResponse,
  35. ResponseBuilder as HttpResponseBuilder,
  36. },
  37. webview::{WebviewIpcHandler, WindowBuilder},
  38. window::{dpi::PhysicalSize, DetachedWindow, FileDropEvent, PendingWindow},
  39. },
  40. utils::{
  41. assets::Assets,
  42. config::{AppUrl, Config, WindowUrl},
  43. PackageInfo,
  44. },
  45. Context, EventLoopMessage, Icon, Invoke, Manager, Pattern, Runtime, Scopes, StateManager, Window,
  46. WindowEvent,
  47. };
  48. use crate::{
  49. app::{GlobalMenuEventListener, WindowMenuEvent},
  50. window::WebResourceRequestHandler,
  51. };
  52. use crate::{hooks::IpcJavascript, pattern::format_real_schema};
  53. #[cfg(any(target_os = "linux", target_os = "windows"))]
  54. use crate::api::path::{resolve_path, BaseDirectory};
  55. use crate::{runtime::menu::Menu, MenuEvent};
  56. const WINDOW_RESIZED_EVENT: &str = "tauri://resize";
  57. const WINDOW_MOVED_EVENT: &str = "tauri://move";
  58. const WINDOW_CLOSE_REQUESTED_EVENT: &str = "tauri://close-requested";
  59. const WINDOW_DESTROYED_EVENT: &str = "tauri://destroyed";
  60. const WINDOW_FOCUS_EVENT: &str = "tauri://focus";
  61. const WINDOW_BLUR_EVENT: &str = "tauri://blur";
  62. const WINDOW_SCALE_FACTOR_CHANGED_EVENT: &str = "tauri://scale-change";
  63. const WINDOW_THEME_CHANGED: &str = "tauri://theme-changed";
  64. const WINDOW_FILE_DROP_EVENT: &str = "tauri://file-drop";
  65. const WINDOW_FILE_DROP_HOVER_EVENT: &str = "tauri://file-drop-hover";
  66. const WINDOW_FILE_DROP_CANCELLED_EVENT: &str = "tauri://file-drop-cancelled";
  67. const MENU_EVENT: &str = "tauri://menu";
  68. #[derive(Default)]
  69. /// Spaced and quoted Content-Security-Policy hash values.
  70. struct CspHashStrings {
  71. script: Vec<String>,
  72. style: Vec<String>,
  73. }
  74. /// Sets the CSP value to the asset HTML if needed (on Linux).
  75. /// Returns the CSP string for access on the response header (on Windows and macOS).
  76. fn set_csp<R: Runtime>(
  77. asset: &mut String,
  78. assets: Arc<dyn Assets>,
  79. asset_path: &AssetKey,
  80. manager: &WindowManager<R>,
  81. csp: Csp,
  82. ) -> String {
  83. let mut csp = csp.into();
  84. let hash_strings =
  85. assets
  86. .csp_hashes(asset_path)
  87. .fold(CspHashStrings::default(), |mut acc, hash| {
  88. match hash {
  89. CspHash::Script(hash) => {
  90. acc.script.push(hash.into());
  91. }
  92. CspHash::Style(hash) => {
  93. acc.style.push(hash.into());
  94. }
  95. _csp_hash => {
  96. debug_eprintln!("Unknown CspHash variant encountered: {:?}", _csp_hash);
  97. }
  98. }
  99. acc
  100. });
  101. let dangerous_disable_asset_csp_modification = &manager
  102. .config()
  103. .tauri
  104. .security
  105. .dangerous_disable_asset_csp_modification;
  106. if dangerous_disable_asset_csp_modification.can_modify("script-src") {
  107. replace_csp_nonce(
  108. asset,
  109. SCRIPT_NONCE_TOKEN,
  110. &mut csp,
  111. "script-src",
  112. hash_strings.script,
  113. );
  114. }
  115. if dangerous_disable_asset_csp_modification.can_modify("style-src") {
  116. replace_csp_nonce(
  117. asset,
  118. STYLE_NONCE_TOKEN,
  119. &mut csp,
  120. "style-src",
  121. hash_strings.style,
  122. );
  123. }
  124. #[cfg(feature = "isolation")]
  125. if let Pattern::Isolation { schema, .. } = &manager.inner.pattern {
  126. let default_src = csp
  127. .entry("default-src".into())
  128. .or_insert_with(Default::default);
  129. default_src.push(crate::pattern::format_real_schema(schema));
  130. }
  131. Csp::DirectiveMap(csp).to_string()
  132. }
  133. #[cfg(target_os = "linux")]
  134. fn set_html_csp(html: &str, csp: &str) -> String {
  135. html.replacen(tauri_utils::html::CSP_TOKEN, csp, 1)
  136. }
  137. // inspired by https://github.com/rust-lang/rust/blob/1be5c8f90912c446ecbdc405cbc4a89f9acd20fd/library/alloc/src/str.rs#L260-L297
  138. fn replace_with_callback<F: FnMut() -> String>(
  139. original: &str,
  140. pattern: &str,
  141. mut replacement: F,
  142. ) -> String {
  143. let mut result = String::new();
  144. let mut last_end = 0;
  145. for (start, part) in original.match_indices(pattern) {
  146. result.push_str(unsafe { original.get_unchecked(last_end..start) });
  147. result.push_str(&replacement());
  148. last_end = start + part.len();
  149. }
  150. result.push_str(unsafe { original.get_unchecked(last_end..original.len()) });
  151. result
  152. }
  153. fn replace_csp_nonce(
  154. asset: &mut String,
  155. token: &str,
  156. csp: &mut HashMap<String, CspDirectiveSources>,
  157. directive: &str,
  158. hashes: Vec<String>,
  159. ) {
  160. let mut nonces = Vec::new();
  161. *asset = replace_with_callback(asset, token, || {
  162. let nonce = rand::random::<usize>();
  163. nonces.push(nonce);
  164. nonce.to_string()
  165. });
  166. if !(nonces.is_empty() && hashes.is_empty()) {
  167. let nonce_sources = nonces
  168. .into_iter()
  169. .map(|n| format!("'nonce-{}'", n))
  170. .collect::<Vec<String>>();
  171. let sources = csp.entry(directive.into()).or_insert_with(Default::default);
  172. let self_source = "'self'".to_string();
  173. if !sources.contains(&self_source) {
  174. sources.push(self_source);
  175. }
  176. sources.extend(nonce_sources);
  177. sources.extend(hashes);
  178. }
  179. }
  180. #[default_runtime(crate::Wry, wry)]
  181. pub struct InnerWindowManager<R: Runtime> {
  182. windows: Mutex<HashMap<String, Window<R>>>,
  183. #[cfg(all(desktop, feature = "system-tray"))]
  184. pub(crate) trays: Mutex<HashMap<String, crate::SystemTrayHandle<R>>>,
  185. pub(crate) plugins: Mutex<PluginStore<R>>,
  186. listeners: Listeners,
  187. pub(crate) state: Arc<StateManager>,
  188. /// The JS message handler.
  189. invoke_handler: Box<InvokeHandler<R>>,
  190. /// The page load hook, invoked when the webview performs a navigation.
  191. on_page_load: Box<OnPageLoad<R>>,
  192. config: Arc<Config>,
  193. assets: Arc<dyn Assets>,
  194. pub(crate) default_window_icon: Option<Icon>,
  195. pub(crate) app_icon: Option<Vec<u8>>,
  196. pub(crate) tray_icon: Option<Icon>,
  197. package_info: PackageInfo,
  198. /// The webview protocols available to all windows.
  199. uri_scheme_protocols: HashMap<String, Arc<CustomProtocol<R>>>,
  200. /// The menu set to all windows.
  201. menu: Option<Menu>,
  202. /// Menu event listeners to all windows.
  203. menu_event_listeners: Arc<Vec<GlobalMenuEventListener<R>>>,
  204. /// Window event listeners to all windows.
  205. window_event_listeners: Arc<Vec<GlobalWindowEventListener<R>>>,
  206. /// Responder for invoke calls.
  207. invoke_responder: Arc<InvokeResponder<R>>,
  208. /// The script that initializes the invoke system.
  209. invoke_initialization_script: String,
  210. /// Application pattern.
  211. pub(crate) pattern: Pattern,
  212. }
  213. impl<R: Runtime> fmt::Debug for InnerWindowManager<R> {
  214. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  215. f.debug_struct("InnerWindowManager")
  216. .field("plugins", &self.plugins)
  217. .field("state", &self.state)
  218. .field("config", &self.config)
  219. .field("default_window_icon", &self.default_window_icon)
  220. .field("app_icon", &self.app_icon)
  221. .field("tray_icon", &self.tray_icon)
  222. .field("package_info", &self.package_info)
  223. .field("menu", &self.menu)
  224. .field("pattern", &self.pattern)
  225. .finish()
  226. }
  227. }
  228. /// A resolved asset.
  229. pub struct Asset {
  230. /// The asset bytes.
  231. pub bytes: Vec<u8>,
  232. /// The asset's mime type.
  233. pub mime_type: String,
  234. /// The `Content-Security-Policy` header value.
  235. pub csp_header: Option<String>,
  236. }
  237. /// Uses a custom URI scheme handler to resolve file requests
  238. pub struct CustomProtocol<R: Runtime> {
  239. /// Handler for protocol
  240. #[allow(clippy::type_complexity)]
  241. pub protocol: Box<
  242. dyn Fn(&AppHandle<R>, &HttpRequest) -> Result<HttpResponse, Box<dyn std::error::Error>>
  243. + Send
  244. + Sync,
  245. >,
  246. }
  247. #[default_runtime(crate::Wry, wry)]
  248. #[derive(Debug)]
  249. pub struct WindowManager<R: Runtime> {
  250. pub inner: Arc<InnerWindowManager<R>>,
  251. }
  252. impl<R: Runtime> Clone for WindowManager<R> {
  253. fn clone(&self) -> Self {
  254. Self {
  255. inner: self.inner.clone(),
  256. }
  257. }
  258. }
  259. impl<R: Runtime> WindowManager<R> {
  260. #[allow(clippy::too_many_arguments)]
  261. pub(crate) fn with_handlers(
  262. #[allow(unused_mut)] mut context: Context<impl Assets>,
  263. plugins: PluginStore<R>,
  264. invoke_handler: Box<InvokeHandler<R>>,
  265. on_page_load: Box<OnPageLoad<R>>,
  266. uri_scheme_protocols: HashMap<String, Arc<CustomProtocol<R>>>,
  267. state: StateManager,
  268. window_event_listeners: Vec<GlobalWindowEventListener<R>>,
  269. (menu, menu_event_listeners): (Option<Menu>, Vec<GlobalMenuEventListener<R>>),
  270. (invoke_responder, invoke_initialization_script): (Arc<InvokeResponder<R>>, String),
  271. ) -> Self {
  272. // generate a random isolation key at runtime
  273. #[cfg(feature = "isolation")]
  274. if let Pattern::Isolation { ref mut key, .. } = &mut context.pattern {
  275. *key = uuid::Uuid::new_v4().to_string();
  276. }
  277. Self {
  278. inner: Arc::new(InnerWindowManager {
  279. windows: Mutex::default(),
  280. #[cfg(all(desktop, feature = "system-tray"))]
  281. trays: Default::default(),
  282. plugins: Mutex::new(plugins),
  283. listeners: Listeners::default(),
  284. state: Arc::new(state),
  285. invoke_handler,
  286. on_page_load,
  287. config: Arc::new(context.config),
  288. assets: context.assets,
  289. default_window_icon: context.default_window_icon,
  290. app_icon: context.app_icon,
  291. tray_icon: context.system_tray_icon,
  292. package_info: context.package_info,
  293. pattern: context.pattern,
  294. uri_scheme_protocols,
  295. menu,
  296. menu_event_listeners: Arc::new(menu_event_listeners),
  297. window_event_listeners: Arc::new(window_event_listeners),
  298. invoke_responder,
  299. invoke_initialization_script,
  300. }),
  301. }
  302. }
  303. pub(crate) fn pattern(&self) -> &Pattern {
  304. &self.inner.pattern
  305. }
  306. /// Get a locked handle to the windows.
  307. pub(crate) fn windows_lock(&self) -> MutexGuard<'_, HashMap<String, Window<R>>> {
  308. self.inner.windows.lock().expect("poisoned window manager")
  309. }
  310. /// State managed by the application.
  311. pub(crate) fn state(&self) -> Arc<StateManager> {
  312. self.inner.state.clone()
  313. }
  314. /// The invoke responder.
  315. pub(crate) fn invoke_responder(&self) -> Arc<InvokeResponder<R>> {
  316. self.inner.invoke_responder.clone()
  317. }
  318. /// Get the base path to serve data from.
  319. ///
  320. /// * In dev mode, this will be based on the `devPath` configuration value.
  321. /// * Otherwise, this will be based on the `distDir` configuration value.
  322. #[cfg(not(dev))]
  323. fn base_path(&self) -> &AppUrl {
  324. &self.inner.config.build.dist_dir
  325. }
  326. #[cfg(dev)]
  327. fn base_path(&self) -> &AppUrl {
  328. &self.inner.config.build.dev_path
  329. }
  330. /// Get the base URL to use for webview requests.
  331. ///
  332. /// In dev mode, this will be based on the `devPath` configuration value.
  333. pub(crate) fn get_url(&self) -> Cow<'_, Url> {
  334. match self.base_path() {
  335. AppUrl::Url(WindowUrl::External(url)) => Cow::Borrowed(url),
  336. #[cfg(windows)]
  337. _ => Cow::Owned(Url::parse("https://tauri.localhost").unwrap()),
  338. #[cfg(not(windows))]
  339. _ => Cow::Owned(Url::parse("tauri://localhost").unwrap()),
  340. }
  341. }
  342. /// Get the origin as it will be seen in the webview.
  343. fn get_browser_origin(&self) -> String {
  344. match self.base_path() {
  345. AppUrl::Url(WindowUrl::External(url)) => url.origin().ascii_serialization(),
  346. _ => format_real_schema("tauri"),
  347. }
  348. }
  349. fn csp(&self) -> Option<Csp> {
  350. if cfg!(feature = "custom-protocol") {
  351. self.inner.config.tauri.security.csp.clone()
  352. } else {
  353. self
  354. .inner
  355. .config
  356. .tauri
  357. .security
  358. .dev_csp
  359. .clone()
  360. .or_else(|| self.inner.config.tauri.security.csp.clone())
  361. }
  362. }
  363. fn prepare_pending_window(
  364. &self,
  365. mut pending: PendingWindow<EventLoopMessage, R>,
  366. label: &str,
  367. window_labels: &[String],
  368. app_handle: AppHandle<R>,
  369. web_resource_request_handler: Option<Box<WebResourceRequestHandler>>,
  370. ) -> crate::Result<PendingWindow<EventLoopMessage, R>> {
  371. let is_init_global = self.inner.config.build.with_global_tauri;
  372. let plugin_init = self
  373. .inner
  374. .plugins
  375. .lock()
  376. .expect("poisoned plugin store")
  377. .initialization_script();
  378. let pattern_init = PatternJavascript {
  379. pattern: self.pattern().into(),
  380. }
  381. .render_default(&Default::default())?;
  382. let ipc_init = IpcJavascript {
  383. isolation_origin: &match self.pattern() {
  384. #[cfg(feature = "isolation")]
  385. Pattern::Isolation { schema, .. } => crate::pattern::format_real_schema(schema),
  386. _ => "".to_string(),
  387. },
  388. }
  389. .render_default(&Default::default())?;
  390. let mut webview_attributes = pending.webview_attributes;
  391. let mut window_labels = window_labels.to_vec();
  392. let l = label.to_string();
  393. if !window_labels.contains(&l) {
  394. window_labels.push(l);
  395. }
  396. webview_attributes = webview_attributes
  397. .initialization_script(&self.inner.invoke_initialization_script)
  398. .initialization_script(&format!(
  399. r#"
  400. Object.defineProperty(window, '__TAURI_METADATA__', {{
  401. value: {{
  402. __windows: {window_labels_array}.map(function (label) {{ return {{ label: label }} }}),
  403. __currentWindow: {{ label: {current_window_label} }}
  404. }}
  405. }})
  406. "#,
  407. window_labels_array = serde_json::to_string(&window_labels)?,
  408. current_window_label = serde_json::to_string(&label)?,
  409. ))
  410. .initialization_script(&self.initialization_script(&ipc_init.into_string(),&pattern_init.into_string(),&plugin_init, is_init_global)?)
  411. ;
  412. #[cfg(feature = "isolation")]
  413. if let Pattern::Isolation { schema, .. } = self.pattern() {
  414. webview_attributes = webview_attributes.initialization_script(
  415. &IsolationJavascript {
  416. origin: self.get_browser_origin(),
  417. isolation_src: &crate::pattern::format_real_schema(schema),
  418. style: tauri_utils::pattern::isolation::IFRAME_STYLE,
  419. }
  420. .render_default(&Default::default())?
  421. .into_string(),
  422. );
  423. }
  424. pending.webview_attributes = webview_attributes;
  425. let mut registered_scheme_protocols = Vec::new();
  426. for (uri_scheme, protocol) in &self.inner.uri_scheme_protocols {
  427. registered_scheme_protocols.push(uri_scheme.clone());
  428. let protocol = protocol.clone();
  429. let app_handle = Mutex::new(app_handle.clone());
  430. pending.register_uri_scheme_protocol(uri_scheme.clone(), move |p| {
  431. (protocol.protocol)(&app_handle.lock().unwrap(), p)
  432. });
  433. }
  434. let window_url = pending.current_url.lock().unwrap().clone();
  435. let window_origin =
  436. if cfg!(windows) && window_url.scheme() != "http" && window_url.scheme() != "https" {
  437. format!("https://{}.localhost", window_url.scheme())
  438. } else {
  439. format!(
  440. "{}://{}{}",
  441. window_url.scheme(),
  442. window_url.host().unwrap(),
  443. if let Some(port) = window_url.port() {
  444. format!(":{}", port)
  445. } else {
  446. "".into()
  447. }
  448. )
  449. };
  450. if !registered_scheme_protocols.contains(&"tauri".into()) {
  451. pending.register_uri_scheme_protocol(
  452. "tauri",
  453. self.prepare_uri_scheme_protocol(&window_origin, web_resource_request_handler),
  454. );
  455. registered_scheme_protocols.push("tauri".into());
  456. }
  457. #[cfg(protocol_asset)]
  458. if !registered_scheme_protocols.contains(&"asset".into()) {
  459. use crate::api::file::SafePathBuf;
  460. use tokio::io::{AsyncReadExt, AsyncSeekExt};
  461. use url::Position;
  462. let asset_scope = self.state().get::<crate::Scopes>().asset_protocol.clone();
  463. pending.register_uri_scheme_protocol("asset", move |request| {
  464. let parsed_path = Url::parse(request.uri())?;
  465. let filtered_path = &parsed_path[..Position::AfterPath];
  466. let path = filtered_path
  467. .strip_prefix("asset://localhost/")
  468. // the `strip_prefix` only returns None when a request is made to `https://tauri.$P` on Windows
  469. // where `$P` is not `localhost/*`
  470. .unwrap_or("");
  471. let path = percent_encoding::percent_decode(path.as_bytes())
  472. .decode_utf8_lossy()
  473. .to_string();
  474. if let Err(e) = SafePathBuf::new(path.clone().into()) {
  475. debug_eprintln!("asset protocol path \"{}\" is not valid: {}", path, e);
  476. return HttpResponseBuilder::new().status(403).body(Vec::new());
  477. }
  478. if !asset_scope.is_allowed(&path) {
  479. debug_eprintln!("asset protocol not configured to allow the path: {}", path);
  480. return HttpResponseBuilder::new().status(403).body(Vec::new());
  481. }
  482. let path_ = path.clone();
  483. let mut response =
  484. HttpResponseBuilder::new().header("Access-Control-Allow-Origin", &window_origin);
  485. // handle 206 (partial range) http request
  486. if let Some(range) = request
  487. .headers()
  488. .get("range")
  489. .and_then(|r| r.to_str().map(|r| r.to_string()).ok())
  490. {
  491. #[derive(Default)]
  492. struct RangeMetadata {
  493. file: Option<tokio::fs::File>,
  494. range: Option<crate::runtime::http::HttpRange>,
  495. metadata: Option<std::fs::Metadata>,
  496. headers: HashMap<&'static str, String>,
  497. status_code: u16,
  498. body: Vec<u8>,
  499. }
  500. let mut range_metadata = crate::async_runtime::safe_block_on(async move {
  501. let mut data = RangeMetadata::default();
  502. // open the file
  503. let mut file = match tokio::fs::File::open(path_.clone()).await {
  504. Ok(file) => file,
  505. Err(e) => {
  506. debug_eprintln!("Failed to open asset: {}", e);
  507. data.status_code = 404;
  508. return data;
  509. }
  510. };
  511. // Get the file size
  512. let file_size = match file.metadata().await {
  513. Ok(metadata) => {
  514. let len = metadata.len();
  515. data.metadata.replace(metadata);
  516. len
  517. }
  518. Err(e) => {
  519. debug_eprintln!("Failed to read asset metadata: {}", e);
  520. data.file.replace(file);
  521. data.status_code = 404;
  522. return data;
  523. }
  524. };
  525. // parse the range
  526. let range = match crate::runtime::http::HttpRange::parse(
  527. &if range.ends_with("-*") {
  528. range.chars().take(range.len() - 1).collect::<String>()
  529. } else {
  530. range.clone()
  531. },
  532. file_size,
  533. ) {
  534. Ok(r) => r,
  535. Err(e) => {
  536. debug_eprintln!("Failed to parse range {}: {:?}", range, e);
  537. data.file.replace(file);
  538. data.status_code = 400;
  539. return data;
  540. }
  541. };
  542. // FIXME: Support multiple ranges
  543. // let support only 1 range for now
  544. if let Some(range) = range.first() {
  545. data.range.replace(*range);
  546. let mut real_length = range.length;
  547. // prevent max_length;
  548. // specially on webview2
  549. if range.length > file_size / 3 {
  550. // max size sent (400ko / request)
  551. // as it's local file system we can afford to read more often
  552. real_length = std::cmp::min(file_size - range.start, 1024 * 400);
  553. }
  554. // last byte we are reading, the length of the range include the last byte
  555. // who should be skipped on the header
  556. let last_byte = range.start + real_length - 1;
  557. data.headers.insert("Connection", "Keep-Alive".into());
  558. data.headers.insert("Accept-Ranges", "bytes".into());
  559. data
  560. .headers
  561. .insert("Content-Length", real_length.to_string());
  562. data.headers.insert(
  563. "Content-Range",
  564. format!("bytes {}-{}/{}", range.start, last_byte, file_size),
  565. );
  566. if let Err(e) = file.seek(std::io::SeekFrom::Start(range.start)).await {
  567. debug_eprintln!("Failed to seek file to {}: {}", range.start, e);
  568. data.file.replace(file);
  569. data.status_code = 422;
  570. return data;
  571. }
  572. let mut f = file.take(real_length);
  573. let r = f.read_to_end(&mut data.body).await;
  574. file = f.into_inner();
  575. data.file.replace(file);
  576. if let Err(e) = r {
  577. debug_eprintln!("Failed read file: {}", e);
  578. data.status_code = 422;
  579. return data;
  580. }
  581. // partial content
  582. data.status_code = 206;
  583. } else {
  584. data.status_code = 200;
  585. }
  586. data
  587. });
  588. for (k, v) in range_metadata.headers {
  589. response = response.header(k, v);
  590. }
  591. let mime_type = if let (Some(mut file), Some(metadata), Some(range)) = (
  592. range_metadata.file,
  593. range_metadata.metadata,
  594. range_metadata.range,
  595. ) {
  596. // if we're already reading the beginning of the file, we do not need to re-read it
  597. if range.start == 0 {
  598. MimeType::parse(&range_metadata.body, &path)
  599. } else {
  600. let (status, bytes) = crate::async_runtime::safe_block_on(async move {
  601. let mut status = None;
  602. if let Err(e) = file.rewind().await {
  603. debug_eprintln!("Failed to rewind file: {}", e);
  604. status.replace(422);
  605. (status, Vec::with_capacity(0))
  606. } else {
  607. // taken from https://docs.rs/infer/0.9.0/src/infer/lib.rs.html#240-251
  608. let limit = std::cmp::min(metadata.len(), 8192) as usize + 1;
  609. let mut bytes = Vec::with_capacity(limit);
  610. if let Err(e) = file.take(8192).read_to_end(&mut bytes).await {
  611. debug_eprintln!("Failed read file: {}", e);
  612. status.replace(422);
  613. }
  614. (status, bytes)
  615. }
  616. });
  617. if let Some(s) = status {
  618. range_metadata.status_code = s;
  619. }
  620. MimeType::parse(&bytes, &path)
  621. }
  622. } else {
  623. MimeType::parse(&range_metadata.body, &path)
  624. };
  625. response
  626. .mimetype(&mime_type)
  627. .status(range_metadata.status_code)
  628. .body(range_metadata.body)
  629. } else {
  630. match crate::async_runtime::safe_block_on(async move { tokio::fs::read(path_).await }) {
  631. Ok(data) => {
  632. let mime_type = MimeType::parse(&data, &path);
  633. response.mimetype(&mime_type).body(data)
  634. }
  635. Err(e) => {
  636. debug_eprintln!("Failed to read file: {}", e);
  637. response.status(404).body(Vec::new())
  638. }
  639. }
  640. }
  641. });
  642. }
  643. #[cfg(feature = "isolation")]
  644. if let Pattern::Isolation {
  645. assets,
  646. schema,
  647. key: _,
  648. crypto_keys,
  649. } = &self.inner.pattern
  650. {
  651. let assets = assets.clone();
  652. let schema_ = schema.clone();
  653. let url_base = format!("{}://localhost", schema_);
  654. let aes_gcm_key = *crypto_keys.aes_gcm().raw();
  655. pending.register_uri_scheme_protocol(schema, move |request| {
  656. match request_to_path(request, &url_base).as_str() {
  657. "index.html" => match assets.get(&"index.html".into()) {
  658. Some(asset) => {
  659. let asset = String::from_utf8_lossy(asset.as_ref());
  660. let template = tauri_utils::pattern::isolation::IsolationJavascriptRuntime {
  661. runtime_aes_gcm_key: &aes_gcm_key,
  662. };
  663. match template.render(asset.as_ref(), &Default::default()) {
  664. Ok(asset) => HttpResponseBuilder::new()
  665. .mimetype("text/html")
  666. .body(asset.into_string().as_bytes().to_vec()),
  667. Err(_) => HttpResponseBuilder::new()
  668. .status(500)
  669. .mimetype("text/plain")
  670. .body(Vec::new()),
  671. }
  672. }
  673. None => HttpResponseBuilder::new()
  674. .status(404)
  675. .mimetype("text/plain")
  676. .body(Vec::new()),
  677. },
  678. _ => HttpResponseBuilder::new()
  679. .status(404)
  680. .mimetype("text/plain")
  681. .body(Vec::new()),
  682. }
  683. });
  684. }
  685. Ok(pending)
  686. }
  687. fn prepare_ipc_handler(
  688. &self,
  689. app_handle: AppHandle<R>,
  690. ) -> WebviewIpcHandler<EventLoopMessage, R> {
  691. let manager = self.clone();
  692. Box::new(move |window, #[allow(unused_mut)] mut request| {
  693. let window = Window::new(manager.clone(), window, app_handle.clone());
  694. #[cfg(feature = "isolation")]
  695. if let Pattern::Isolation { crypto_keys, .. } = manager.pattern() {
  696. match RawIsolationPayload::try_from(request.as_str())
  697. .and_then(|raw| crypto_keys.decrypt(raw))
  698. {
  699. Ok(json) => request = json,
  700. Err(e) => {
  701. let error: crate::Error = e.into();
  702. let _ = window.eval(&format!(
  703. r#"console.error({})"#,
  704. JsonValue::String(error.to_string())
  705. ));
  706. return;
  707. }
  708. }
  709. }
  710. match serde_json::from_str::<InvokePayload>(&request) {
  711. Ok(message) => {
  712. let _ = window.on_message(message);
  713. }
  714. Err(e) => {
  715. let error: crate::Error = e.into();
  716. let _ = window.eval(&format!(
  717. r#"console.error({})"#,
  718. JsonValue::String(error.to_string())
  719. ));
  720. }
  721. }
  722. })
  723. }
  724. pub fn get_asset(&self, mut path: String) -> Result<Asset, Box<dyn std::error::Error>> {
  725. let assets = &self.inner.assets;
  726. if path.ends_with('/') {
  727. path.pop();
  728. }
  729. path = percent_encoding::percent_decode(path.as_bytes())
  730. .decode_utf8_lossy()
  731. .to_string();
  732. let path = if path.is_empty() {
  733. // if the url is `tauri://localhost`, we should load `index.html`
  734. "index.html".to_string()
  735. } else {
  736. // skip leading `/`
  737. path.chars().skip(1).collect::<String>()
  738. };
  739. let mut asset_path = AssetKey::from(path.as_str());
  740. let asset_response = assets
  741. .get(&path.as_str().into())
  742. .or_else(|| {
  743. eprintln!("Asset `{}` not found; fallback to {}.html", path, path);
  744. let fallback = format!("{}.html", path.as_str()).into();
  745. let asset = assets.get(&fallback);
  746. asset_path = fallback;
  747. asset
  748. })
  749. .or_else(|| {
  750. debug_eprintln!(
  751. "Asset `{}` not found; fallback to {}/index.html",
  752. path,
  753. path
  754. );
  755. let fallback = format!("{}/index.html", path.as_str()).into();
  756. let asset = assets.get(&fallback);
  757. asset_path = fallback;
  758. asset
  759. })
  760. .or_else(|| {
  761. debug_eprintln!("Asset `{}` not found; fallback to index.html", path);
  762. let fallback = AssetKey::from("index.html");
  763. let asset = assets.get(&fallback);
  764. asset_path = fallback;
  765. asset
  766. })
  767. .ok_or_else(|| crate::Error::AssetNotFound(path.clone()))
  768. .map(Cow::into_owned);
  769. let mut csp_header = None;
  770. let is_html = asset_path.as_ref().ends_with(".html");
  771. match asset_response {
  772. Ok(asset) => {
  773. let final_data = if is_html {
  774. let mut asset = String::from_utf8_lossy(&asset).into_owned();
  775. if let Some(csp) = self.csp() {
  776. csp_header.replace(set_csp(
  777. &mut asset,
  778. self.inner.assets.clone(),
  779. &asset_path,
  780. self,
  781. csp,
  782. ));
  783. }
  784. asset.as_bytes().to_vec()
  785. } else {
  786. asset
  787. };
  788. let mime_type = MimeType::parse(&final_data, &path);
  789. Ok(Asset {
  790. bytes: final_data.to_vec(),
  791. mime_type,
  792. csp_header,
  793. })
  794. }
  795. Err(e) => {
  796. debug_eprintln!("{:?}", e); // TODO log::error!
  797. Err(Box::new(e))
  798. }
  799. }
  800. }
  801. #[allow(clippy::type_complexity)]
  802. fn prepare_uri_scheme_protocol(
  803. &self,
  804. window_origin: &str,
  805. web_resource_request_handler: Option<
  806. Box<dyn Fn(&HttpRequest, &mut HttpResponse) + Send + Sync>,
  807. >,
  808. ) -> Box<dyn Fn(&HttpRequest) -> Result<HttpResponse, Box<dyn std::error::Error>> + Send + Sync>
  809. {
  810. let manager = self.clone();
  811. let window_origin = window_origin.to_string();
  812. Box::new(move |request| {
  813. let path = request
  814. .uri()
  815. .split(&['?', '#'][..])
  816. // ignore query string and fragment
  817. .next()
  818. .unwrap()
  819. .strip_prefix("tauri://localhost")
  820. .map(|p| p.to_string())
  821. // the `strip_prefix` only returns None when a request is made to `https://tauri.$P` on Windows
  822. // where `$P` is not `localhost/*`
  823. .unwrap_or_else(|| "".to_string());
  824. let asset = manager.get_asset(path)?;
  825. let mut builder = HttpResponseBuilder::new()
  826. .header("Access-Control-Allow-Origin", &window_origin)
  827. .mimetype(&asset.mime_type);
  828. if let Some(csp) = &asset.csp_header {
  829. builder = builder.header("Content-Security-Policy", csp);
  830. }
  831. let mut response = builder.body(asset.bytes)?;
  832. if let Some(handler) = &web_resource_request_handler {
  833. handler(request, &mut response);
  834. // if it's an HTML file, we need to set the CSP meta tag on Linux
  835. #[cfg(target_os = "linux")]
  836. if let Some(response_csp) = response.headers().get("Content-Security-Policy") {
  837. let response_csp = String::from_utf8_lossy(response_csp.as_bytes());
  838. let body = set_html_csp(&String::from_utf8_lossy(response.body()), &response_csp);
  839. *response.body_mut() = body.as_bytes().to_vec();
  840. }
  841. } else {
  842. #[cfg(target_os = "linux")]
  843. {
  844. if let Some(csp) = &asset.csp_header {
  845. let body = set_html_csp(&String::from_utf8_lossy(response.body()), csp);
  846. *response.body_mut() = body.as_bytes().to_vec();
  847. }
  848. }
  849. }
  850. Ok(response)
  851. })
  852. }
  853. fn initialization_script(
  854. &self,
  855. ipc_script: &str,
  856. pattern_script: &str,
  857. plugin_initialization_script: &str,
  858. with_global_tauri: bool,
  859. ) -> crate::Result<String> {
  860. #[derive(Template)]
  861. #[default_template("../scripts/init.js")]
  862. struct InitJavascript<'a> {
  863. origin: String,
  864. #[raw]
  865. pattern_script: &'a str,
  866. #[raw]
  867. ipc_script: &'a str,
  868. #[raw]
  869. bundle_script: &'a str,
  870. // A function to immediately listen to an event.
  871. #[raw]
  872. listen_function: &'a str,
  873. #[raw]
  874. core_script: &'a str,
  875. #[raw]
  876. event_initialization_script: &'a str,
  877. #[raw]
  878. plugin_initialization_script: &'a str,
  879. #[raw]
  880. freeze_prototype: &'a str,
  881. #[raw]
  882. hotkeys: &'a str,
  883. }
  884. let bundle_script = if with_global_tauri {
  885. include_str!("../scripts/bundle.global.js")
  886. } else {
  887. ""
  888. };
  889. let freeze_prototype = if self.inner.config.tauri.security.freeze_prototype {
  890. include_str!("../scripts/freeze_prototype.js")
  891. } else {
  892. ""
  893. };
  894. #[cfg(any(debug_assertions, feature = "devtools"))]
  895. let hotkeys = &format!(
  896. "
  897. {};
  898. window.hotkeys('{}', () => {{
  899. window.__TAURI_INVOKE__('tauri', {{
  900. __tauriModule: 'Window',
  901. message: {{
  902. cmd: 'manage',
  903. data: {{
  904. cmd: {{
  905. type: '__toggleDevtools'
  906. }}
  907. }}
  908. }}
  909. }});
  910. }});
  911. ",
  912. include_str!("../scripts/hotkey.js"),
  913. if cfg!(target_os = "macos") {
  914. "command+option+i"
  915. } else {
  916. "ctrl+shift+i"
  917. }
  918. );
  919. #[cfg(not(any(debug_assertions, feature = "devtools")))]
  920. let hotkeys = "";
  921. InitJavascript {
  922. origin: self.get_browser_origin(),
  923. pattern_script,
  924. ipc_script,
  925. bundle_script,
  926. listen_function: &format!(
  927. "function listen(eventName, cb) {{ {} }}",
  928. crate::event::listen_js(
  929. self.event_listeners_object_name(),
  930. "eventName".into(),
  931. 0,
  932. None,
  933. "window['_' + window.__TAURI__.transformCallback(cb) ]".into()
  934. )
  935. ),
  936. core_script: include_str!("../scripts/core.js"),
  937. event_initialization_script: &self.event_initialization_script(),
  938. plugin_initialization_script,
  939. freeze_prototype,
  940. hotkeys,
  941. }
  942. .render_default(&Default::default())
  943. .map(|s| s.into_string())
  944. .map_err(Into::into)
  945. }
  946. fn event_initialization_script(&self) -> String {
  947. format!(
  948. "
  949. Object.defineProperty(window, '{function}', {{
  950. value: function (eventData) {{
  951. const listeners = (window['{listeners}'] && window['{listeners}'][eventData.event]) || []
  952. for (let i = listeners.length - 1; i >= 0; i--) {{
  953. const listener = listeners[i]
  954. if (listener.windowLabel === null || listener.windowLabel === eventData.windowLabel) {{
  955. eventData.id = listener.id
  956. listener.handler(eventData)
  957. }}
  958. }}
  959. }}
  960. }});
  961. ",
  962. function = self.event_emit_function_name(),
  963. listeners = self.event_listeners_object_name()
  964. )
  965. }
  966. }
  967. #[cfg(test)]
  968. mod test {
  969. use crate::{generate_context, plugin::PluginStore, StateManager, Wry};
  970. use super::WindowManager;
  971. #[test]
  972. fn check_get_url() {
  973. let context = generate_context!("test/fixture/src-tauri/tauri.conf.json", crate);
  974. let manager: WindowManager<Wry> = WindowManager::with_handlers(
  975. context,
  976. PluginStore::default(),
  977. Box::new(|_| ()),
  978. Box::new(|_, _| ()),
  979. Default::default(),
  980. StateManager::new(),
  981. Default::default(),
  982. Default::default(),
  983. (std::sync::Arc::new(|_, _, _, _| ()), "".into()),
  984. );
  985. #[cfg(custom_protocol)]
  986. {
  987. assert_eq!(
  988. manager.get_url().to_string(),
  989. if cfg!(windows) {
  990. "https://tauri.localhost/"
  991. } else {
  992. "tauri://localhost"
  993. }
  994. );
  995. }
  996. #[cfg(dev)]
  997. assert_eq!(manager.get_url().to_string(), "http://localhost:4000/");
  998. }
  999. }
  1000. impl<R: Runtime> WindowManager<R> {
  1001. pub fn run_invoke_handler(&self, invoke: Invoke<R>) {
  1002. (self.inner.invoke_handler)(invoke);
  1003. }
  1004. pub fn run_on_page_load(&self, window: Window<R>, payload: PageLoadPayload) {
  1005. (self.inner.on_page_load)(window.clone(), payload.clone());
  1006. self
  1007. .inner
  1008. .plugins
  1009. .lock()
  1010. .expect("poisoned plugin store")
  1011. .on_page_load(window, payload);
  1012. }
  1013. pub fn extend_api(&self, invoke: Invoke<R>) {
  1014. self
  1015. .inner
  1016. .plugins
  1017. .lock()
  1018. .expect("poisoned plugin store")
  1019. .extend_api(invoke);
  1020. }
  1021. pub fn initialize_plugins(&self, app: &AppHandle<R>) -> crate::Result<()> {
  1022. self
  1023. .inner
  1024. .plugins
  1025. .lock()
  1026. .expect("poisoned plugin store")
  1027. .initialize(app, &self.inner.config.plugins)
  1028. }
  1029. pub fn prepare_window(
  1030. &self,
  1031. app_handle: AppHandle<R>,
  1032. mut pending: PendingWindow<EventLoopMessage, R>,
  1033. window_labels: &[String],
  1034. web_resource_request_handler: Option<Box<WebResourceRequestHandler>>,
  1035. ) -> crate::Result<PendingWindow<EventLoopMessage, R>> {
  1036. if self.windows_lock().contains_key(&pending.label) {
  1037. return Err(crate::Error::WindowLabelAlreadyExists(pending.label));
  1038. }
  1039. #[allow(unused_mut)] // mut url only for the data-url parsing
  1040. let mut url = match &pending.webview_attributes.url {
  1041. WindowUrl::App(path) => {
  1042. let url = self.get_url();
  1043. // ignore "index.html" just to simplify the url
  1044. if path.to_str() != Some("index.html") {
  1045. url
  1046. .join(&*path.to_string_lossy())
  1047. .map_err(crate::Error::InvalidUrl)
  1048. // this will never fail
  1049. .unwrap()
  1050. } else {
  1051. url.into_owned()
  1052. }
  1053. }
  1054. WindowUrl::External(url) => url.clone(),
  1055. _ => unimplemented!(),
  1056. };
  1057. #[cfg(not(feature = "window-data-url"))]
  1058. if url.scheme() == "data" {
  1059. return Err(crate::Error::InvalidWindowUrl(
  1060. "data URLs are not supported without the `window-data-url` feature.",
  1061. ));
  1062. }
  1063. #[cfg(feature = "window-data-url")]
  1064. if let Some(csp) = self.csp() {
  1065. if url.scheme() == "data" {
  1066. if let Ok(data_url) = data_url::DataUrl::process(url.as_str()) {
  1067. let (body, _) = data_url.decode_to_vec().unwrap();
  1068. let html = String::from_utf8_lossy(&body).into_owned();
  1069. // naive way to check if it's an html
  1070. if html.contains('<') && html.contains('>') {
  1071. let mut document = tauri_utils::html::parse(html);
  1072. tauri_utils::html::inject_csp(&mut document, &csp.to_string());
  1073. url.set_path(&format!("text/html,{}", document.to_string()));
  1074. }
  1075. }
  1076. }
  1077. }
  1078. *pending.current_url.lock().unwrap() = url;
  1079. if !pending.window_builder.has_icon() {
  1080. if let Some(default_window_icon) = self.inner.default_window_icon.clone() {
  1081. pending.window_builder = pending
  1082. .window_builder
  1083. .icon(default_window_icon.try_into()?)?;
  1084. }
  1085. }
  1086. if pending.window_builder.get_menu().is_none() {
  1087. if let Some(menu) = &self.inner.menu {
  1088. pending = pending.set_menu(menu.clone());
  1089. }
  1090. }
  1091. let label = pending.label.clone();
  1092. pending = self.prepare_pending_window(
  1093. pending,
  1094. &label,
  1095. window_labels,
  1096. app_handle.clone(),
  1097. web_resource_request_handler,
  1098. )?;
  1099. pending.ipc_handler = Some(self.prepare_ipc_handler(app_handle));
  1100. // in `Windows`, we need to force a data_directory
  1101. // but we do respect user-specification
  1102. #[cfg(any(target_os = "linux", target_os = "windows"))]
  1103. if pending.webview_attributes.data_directory.is_none() {
  1104. let local_app_data = resolve_path(
  1105. &self.inner.config,
  1106. &self.inner.package_info,
  1107. self.inner.state.get::<crate::Env>().inner(),
  1108. &self.inner.config.tauri.bundle.identifier,
  1109. Some(BaseDirectory::LocalData),
  1110. );
  1111. if let Ok(user_data_dir) = local_app_data {
  1112. pending.webview_attributes.data_directory = Some(user_data_dir);
  1113. }
  1114. }
  1115. // make sure the directory is created and available to prevent a panic
  1116. if let Some(user_data_dir) = &pending.webview_attributes.data_directory {
  1117. if !user_data_dir.exists() {
  1118. create_dir_all(user_data_dir)?;
  1119. }
  1120. }
  1121. #[cfg(feature = "isolation")]
  1122. let pattern = self.pattern().clone();
  1123. let current_url_ = pending.current_url.clone();
  1124. let navigation_handler = pending.navigation_handler.take();
  1125. pending.navigation_handler = Some(Box::new(move |url| {
  1126. // always allow navigation events for the isolation iframe and do not emit them for consumers
  1127. #[cfg(feature = "isolation")]
  1128. if let Pattern::Isolation { schema, .. } = &pattern {
  1129. if url.scheme() == schema
  1130. && url.domain() == Some(crate::pattern::ISOLATION_IFRAME_SRC_DOMAIN)
  1131. {
  1132. return true;
  1133. }
  1134. }
  1135. *current_url_.lock().unwrap() = url.clone();
  1136. if let Some(handler) = &navigation_handler {
  1137. handler(url)
  1138. } else {
  1139. true
  1140. }
  1141. }));
  1142. Ok(pending)
  1143. }
  1144. pub fn attach_window(
  1145. &self,
  1146. app_handle: AppHandle<R>,
  1147. window: DetachedWindow<EventLoopMessage, R>,
  1148. ) -> Window<R> {
  1149. let window = Window::new(self.clone(), window, app_handle);
  1150. let window_ = window.clone();
  1151. let window_event_listeners = self.inner.window_event_listeners.clone();
  1152. let manager = self.clone();
  1153. window.on_window_event(move |event| {
  1154. let _ = on_window_event(&window_, &manager, event);
  1155. for handler in window_event_listeners.iter() {
  1156. handler(GlobalWindowEvent {
  1157. window: window_.clone(),
  1158. event: event.clone(),
  1159. });
  1160. }
  1161. });
  1162. {
  1163. let window_ = window.clone();
  1164. let menu_event_listeners = self.inner.menu_event_listeners.clone();
  1165. window.on_menu_event(move |event| {
  1166. let _ = on_menu_event(&window_, &event);
  1167. for handler in menu_event_listeners.iter() {
  1168. handler(WindowMenuEvent {
  1169. window: window_.clone(),
  1170. menu_item_id: event.menu_item_id.clone(),
  1171. });
  1172. }
  1173. });
  1174. }
  1175. // insert the window into our manager
  1176. {
  1177. self
  1178. .windows_lock()
  1179. .insert(window.label().to_string(), window.clone());
  1180. }
  1181. // let plugins know that a new window has been added to the manager
  1182. let manager = self.inner.clone();
  1183. let window_ = window.clone();
  1184. // run on main thread so the plugin store doesn't dead lock with the event loop handler in App
  1185. let _ = window.run_on_main_thread(move || {
  1186. manager
  1187. .plugins
  1188. .lock()
  1189. .expect("poisoned plugin store")
  1190. .created(window_);
  1191. });
  1192. window
  1193. }
  1194. pub(crate) fn on_window_close(&self, label: &str) {
  1195. self.windows_lock().remove(label);
  1196. }
  1197. pub fn emit_filter<S, F>(
  1198. &self,
  1199. event: &str,
  1200. source_window_label: Option<&str>,
  1201. payload: S,
  1202. filter: F,
  1203. ) -> crate::Result<()>
  1204. where
  1205. S: Serialize + Clone,
  1206. F: Fn(&Window<R>) -> bool,
  1207. {
  1208. assert_event_name_is_valid(event);
  1209. self
  1210. .windows_lock()
  1211. .values()
  1212. .filter(|&w| filter(w))
  1213. .try_for_each(|window| window.emit_internal(event, source_window_label, payload.clone()))
  1214. }
  1215. pub fn labels(&self) -> HashSet<String> {
  1216. self.windows_lock().keys().cloned().collect()
  1217. }
  1218. pub fn config(&self) -> Arc<Config> {
  1219. self.inner.config.clone()
  1220. }
  1221. pub fn package_info(&self) -> &PackageInfo {
  1222. &self.inner.package_info
  1223. }
  1224. pub fn unlisten(&self, handler_id: EventHandler) {
  1225. self.inner.listeners.unlisten(handler_id)
  1226. }
  1227. pub fn trigger(&self, event: &str, window: Option<String>, data: Option<String>) {
  1228. assert_event_name_is_valid(event);
  1229. self.inner.listeners.trigger(event, window, data)
  1230. }
  1231. pub fn listen<F: Fn(Event) + Send + 'static>(
  1232. &self,
  1233. event: String,
  1234. window: Option<String>,
  1235. handler: F,
  1236. ) -> EventHandler {
  1237. assert_event_name_is_valid(&event);
  1238. self.inner.listeners.listen(event, window, handler)
  1239. }
  1240. pub fn once<F: FnOnce(Event) + Send + 'static>(
  1241. &self,
  1242. event: String,
  1243. window: Option<String>,
  1244. handler: F,
  1245. ) -> EventHandler {
  1246. assert_event_name_is_valid(&event);
  1247. self.inner.listeners.once(event, window, handler)
  1248. }
  1249. pub fn event_listeners_object_name(&self) -> String {
  1250. self.inner.listeners.listeners_object_name()
  1251. }
  1252. pub fn event_emit_function_name(&self) -> String {
  1253. self.inner.listeners.function_name()
  1254. }
  1255. pub fn get_window(&self, label: &str) -> Option<Window<R>> {
  1256. self.windows_lock().get(label).cloned()
  1257. }
  1258. pub fn windows(&self) -> HashMap<String, Window<R>> {
  1259. self.windows_lock().clone()
  1260. }
  1261. }
  1262. /// Tray APIs
  1263. #[cfg(all(desktop, feature = "system-tray"))]
  1264. impl<R: Runtime> WindowManager<R> {
  1265. pub fn get_tray(&self, id: &str) -> Option<crate::SystemTrayHandle<R>> {
  1266. self.inner.trays.lock().unwrap().get(id).cloned()
  1267. }
  1268. pub fn trays(&self) -> HashMap<String, crate::SystemTrayHandle<R>> {
  1269. self.inner.trays.lock().unwrap().clone()
  1270. }
  1271. pub fn attach_tray(&self, id: String, tray: crate::SystemTrayHandle<R>) {
  1272. self.inner.trays.lock().unwrap().insert(id, tray);
  1273. }
  1274. pub fn get_tray_by_runtime_id(&self, id: u16) -> Option<(String, crate::SystemTrayHandle<R>)> {
  1275. let trays = self.inner.trays.lock().unwrap();
  1276. let iter = trays.iter();
  1277. for (tray_id, tray) in iter {
  1278. if tray.id == id {
  1279. return Some((tray_id.clone(), tray.clone()));
  1280. }
  1281. }
  1282. None
  1283. }
  1284. }
  1285. fn on_window_event<R: Runtime>(
  1286. window: &Window<R>,
  1287. manager: &WindowManager<R>,
  1288. event: &WindowEvent,
  1289. ) -> crate::Result<()> {
  1290. match event {
  1291. WindowEvent::Resized(size) => window.emit(WINDOW_RESIZED_EVENT, size)?,
  1292. WindowEvent::Moved(position) => window.emit(WINDOW_MOVED_EVENT, position)?,
  1293. WindowEvent::CloseRequested { api } => {
  1294. if window.has_js_listener(Some(window.label().into()), WINDOW_CLOSE_REQUESTED_EVENT) {
  1295. api.prevent_close();
  1296. }
  1297. window.emit(WINDOW_CLOSE_REQUESTED_EVENT, ())?;
  1298. }
  1299. WindowEvent::Destroyed => {
  1300. window.emit(WINDOW_DESTROYED_EVENT, ())?;
  1301. let label = window.label();
  1302. let windows_map = manager.inner.windows.lock().unwrap();
  1303. let windows = windows_map.values();
  1304. for window in windows {
  1305. window.eval(&format!(
  1306. r#"(function () {{ const metadata = window.__TAURI_METADATA__; if (metadata != null) {{ metadata.__windows = window.__TAURI_METADATA__.__windows.filter(w => w.label !== "{}"); }} }})()"#,
  1307. label
  1308. ))?;
  1309. }
  1310. }
  1311. WindowEvent::Focused(focused) => window.emit(
  1312. if *focused {
  1313. WINDOW_FOCUS_EVENT
  1314. } else {
  1315. WINDOW_BLUR_EVENT
  1316. },
  1317. (),
  1318. )?,
  1319. WindowEvent::ScaleFactorChanged {
  1320. scale_factor,
  1321. new_inner_size,
  1322. ..
  1323. } => window.emit(
  1324. WINDOW_SCALE_FACTOR_CHANGED_EVENT,
  1325. ScaleFactorChanged {
  1326. scale_factor: *scale_factor,
  1327. size: *new_inner_size,
  1328. },
  1329. )?,
  1330. WindowEvent::FileDrop(event) => match event {
  1331. FileDropEvent::Hovered(paths) => window.emit(WINDOW_FILE_DROP_HOVER_EVENT, paths)?,
  1332. FileDropEvent::Dropped(paths) => {
  1333. let scopes = window.state::<Scopes>();
  1334. for path in paths {
  1335. if path.is_file() {
  1336. let _ = scopes.allow_file(path);
  1337. } else {
  1338. let _ = scopes.allow_directory(path, false);
  1339. }
  1340. }
  1341. window.emit(WINDOW_FILE_DROP_EVENT, paths)?
  1342. }
  1343. FileDropEvent::Cancelled => window.emit(WINDOW_FILE_DROP_CANCELLED_EVENT, ())?,
  1344. _ => unimplemented!(),
  1345. },
  1346. WindowEvent::ThemeChanged(theme) => window.emit(WINDOW_THEME_CHANGED, theme.to_string())?,
  1347. }
  1348. Ok(())
  1349. }
  1350. #[derive(Clone, Serialize)]
  1351. #[serde(rename_all = "camelCase")]
  1352. struct ScaleFactorChanged {
  1353. scale_factor: f64,
  1354. size: PhysicalSize<u32>,
  1355. }
  1356. fn on_menu_event<R: Runtime>(window: &Window<R>, event: &MenuEvent) -> crate::Result<()> {
  1357. window.emit(MENU_EVENT, event.menu_item_id.clone())
  1358. }
  1359. #[cfg(feature = "isolation")]
  1360. fn request_to_path(request: &tauri_runtime::http::Request, base_url: &str) -> String {
  1361. let mut path = request
  1362. .uri()
  1363. .split(&['?', '#'][..])
  1364. // ignore query string
  1365. .next()
  1366. .unwrap()
  1367. .trim_start_matches(base_url)
  1368. .to_string();
  1369. if path.ends_with('/') {
  1370. path.pop();
  1371. }
  1372. let path = percent_encoding::percent_decode(path.as_bytes())
  1373. .decode_utf8_lossy()
  1374. .to_string();
  1375. if path.is_empty() {
  1376. // if the url has no path, we should load `index.html`
  1377. "index.html".to_string()
  1378. } else {
  1379. // skip leading `/`
  1380. path.chars().skip(1).collect()
  1381. }
  1382. }
  1383. #[cfg(test)]
  1384. mod tests {
  1385. use super::replace_with_callback;
  1386. #[test]
  1387. fn string_replace_with_callback() {
  1388. let mut tauri_index = 0;
  1389. #[allow(clippy::single_element_loop)]
  1390. for (src, pattern, replacement, result) in [(
  1391. "tauri is awesome, tauri is amazing",
  1392. "tauri",
  1393. || {
  1394. tauri_index += 1;
  1395. tauri_index.to_string()
  1396. },
  1397. "1 is awesome, 2 is amazing",
  1398. )] {
  1399. assert_eq!(replace_with_callback(src, pattern, replacement), result);
  1400. }
  1401. }
  1402. }