manager.rs 49 KB

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