manager.rs 43 KB

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