From fa3348a5ad077945ef09c76502da8f546c2a8e7c Mon Sep 17 00:00:00 2001 From: haex Date: Thu, 9 Oct 2025 11:16:25 +0200 Subject: [PATCH] polyfill for spa added. works now on android --- nuxt.config.ts | 2 + src-tauri/Cargo.lock | 1 + src-tauri/Cargo.toml | 1 + src-tauri/bindings/DbAction.ts | 2 +- src-tauri/bindings/ExtensionInfoResponse.ts | 2 +- src-tauri/bindings/FsAction.ts | 2 +- src-tauri/database/vault.db | Bin 131072 -> 131072 bytes .../app/src/main/assets/database/vault.db | Bin 131072 -> 131072 bytes src-tauri/gen/schemas/android-schema.json | 6 - src-tauri/gen/schemas/mobile-schema.json | 6 - src-tauri/src/crdt/transformer.rs | 13 + src-tauri/src/extension/core/manager.rs | 313 +++++- src-tauri/src/extension/core/manifest.rs | 20 +- src-tauri/src/extension/core/protocol.rs | 959 ++++++++++++++++-- src-tauri/src/extension/error.rs | 16 + src-tauri/src/extension/mod.rs | 44 +- src-tauri/src/extension/permissions/types.rs | 9 +- src-tauri/src/lib.rs | 1 + src-tauri/tauri.conf.json | 32 +- src/app.vue | 3 + .../haex/extension/dialog/install.vue | 68 +- .../haex/extension/dialog/reinstall.vue | 88 +- .../haex/extension/dialog/remove.vue | 123 ++- .../haex/extension/installed-card.vue | 157 +++ .../haex/extension/marketplace-card.vue | 173 ++++ .../item.vue} | 2 +- .../list.vue} | 0 src/composables/extensionMessageHandler.ts | 104 +- src/composables/useAndroidBackButton.ts | 60 ++ src/i18n.config.ts | 8 + .../[vaultId]/extensions/[extensionId].vue | 138 ++- .../vault/[vaultId]/extensions/index.vue | 479 +++++++-- src/plugins/plugins/i18n.client.ts | 9 + src/stores/extensions/index.ts | 85 +- src/stores/ui/index.ts | 13 - 35 files changed, 2566 insertions(+), 373 deletions(-) create mode 100644 src/components/haex/extension/installed-card.vue create mode 100644 src/components/haex/extension/marketplace-card.vue rename src/components/haex/extension/{permission-item.vue => permission/item.vue} (98%) rename src/components/haex/extension/{permission-list.vue => permission/list.vue} (100%) create mode 100644 src/composables/useAndroidBackButton.ts create mode 100644 src/i18n.config.ts create mode 100644 src/plugins/plugins/i18n.client.ts diff --git a/nuxt.config.ts b/nuxt.config.ts index ed6cb08..7fbc33a 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -80,6 +80,8 @@ export default defineNuxtConfig({ redirectOn: 'root', // recommended }, types: 'composition', + + vueI18n: './i18n.config.ts', }, zodI18n: { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 221919a..8c406ee 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1696,6 +1696,7 @@ dependencies = [ "ed25519-dalek", "fs_extra", "hex", + "lazy_static", "mime", "mime_guess", "rusqlite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6575f83..66d357f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -32,6 +32,7 @@ base64 = "0.22" ed25519-dalek = "2.1" fs_extra = "1.3.0" hex = "0.4" +lazy_static = "1.5" mime = "0.3" mime_guess = "2.0" serde = { version = "1", features = ["derive"] } diff --git a/src-tauri/bindings/DbAction.ts b/src-tauri/bindings/DbAction.ts index 4782511..449958b 100644 --- a/src-tauri/bindings/DbAction.ts +++ b/src-tauri/bindings/DbAction.ts @@ -3,4 +3,4 @@ /** * Definiert Aktionen, die auf eine Datenbank angewendet werden können. */ -export type DbAction = "read" | "readwrite" | "create" | "delete" | "alterdrop"; +export type DbAction = "read" | "readWrite" | "create" | "delete" | "alterDrop"; diff --git a/src-tauri/bindings/ExtensionInfoResponse.ts b/src-tauri/bindings/ExtensionInfoResponse.ts index c794c02..e93574b 100644 --- a/src-tauri/bindings/ExtensionInfoResponse.ts +++ b/src-tauri/bindings/ExtensionInfoResponse.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type ExtensionInfoResponse = { key_hash: string, name: string, full_id: string, version: string, display_name: string | null, namespace: string | null, allowed_origin: string, }; +export type ExtensionInfoResponse = { keyHash: string, name: string, fullId: string, version: string, displayName: string | null, namespace: string | null, allowedOrigin: string, }; diff --git a/src-tauri/bindings/FsAction.ts b/src-tauri/bindings/FsAction.ts index b42ac58..9529d0c 100644 --- a/src-tauri/bindings/FsAction.ts +++ b/src-tauri/bindings/FsAction.ts @@ -3,4 +3,4 @@ /** * Definiert Aktionen, die auf das Dateisystem angewendet werden können. */ -export type FsAction = "read" | "readwrite"; +export type FsAction = "read" | "readWrite"; diff --git a/src-tauri/database/vault.db b/src-tauri/database/vault.db index 5247ff49c7c5254de83812d773557528add6ad1f..9d40fc30a2b2a8e172b16d316b59032ddbe06455 100644 GIT binary patch delta 272 zcmZo@;Am*zm>@04%)r2)0K_mLI8n!#k$Gdn5`GRQ{xk;u&HQPb1qGt`eZ84j85((6 z&CDzl&5cdWk`j{*(o7P~Q%uv0(^6AYEldo|O^hwlEK-t^l2a@V4U&>g4N@#DEzB$p z%q=Za4U$rlOe~Vj8D}o7t@t~6vwR@X{O=6>fB3&|7F2l6&&tBgz{u46M}PYteMZg% zUPc=RzD0b=ymNRH8Ev);O<>ew@04#K6Fy0K_mLFj2>tk!fSX5`H#D{>=>hn>Pz8tmL1(Sw0ZR|HHul zXS1Nedwyo1^5#GK+yCe@awhOHl`!xv;!Eb8!<)$Cuw7^ZqZZ?41py02uBI#*c5zQn r#uop{jf^7GH%wyW-@b4XqYo?4*6r`7GX}D3u3=VW+@3g>aefj24fH0< diff --git a/src-tauri/gen/android/app/src/main/assets/database/vault.db b/src-tauri/gen/android/app/src/main/assets/database/vault.db index 3c09876cb9b5d0e7a3bc9684eefddb82304ae5cc..9d40fc30a2b2a8e172b16d316b59032ddbe06455 100644 GIT binary patch delta 3816 zcmeHKZ)_7~81J*TckNx<>)6=s58JLTWHhT=OSiTQI9JAm1>8iofDmo3z3X;tYe##9 zfpl9tz?f*5zJxc#KOYbiAyK0HU}6M~F@{fy-;94UW{6)5egl@E!Mp3ae?kHpq9!zH zb8qi?f6w#&-sgRP&oeP$nwT(6RTb-XIvuBc3!mcGT;2_OUE_h5jmlrZ+*V&yGPiAC zT4@_tPFft)9r}CIIqE!_HC`h(8GbgN(;qRs1qb!@-~tzN*k3Q|V)6R}u$3;Ke7uxo zgF!eZ;+av{f~NF#{LCO2QHW~9C<#>`PaXmQV&`gt?GNG)2NZx1MW7$eYv{6E%_@@h zcwn+0|270pC_%GCi8-E-rI-}uWyC;3F+U`SIawT)U6eU2@Un>P`cib&XrQHGzF*A0 z!zYGewK0`QC!^>>Q6)NMw4uvIEIye8)ghQ*Z350B&>Q_j|6^feuzSLOEF zNVcH?jt-;kwmM+crQl2b5OfJ*Dw>o=WGN9}T!|Q$lZO_E1BqdAL|G2{#8wlKqDw8~ z{P2>nM$(ZXDH<9SmrPDc{c&DSC(#eKwOEa{`W~@9!4T|@291YrtTg(?YA?3u)>=($ z))9nUN{S2OC{AR+vr6S6#7EVbLo%Q27iApCfCFskj-?J~GEjFnlMgbmsXnCf=nCg& zhh5?P{IGL!jMh@+B~s=iL%1yiRd{j?D#23V%8Z}RfCc}wA3FEtVHS3N9&KSf54tc` zC`-vnUW&`9MOKxKlGF2JZgb~T!M^RiT)iO1B~hs7x_f%L?!N8Y(HQAPjYO$N9kYTu z)MRGyr%BjL?6jyEJWrlk<-m&D#-Pe>&F(gntk(;jc1ltgqwwpQa_5|ji}AwtQ-K`q^)NkI+`CHhn5j%PcAy`5ZF z_vX&sTzHvfnCt20@^LTNhq-kM@hX&-Kd!KX7#bN&g`H_qIdh83YAHUh?6-=vJhPWO zHvQG{a`St6Ut#6Uri>)(bi$-Vi%Wftr_)dlM1dp*T{byNSIpCP7?&M@6tRII*dC|G z-tjb47|5(%kFUzm2!;ZL(XJ#yOzKxQeVuZMm1BZuC8#Y~a+)b;&_J+#O&ZQc8ETcy z*`0v*5W{qp<}b+#t5ta@Vq!XGnu$$2xIN7Hwj!f0Yc=|Ot-Rmc?2GVGcdVKB3oS8k zOccdHv)kY74a5RMBoY-`J?==f#VrI{1HM+bzcnDbBSNG(5b?w5cQd1ZY=b;lK;)bK z(WuWGhj|`Y1V8Ux&$s&42U@&v z`s^#$u2WjDw{*080hl?%o_|dNn7N{B|5O30R$L7Q1z_gjJ9LHiJ<) zd<=6UP?6Yd!W53#qJU=v+9D1j?){rO?wXl}Bml4LtN z;8=%>i!Ks%P^}Yz%CXZ#3| z=nGRl4h<`%@>%2CYVlQNpqzabDn>88#?qeOg~d+8TSl^eKO8rrgsBNw1*ckVH3)oS z5L)OZRd%66Kt{6{e{~QVk&7r(9k_6UhZpYNi`;J`v$B^^S}4au?_m^T3Vp`k>0R!> ZAE533r9OsTd4XufAmu#cUZ`uBKLOO3x8VQ) delta 4242 zcmc&%du$v>8Ncz}?cVL)-P*C8jqTVzJ89EvOfs|YRf-bFO@ec{PG|z9uk-BeZgRr0 zot#q=mDce&5o%Rb;!V6x$cs`05)^3tN05pHLaL}BQ7I}yNIVh|2$d*BMM_1>Yj)3% zvl~)@B5}GM?Yw^T&3?a`@An-!k~nfC@x_)pJ`#y!>64&O4SkH`UGf+oS@+meaasey z`?QR#5Z=#3BhRP5nR+J0@|S8L|M}$KlFuianJ+Q-#v5WkiK?-|*kCYp;Wv?tvtmyV zIMy_ZtwComb`BMsL!-`6sW?1TDvUS>_7zLv`_isdRUhO3^c-mQlF6O9f$S)q-^C>d zhUox2N?{h7M)2s~*;>}W>l9cAvi?R7i2l>3K*sCm9^%S194jIOCf57g&wyMf-}8wr z-MhAAcXe<6_?B#EXvEO)XwfZ_5WCdry&k>OzcdAeSg^3SGv45DI1AGLU(bNLejATE z3(FVx;XPGVJUCj_N9GLWy0QbM;e!Xr?9g0UX<)^C9!wOxanRs@XA;Cf$)B18ZRt|s zGtS|{V|ehOQ)={oFbO(n&(%rrsK4I>y8mqtNX#RRjJQAVgERCC^#d-PpQ)Z$K>n8} zLA_AjwtQQ?_oA@U8(`z}lcJ^Bk*QKc`6J6%QB}dj6Z1Ct7bZbNO{qRK!@qJC+!cJ5 z7h+s)cb$LrB-q6-D}Syw^!CxQxmj_4-4tN`y(d5ms0(IxRvKP70lv1dhnil&TL(VI z*HW@=6m=aO6)8ZIPo}|gua0T(pPd4nHv$;{>@-mQ>(ih+aOu8yni2PO&uv}shfV{= zD@0fFnF&6{irwAdSO*0%sk`XXt=>qK_b;BNhQD_jyc&MJnjYReAKT2P%p@cBt*Jm= ziig?E#Ua}{w1mOld3F_= zCut$zX5rvaasRt6p5VAhEn+crI;a%pE zXwktcpe2S?pE~xVF-F|Ewz6+J)qnOJSjNZ4w?|oV?OIUIdw-6v0<9Mi7nZhrXWlds5+)BLkH@PhFsfp4Psap zfeNv0%hq+(Q56TA>aR<@mFGh++p=`5DuxU-8A8)CEe%SVX_yjrOiOX;A`odQHPc0!lgA&2nG$4?$MPn))mEdiL0B$)>V^8 zmP$+q)4fQJri>xerO;4vC4@}fwsquc^xK*;@b9_y_|<$oM8mdWyPD!ystjdGu_R<8 z+eJE}ZID20BkFR>7Dzo!TnoAqadgGNmV*u1(h>0W!)u?;$3igKwh_`b8_7@~!zPj> zqL2XGAvVIQWSJNfOEWB8w0MiXv;4K@galdh(w4@}RO&RaSLL zF*UPrI#XF{dU#0DjRLm5O53OISWv+rdtILN{) z5#jH`yTUudTf!fO-wD4Fekr^mye9lucx9$8`&B`TMI#^;6ns!{L6HoKL{P9n!30G- zC}Keooq2bCGW*=7TKYe}gG~au3`-worVhR>%>1h#WMA6B*8)DQAKwuVYw5Y#b2}>C zbxRu>=NdMq6LusWd#OfvKoHm`GSTD-Zd2-7;s!4zzQMF5H^g3IUXAz0-iiJ&dKg@e zHb#PDQpB4x%9>f8thkl0Mb37bD+f^ zJ_$y+^21b9P%U_LV~BLc2PuDG3bY9IOORgosalv9UN1yxlnG!~@VRk&HZG0bGA`9F zj!W~?xyK{h!kGR@5Yw~CMe7cPVe)<^kQ5+N9I9xNXl+FyL{q!#g8&P?w`*AcH6OI5 z8@S|nt|Ai3bqgY)rCKD=_&9oaWKI-Tw!!8;AT~0aBNvy7joxaUT)Dh*PHi|hQybMm zy`r!u*3S|-kZ`xsQa2@XeUumU5#X-ha%k3#Lp8)m@CXv@e6Sh`*rMe^!w36nFllR)SFbrYygcDY9}l9^?_rL zglSHpFywr;;2mdM;&X4!jqQusHb17>YfNtGSjZISW#2+mpz!?mQ{Zrr!!U(aj9BUm zB-;vR9;Jmv^Y6`Uc1s5%ep;@`x_|JiYTL#i$g-j=gYu_B3%w4$K6nCAoLS-Z@n2xN z+Zl29`pT5+nfBPq@o1aZomqEx%Ob-oITATwj~1x#bBlXQo!Ps%WG78jIz1R~^Dbu6 z%!XD*?9WxOR%e>{W#jHjR?OwV1nV`XJ3w8yYJZwb?;RwC(c(U*G>Z3)c Result<(), DatabaseError> { + // Add both haex_timestamp and haex_tombstone columns insert_stmt .columns .push(Ident::new(self.columns.hlc_timestamp)); + insert_stmt + .columns + .push(Ident::new(self.columns.tombstone)); match insert_stmt.source.as_mut() { Some(query) => match &mut *query.body { SetExpr::Values(values) => { for row in &mut values.rows { + // Add haex_timestamp value row.push(Expr::Value( Value::SingleQuotedString(timestamp.to_string()).into(), )); + // Add haex_tombstone value (0 = not deleted) + row.push(Expr::Value( + Value::Number("0".to_string(), false).into(), + )); } } SetExpr::Select(select) => { let hlc_expr = Expr::Value(Value::SingleQuotedString(timestamp.to_string()).into()); select.projection.push(SelectItem::UnnamedExpr(hlc_expr)); + // Add haex_tombstone value (0 = not deleted) + let tombstone_expr = + Expr::Value(Value::Number("0".to_string(), false).into()); + select.projection.push(SelectItem::UnnamedExpr(tombstone_expr)); } _ => { return Err(DatabaseError::UnsupportedStatement { diff --git a/src-tauri/src/extension/core/manager.rs b/src-tauri/src/extension/core/manager.rs index 183d3d0..8766394 100644 --- a/src-tauri/src/extension/core/manager.rs +++ b/src-tauri/src/extension/core/manager.rs @@ -8,10 +8,11 @@ use crate::extension::database::executor::SqlExecutor; use crate::extension::error::ExtensionError; use crate::extension::permissions::manager::PermissionManager; use crate::extension::permissions::types::ExtensionPermission; -use crate::table_names::TABLE_EXTENSIONS; +use crate::table_names::{TABLE_EXTENSIONS, TABLE_EXTENSION_PERMISSIONS}; use crate::AppState; use std::collections::HashMap; -use std::fs::{self, File}; +use std::fs; +use std::io::Cursor; use std::path::PathBuf; use std::sync::Mutex; use std::time::{Duration, SystemTime}; @@ -33,6 +34,7 @@ pub struct MissingExtension { } struct ExtensionDataFromDb { + full_extension_id: String, manifest: ExtensionManifest, enabled: bool, } @@ -64,19 +66,19 @@ impl ExtensionManager { /// Extrahiert eine Extension-ZIP-Datei und validiert das Manifest fn extract_and_validate_extension( - source_path: &str, + bytes: Vec, temp_prefix: &str, ) -> Result { - let source = PathBuf::from(source_path); let temp = std::env::temp_dir().join(format!("{}_{}", temp_prefix, uuid::Uuid::new_v4())); - std::fs::create_dir_all(&temp).map_err(|e| ExtensionError::Filesystem { source: e })?; + fs::create_dir_all(&temp) + .map_err(|e| ExtensionError::filesystem_with_path(temp.display().to_string(), e))?; - let file = File::open(&source).map_err(|e| ExtensionError::Filesystem { source: e })?; - let mut archive = - ZipArchive::new(file).map_err(|e| ExtensionError::InstallationFailed { + let mut archive = ZipArchive::new(Cursor::new(bytes)).map_err(|e| { + ExtensionError::InstallationFailed { reason: format!("Invalid ZIP: {}", e), - })?; + } + })?; archive .extract(&temp) @@ -84,7 +86,30 @@ impl ExtensionManager { reason: format!("Cannot extract ZIP: {}", e), })?; + // Check if manifest.json is directly in temp or in a subdirectory let manifest_path = temp.join("manifest.json"); + let actual_dir = if manifest_path.exists() { + temp.clone() + } else { + // manifest.json is in a subdirectory - find it + let mut found_dir = None; + for entry in fs::read_dir(&temp) + .map_err(|e| ExtensionError::filesystem_with_path(temp.display().to_string(), e))? + { + let entry = entry.map_err(|e| ExtensionError::Filesystem { source: e })?; + let path = entry.path(); + if path.is_dir() && path.join("manifest.json").exists() { + found_dir = Some(path); + break; + } + } + + found_dir.ok_or_else(|| ExtensionError::ManifestError { + reason: "manifest.json not found in extension archive".to_string(), + })? + }; + + let manifest_path = actual_dir.join("manifest.json"); let manifest_content = std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError { reason: format!("Cannot read manifest: {}", e), @@ -92,14 +117,14 @@ impl ExtensionManager { let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?; - let content_hash = ExtensionCrypto::hash_directory(&temp).map_err(|e| { + let content_hash = ExtensionCrypto::hash_directory(&actual_dir).map_err(|e| { ExtensionError::SignatureVerificationFailed { reason: e.to_string(), } })?; Ok(ExtractedExtension { - temp_dir: temp, + temp_dir: actual_dir, manifest, content_hash, }) @@ -119,7 +144,8 @@ impl ExtensionManager { // Sicherstellen, dass das Basisverzeichnis existiert if !path.exists() { - fs::create_dir_all(&path).map_err(|e| ExtensionError::Filesystem { source: e })?; + fs::create_dir_all(&path) + .map_err(|e| ExtensionError::filesystem_with_path(path.display().to_string(), e))?; } Ok(path) } @@ -145,9 +171,34 @@ impl ExtensionManager { app_handle: &AppHandle, full_extension_id: &str, ) -> Result { + // Parse full_extension_id: key_hash_name_version + // Split on first underscore to get key_hash + let first_underscore = + full_extension_id + .find('_') + .ok_or_else(|| ExtensionError::ValidationError { + reason: format!("Invalid full_extension_id format: {}", full_extension_id), + })?; + + let key_hash = &full_extension_id[..first_underscore]; + let rest = &full_extension_id[first_underscore + 1..]; + + // Split on last underscore to get version + let last_underscore = rest + .rfind('_') + .ok_or_else(|| ExtensionError::ValidationError { + reason: format!("Invalid full_extension_id format: {}", full_extension_id), + })?; + + let name = &rest[..last_underscore]; + let version = &rest[last_underscore + 1..]; + + // Build hierarchical path: key_hash/name/version/ let specific_extension_dir = self .get_base_extension_dir(app_handle)? - .join(full_extension_id); + .join(key_hash) + .join(name) + .join(version); Ok(specific_extension_dir) } @@ -220,14 +271,44 @@ impl ExtensionManager { }) } + pub async fn remove_extension_by_full_id( + &self, + app_handle: &AppHandle, + full_extension_id: &str, + state: &State<'_, AppState>, + ) -> Result<(), ExtensionError> { + // Parse full_extension_id: key_hash_name_version + // Since _ is not allowed in name and version, we can split safely + let parts: Vec<&str> = full_extension_id.split('_').collect(); + + if parts.len() != 3 { + return Err(ExtensionError::ValidationError { + reason: format!( + "Invalid full_extension_id format (expected 3 parts): {}", + full_extension_id + ), + }); + } + + let key_hash = parts[0]; + let name = parts[1]; + let version = parts[2]; + + self.remove_extension_internal(app_handle, key_hash, name, version, state) + .await + } + pub async fn remove_extension_internal( &self, app_handle: &AppHandle, key_hash: &str, - extension_id: &str, + extension_name: &str, extension_version: &str, state: &State<'_, AppState>, ) -> Result<(), ExtensionError> { + // Erstelle full_extension_id: key_hash_name_version + let full_extension_id = format!("{}_{}_{}",key_hash, extension_name, extension_version); + // Lösche Permissions und Extension-Eintrag in einer Transaktion with_connection(&state.db, |conn| { let tx = conn.transaction().map_err(DatabaseError::from)?; @@ -236,31 +317,59 @@ impl ExtensionManager { reason: "Failed to lock HLC service".to_string(), })?; - // Lösche alle Permissions - PermissionManager::delete_permissions_in_transaction(&tx, &hlc_service, extension_id)?; + // Lösche alle Permissions mit full_extension_id + PermissionManager::delete_permissions_in_transaction( + &tx, + &hlc_service, + &full_extension_id, + )?; - // Lösche Extension-Eintrag + // Lösche Extension-Eintrag mit full_extension_id let sql = format!("DELETE FROM {} WHERE id = ?", TABLE_EXTENSIONS); SqlExecutor::execute_internal_typed( &tx, &hlc_service, &sql, - rusqlite::params![extension_id], + rusqlite::params![full_extension_id], )?; tx.commit().map_err(DatabaseError::from) })?; - // Entferne aus dem In-Memory-Manager - self.remove_extension(&extension_id)?; + // Entferne aus dem In-Memory-Manager mit full_extension_id + self.remove_extension(&full_extension_id)?; - // Lösche Dateien vom Dateisystem + // Lösche nur den spezifischen Versions-Ordner: key_hash/name/version let extension_dir = - self.get_extension_dir(app_handle, key_hash, extension_id, extension_version)?; + self.get_extension_dir(app_handle, key_hash, extension_name, extension_version)?; if extension_dir.exists() { - std::fs::remove_dir_all(&extension_dir) - .map_err(|e| ExtensionError::Filesystem { source: e })?; + std::fs::remove_dir_all(&extension_dir).map_err(|e| { + ExtensionError::filesystem_with_path(extension_dir.display().to_string(), e) + })?; + + // Versuche, leere Parent-Ordner zu löschen + // 1. Extension-Name-Ordner (key_hash/name) + if let Some(name_dir) = extension_dir.parent() { + if name_dir.exists() { + if let Ok(entries) = std::fs::read_dir(name_dir) { + if entries.count() == 0 { + let _ = std::fs::remove_dir(name_dir); + + // 2. Key-Hash-Ordner (key_hash) - nur wenn auch leer + if let Some(key_hash_dir) = name_dir.parent() { + if key_hash_dir.exists() { + if let Ok(entries) = std::fs::read_dir(key_hash_dir) { + if entries.count() == 0 { + let _ = std::fs::remove_dir(key_hash_dir); + } + } + } + } + } + } + } + } } Ok(()) @@ -268,9 +377,9 @@ impl ExtensionManager { pub async fn preview_extension_internal( &self, - source_path: String, + file_bytes: Vec, ) -> Result { - let extracted = Self::extract_and_validate_extension(&source_path, "haexhub_preview")?; + let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_preview")?; let is_valid_signature = ExtensionCrypto::verify_signature( &extracted.manifest.public_key, @@ -293,11 +402,11 @@ impl ExtensionManager { pub async fn install_extension_with_permissions_internal( &self, app_handle: AppHandle, - source_path: String, + file_bytes: Vec, custom_permissions: EditablePermissions, state: &State<'_, AppState>, ) -> Result { - let extracted = Self::extract_and_validate_extension(&source_path, "haexhub_ext")?; + let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_ext")?; // Signatur verifizieren (bei Installation wird ein Fehler geworfen, nicht nur geprüft) ExtensionCrypto::verify_signature( @@ -316,17 +425,95 @@ impl ExtensionManager { &extracted.manifest.version, )?; - std::fs::create_dir_all(&extensions_dir) - .map_err(|e| ExtensionError::Filesystem { source: e })?; + std::fs::create_dir_all(&extensions_dir).map_err(|e| { + ExtensionError::filesystem_with_path(extensions_dir.display().to_string(), e) + })?; - copy_directory( - extracted.temp_dir.to_string_lossy().to_string(), - extensions_dir.to_string_lossy().to_string(), - )?; + // Copy contents of extracted.temp_dir to extensions_dir + // Note: extracted.temp_dir already points to the correct directory with manifest.json + for entry in fs::read_dir(&extracted.temp_dir).map_err(|e| { + ExtensionError::filesystem_with_path(extracted.temp_dir.display().to_string(), e) + })? { + let entry = entry.map_err(|e| ExtensionError::Filesystem { source: e })?; + let path = entry.path(); + let file_name = entry.file_name(); + let dest_path = extensions_dir.join(&file_name); + + if path.is_dir() { + copy_directory( + path.to_string_lossy().to_string(), + dest_path.to_string_lossy().to_string(), + )?; + } else { + fs::copy(&path, &dest_path).map_err(|e| { + ExtensionError::filesystem_with_path(path.display().to_string(), e) + })?; + } + } let permissions = custom_permissions.to_internal_permissions(&full_extension_id); - PermissionManager::save_permissions(state, &permissions).await?; + // Extension-Eintrag und Permissions in einer Transaktion speichern + with_connection(&state.db, |conn| { + let tx = conn.transaction().map_err(DatabaseError::from)?; + + let hlc_service = state.hlc.lock().map_err(|_| DatabaseError::MutexPoisoned { + reason: "Failed to lock HLC service".to_string(), + })?; + + // 1. Extension-Eintrag erstellen (oder aktualisieren falls schon vorhanden) + let insert_ext_sql = format!( + "INSERT OR REPLACE INTO {} (id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + TABLE_EXTENSIONS + ); + + SqlExecutor::execute_internal_typed( + &tx, + &hlc_service, + &insert_ext_sql, + rusqlite::params![ + full_extension_id, + extracted.manifest.name, + extracted.manifest.version, + extracted.manifest.author, + extracted.manifest.entry, + extracted.manifest.icon, + extracted.manifest.public_key, + extracted.manifest.signature, + extracted.manifest.homepage, + extracted.manifest.description, + true, // enabled + ], + )?; + + // 2. Permissions speichern (oder aktualisieren falls schon vorhanden) + let insert_perm_sql = format!( + "INSERT OR REPLACE INTO {} (id, extension_id, resource_type, action, target, constraints, status) VALUES (?, ?, ?, ?, ?, ?, ?)", + TABLE_EXTENSION_PERMISSIONS + ); + + for perm in &permissions { + use crate::database::generated::HaexExtensionPermissions; + let db_perm: HaexExtensionPermissions = perm.into(); + + SqlExecutor::execute_internal_typed( + &tx, + &hlc_service, + &insert_perm_sql, + rusqlite::params![ + db_perm.id, + db_perm.extension_id, + db_perm.resource_type, + db_perm.action, + db_perm.target, + db_perm.constraints, + db_perm.status, + ], + )?; + } + + tx.commit().map_err(DatabaseError::from) + })?; let extension = Extension { id: full_extension_id.clone(), @@ -372,16 +559,28 @@ impl ExtensionManager { // Schritt 1: Alle Daten aus der Datenbank in einem Rutsch laden. let extensions = with_connection(&state.db, |conn| { - let sql = "SELECT id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled FROM haexExtensions"; - let results = SqlExecutor::select_internal(conn, sql, &[])?; + let sql = format!( + "SELECT id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled FROM {}", + TABLE_EXTENSIONS + ); + eprintln!("DEBUG: SQL Query before transformation: {}", sql); + let results = SqlExecutor::select_internal(conn, &sql, &[])?; + eprintln!("DEBUG: Query returned {} results", results.len()); let mut data = Vec::new(); for result in results { + let full_extension_id = result["id"] + .as_str() + .ok_or_else(|| DatabaseError::SerializationError { + reason: "Missing id field".to_string(), + })? + .to_string(); + let manifest = ExtensionManifest { - id: result["id"] + id: result["name"] .as_str() .ok_or_else(|| DatabaseError::SerializationError { - reason: "Missing id field".to_string(), + reason: "Missing name field".to_string(), })? .to_string(), name: result["name"] @@ -411,7 +610,11 @@ impl ExtensionManager { .or_else(|| result["enabled"].as_i64().map(|v| v != 0)) .unwrap_or(false); - data.push(ExtensionDataFromDb { manifest, enabled }); + data.push(ExtensionDataFromDb { + full_extension_id, + manifest, + enabled, + }); } Ok(data) })?; @@ -419,12 +622,19 @@ impl ExtensionManager { // Schritt 2: Die gesammelten Daten verarbeiten (Dateisystem, State-Mutationen). let mut loaded_extension_ids = Vec::new(); - for extension in extensions { - let full_extension_id = extension.manifest.full_extension_id()?; + eprintln!("DEBUG: Found {} extensions in database", extensions.len()); + + for extension_data in extensions { + let full_extension_id = extension_data.full_extension_id; + eprintln!("DEBUG: Processing extension: {}", full_extension_id); let extension_path = self.get_extension_path_by_full_extension_id(app_handle, &full_extension_id)?; if !extension_path.exists() || !extension_path.join("manifest.json").exists() { + eprintln!( + "DEBUG: Extension files missing for: {} at {:?}", + full_extension_id, extension_path + ); self.missing_extensions .lock() .map_err(|e| ExtensionError::MutexPoisoned { @@ -432,26 +642,31 @@ impl ExtensionManager { })? .push(MissingExtension { full_extension_id: full_extension_id.clone(), - name: extension.manifest.name.clone(), - version: extension.manifest.version.clone(), + name: extension_data.manifest.name.clone(), + version: extension_data.manifest.version.clone(), }); continue; } + eprintln!( + "DEBUG: Extension loaded successfully: {}", + full_extension_id + ); + let extension = Extension { id: full_extension_id.clone(), - name: extension.manifest.name.clone(), + name: extension_data.manifest.name.clone(), source: ExtensionSource::Production { path: extension_path, - version: extension.manifest.version.clone(), + version: extension_data.manifest.version.clone(), }, - manifest: extension.manifest, - enabled: extension.enabled, + manifest: extension_data.manifest, + enabled: extension_data.enabled, last_accessed: SystemTime::now(), }; + loaded_extension_ids.push(full_extension_id.clone()); self.add_production_extension(extension)?; - loaded_extension_ids.push(full_extension_id); } Ok(loaded_extension_ids) diff --git a/src-tauri/src/extension/core/manifest.rs b/src-tauri/src/extension/core/manifest.rs index 1b1b0fd..eec7aa7 100644 --- a/src-tauri/src/extension/core/manifest.rs +++ b/src-tauri/src/extension/core/manifest.rs @@ -76,6 +76,18 @@ impl ExtensionManifest { } pub fn full_extension_id(&self) -> Result { + // Validate that name and version don't contain underscores + if self.name.contains('_') { + return Err(ExtensionError::ValidationError { + reason: format!("Extension name cannot contain underscores: {}", self.name), + }); + } + if self.version.contains('_') { + return Err(ExtensionError::ValidationError { + reason: format!("Extension version cannot contain underscores: {}", self.version), + }); + } + let key_hash = self.calculate_key_hash()?; Ok(format!("{}_{}_{}", key_hash, self.name, self.version)) } @@ -175,6 +187,7 @@ impl ExtensionPermissions { #[derive(Serialize, Deserialize, Clone, Debug, TS)] #[ts(export)] +#[serde(rename_all = "camelCase")] pub struct ExtensionInfoResponse { pub key_hash: String, pub name: String, @@ -189,10 +202,15 @@ impl ExtensionInfoResponse { pub fn from_extension( extension: &crate::extension::core::types::Extension, ) -> Result { - // Annahme: get_tauri_origin ist in deinem `types`-Modul oder woanders definiert use crate::extension::core::types::get_tauri_origin; + // In development mode, use a wildcard for localhost to match any port + #[cfg(debug_assertions)] + let allowed_origin = "http://localhost:3003".to_string(); + + #[cfg(not(debug_assertions))] let allowed_origin = get_tauri_origin(); + let key_hash = extension.manifest.calculate_key_hash()?; let full_id = extension.manifest.full_extension_id()?; diff --git a/src-tauri/src/extension/core/protocol.rs b/src-tauri/src/extension/core/protocol.rs index 41c26fd..bc78bdf 100644 --- a/src-tauri/src/extension/core/protocol.rs +++ b/src-tauri/src/extension/core/protocol.rs @@ -1,16 +1,27 @@ // src-tauri/src/extension/core/protocol.rs +use crate::extension::core::types::get_tauri_origin; use crate::extension::error::ExtensionError; use crate::AppState; use mime; use serde::Deserialize; +use serde::Serialize; +use std::collections::HashMap; use std::fmt; use std::fs; use std::path::PathBuf; +use std::sync::Mutex; +use tauri::http::Uri; use tauri::http::{Request, Response}; use tauri::{AppHandle, State}; -#[derive(Deserialize, Debug)] +// Cache for modified HTML files (extension_id -> modified content) +lazy_static::lazy_static! { + static ref HTML_CACHE: Mutex>> = Mutex::new(HashMap::new()); + static ref EXTENSION_CACHE: Mutex> = Mutex::new(None); +} + +#[derive(Deserialize, Serialize, Debug, Clone)] struct ExtensionInfo { key_hash: String, name: String, @@ -22,6 +33,7 @@ enum DataProcessingError { HexDecoding(hex::FromHexError), Utf8Conversion(std::string::FromUtf8Error), JsonParsing(serde_json::Error), + Custom(String), } impl fmt::Display for DataProcessingError { @@ -32,6 +44,7 @@ impl fmt::Display for DataProcessingError { write!(f, "UTF-8-Konvertierungsfehler: {}", e) } DataProcessingError::JsonParsing(e) => write!(f, "JSON-Parsing-Fehler: {}", e), + DataProcessingError::Custom(msg) => write!(f, "Datenverarbeitungsfehler: {}", msg), } } } @@ -42,10 +55,17 @@ impl std::error::Error for DataProcessingError { DataProcessingError::HexDecoding(e) => Some(e), DataProcessingError::Utf8Conversion(e) => Some(e), DataProcessingError::JsonParsing(e) => Some(e), + DataProcessingError::Custom(_) => None, } } } +impl From for DataProcessingError { + fn from(msg: String) -> Self { + DataProcessingError::Custom(msg) + } +} + impl From for DataProcessingError { fn from(err: hex::FromHexError) -> Self { DataProcessingError::HexDecoding(err) @@ -152,101 +172,770 @@ pub fn extension_protocol_handler( app_handle: &AppHandle, request: &Request>, ) -> Result>, Box> { - let uri_ref = request.uri(); - println!("Protokoll Handler für: {}", uri_ref); + // Get the origin from the request + let origin = request + .headers() + .get("origin") + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); - let host = uri_ref - .host() - .ok_or("Kein Host (Extension ID) in URI gefunden")? - .to_string(); + // Only allow same-protocol requests (haex-extension://) or tauri origin + // For null/empty origin (initial load), use wildcard + let allowed_origin = if origin.starts_with("haex-extension://") || origin == get_tauri_origin() + { + origin + } else if origin.is_empty() || origin == "null" { + "*" // Allow initial load without origin + } else { + // Reject other origins + return Response::builder() + .status(403) + .body(Vec::from("Origin not allowed")) + .map_err(|e| e.into()); + }; + + // Handle OPTIONS requests for CORS preflight + if request.method() == "OPTIONS" { + return Response::builder() + .status(200) + .header("Access-Control-Allow-Origin", allowed_origin) + .header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + .header("Access-Control-Allow-Headers", "*") + .header("Access-Control-Allow-Credentials", "true") + .body(Vec::new()) + .map_err(|e| e.into()); + } + + let uri_ref = request.uri(); + let referer = request + .headers() + .get("referer") + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + + println!("Protokoll Handler für: {}", uri_ref); + println!("Origin: {}", origin); + println!("Referer: {}", referer); + + /* let encoded_info = + match parse_encoded_info_from_origin_or_uri_or_referer(&origin, uri_ref, &referer) { + Ok(info) => info, + Err(e) => { + eprintln!("Fehler beim Parsen des Origin für Extension-Info: {}", e); + return Response::builder() + .status(400) + .header("Access-Control-Allow-Origin", allowed_origin) + .body(Vec::from(format!("Ungültiger Origin: {}", e))) + .map_err(|e| e.into()); + } + }; */ + + let info = + match parse_encoded_info_from_origin_or_uri_or_referer_or_cache(&origin, uri_ref, &referer) + { + Ok(decoded) => { + println!("=== Extension Protocol Handler ==="); + println!("Full URI: {}", uri_ref); + println!( + "Encoded Info (aus Origin/URI/Referer/Cache): {}", + encode_hex_for_log(&decoded) + ); // Hilfs-Log + println!("Decoded info:"); + println!(" KeyHash: {}", decoded.key_hash); + println!(" Name: {}", decoded.name); + println!(" Version: {}", decoded.version); + decoded + } + Err(e) => { + eprintln!("Fehler beim Parsen (alle Fallbacks): {}", e); + return Response::builder() + .status(400) + .header("Access-Control-Allow-Origin", allowed_origin) + .body(Vec::from(format!("Ungültige Anfrage: {}", e))) + .map_err(|e| e.into()); + } + }; let path_str = uri_ref.path(); let segments_iter = path_str.split('/').filter(|s| !s.is_empty()); let resource_segments: Vec<&str> = segments_iter.collect(); let raw_asset_path = resource_segments.join("/"); + + // Handle SPA routing - serve index.html for all non-asset paths let asset_to_load = if raw_asset_path.is_empty() { "index.html" - } else { + } else if raw_asset_path.starts_with("_nuxt/") + || raw_asset_path.ends_with(".js") + || raw_asset_path.ends_with(".css") + || raw_asset_path.ends_with(".json") + || raw_asset_path.ends_with(".ico") + || raw_asset_path.ends_with(".txt") + || raw_asset_path.ends_with(".svg") + || raw_asset_path.ends_with(".png") + || raw_asset_path.ends_with(".jpg") + || raw_asset_path.ends_with(".jpeg") + || raw_asset_path.ends_with(".gif") + || raw_asset_path.ends_with(".woff") + || raw_asset_path.ends_with(".woff2") + || raw_asset_path.ends_with(".ttf") + || raw_asset_path.ends_with(".eot") + { + // Serve actual asset &raw_asset_path + } else { + // SPA fallback - serve index.html for routes + "index.html" }; - match process_hex_encoded_json(&host) { - Ok(info) => { - println!("Daten erfolgreich verarbeitet:"); - println!(" KeyHash: {}", info.key_hash); - println!(" Name: {}", info.name); - println!(" Version: {}", info.version); - let absolute_secure_path = resolve_secure_extension_asset_path( - app_handle, - state, - &info.key_hash, - &info.name, - &info.version, - &asset_to_load, - )?; + println!("Path: {}", path_str); + println!("Asset to load: {}", asset_to_load); - println!("absolute_secure_path: {}", absolute_secure_path.display()); + /* match process_hex_encoded_json(&encoded_info) { + Ok(info) => { + println!("=== Extension Protocol Handler ==="); + println!("Full URI: {}", uri_ref); + println!("Origin: {}", origin); + println!("Encoded Info (aus Origin): {}", encoded_info); + println!("Path: {}", path_str); + println!("Asset to load: {}", asset_to_load); + println!("Decoded info:"); + println!(" KeyHash: {}", info.key_hash); + println!(" Name: {}", info.name); + println!(" Version: {}", info.version); - if absolute_secure_path.exists() && absolute_secure_path.is_file() { - match fs::read(&absolute_secure_path) { - Ok(content) => { - let mime_type = mime_guess::from_path(&absolute_secure_path) - .first_or(mime::APPLICATION_OCTET_STREAM) - .to_string(); - let content_length = content.len(); - println!( - "Liefere {} ({}, {} bytes) ", - absolute_secure_path.display(), - mime_type, - content_length - ); - Response::builder() - .status(200) - .header("Content-Type", mime_type) - .header("Content-Length", content_length.to_string()) - .header("Accept-Ranges", "bytes") - .body(content) - .map_err(|e| e.into()) + let absolute_secure_path = resolve_secure_extension_asset_path( + app_handle, + state, + &info.key_hash, + &info.name, + &info.version, + &asset_to_load, + )?; + + println!("Resolved path: {}", absolute_secure_path.display()); + println!("File exists: {}", absolute_secure_path.exists()); + + if absolute_secure_path.exists() && absolute_secure_path.is_file() { + match fs::read(&absolute_secure_path) { + Ok(mut content) => { + let mime_type = mime_guess::from_path(&absolute_secure_path) + .first_or(mime::APPLICATION_OCTET_STREAM) + .to_string(); + + if asset_to_load == "index.html" && mime_type.contains("html") { + if let Ok(html_str) = String::from_utf8(content.clone()) { + let base_tag = format!(r#""#, encoded_info); + let modified_html = if let Some(head_pos) = html_str.find("") + { + let insert_pos = head_pos + 6; + format!( + "{}{}{}", + &html_str[..insert_pos], + base_tag, + &html_str[insert_pos..] + ) + } else { + // Fallback: Prepend + format!("{}{}", base_tag, html_str) + }; + content = modified_html.into_bytes(); + } + } + // Inject localStorage polyfill for HTML files with caching + if asset_to_load == "index.html" && mime_type.contains("html") { + // Create cache key: extension_id (from host) + let cache_key = format!("{}_{}", host, asset_to_load); + + // Check cache first + if let Ok(cache) = HTML_CACHE.lock() { + if let Some(cached_content) = cache.get(&cache_key) { + println!("Serving cached HTML for: {}", cache_key); + content = cached_content.clone(); + + let content_length = content.len(); + return Response::builder() + .status(200) + .header("Content-Type", mime_type) + .header("Content-Length", content_length.to_string()) + .header("Accept-Ranges", "bytes") + .header("X-HaexHub-Cache", "HIT") + .header("Access-Control-Allow-Origin", allowed_origin) + .header( + "Access-Control-Allow-Methods", + "GET, POST, OPTIONS", + ) + .header("Access-Control-Allow-Headers", "*") + .header("Access-Control-Allow-Credentials", "true") + .body(content) + .map_err(|e| e.into()); + } + } + + // Not in cache, modify and cache it + if let Ok(html_content) = String::from_utf8(content.clone()) { + let polyfill = r#""#; + + // Inject as the FIRST thing in , before any other script + let modified_html = if let Some(head_pos) = + html_content.find("") + { + let insert_pos = head_pos + 6; // After + format!( + "{}{}{}", + &html_content[..insert_pos], + polyfill, + &html_content[insert_pos..] + ) + } else if let Some(charset_pos) = html_content.find("{}{}", + &html_content[..charset_pos], + polyfill, + &html_content[charset_pos..] + ) + } else if let Some(html_pos) = html_content.find("") + { + // Insert right after DOCTYPE + let insert_pos = html_pos + 15; // After + format!( + "{}{}{}", + &html_content[..insert_pos], + polyfill, + &html_content[insert_pos..] + ) + } else { + // Prepend to entire file + format!("{}{}", polyfill, html_content) + }; + + content = modified_html.into_bytes(); + + // Cache the modified HTML + if let Ok(mut cache) = HTML_CACHE.lock() { + cache.insert(cache_key, content.clone()); + println!("Cached modified HTML for future requests"); + } + } + } + + let content_length = content.len(); + println!( + "Liefere {} ({}, {} bytes) ", + absolute_secure_path.display(), + mime_type, + content_length + ); + Response::builder() + .status(200) + .header("Content-Type", &mime_type) + .header("Content-Length", content_length.to_string()) + .header("Accept-Ranges", "bytes") + .header( + "X-HaexHub-Cache", + if asset_to_load == "index.html" && mime_type.contains("html") { + "MISS" + } else { + "N/A" + }, + ) + .header("Access-Control-Allow-Origin", allowed_origin) + .header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + .header("Access-Control-Allow-Headers", "*") + .header("Access-Control-Allow-Credentials", "true") + .body(content) + .map_err(|e| e.into()) + } + Err(e) => { + eprintln!( + "Fehler beim Lesen der Datei {}: {}", + absolute_secure_path.display(), + e + ); + let status_code = if e.kind() == std::io::ErrorKind::NotFound { + 404 + } else if e.kind() == std::io::ErrorKind::PermissionDenied { + 403 + } else { + 500 + }; + + Response::builder() + .status(status_code) + .header("Access-Control-Allow-Origin", allowed_origin) + .header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + .header("Access-Control-Allow-Headers", "*") + .body(Vec::new()) + .map_err(|e| e.into()) + } + } + } else { + eprintln!( + "Asset nicht gefunden oder ist kein File: {}", + absolute_secure_path.display() + ); + Response::builder() + .status(404) + .header("Access-Control-Allow-Origin", allowed_origin) + .header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + .header("Access-Control-Allow-Headers", "*") + .body(Vec::new()) + .map_err(|e| e.into()) + } + } + Err(e) => { + eprintln!("Fehler bei der Datenverarbeitung: {}", e); + + Response::builder() + .status(500) + .header("Access-Control-Allow-Origin", allowed_origin) + .header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + .header("Access-Control-Allow-Headers", "*") + .body(Vec::new()) + .map_err(|e| e.into()) + } + } + */ + + let absolute_secure_path = resolve_secure_extension_asset_path( + app_handle, + state, + &info.key_hash, + &info.name, + &info.version, + &asset_to_load, + )?; + + println!("Resolved path: {}", absolute_secure_path.display()); + println!("File exists: {}", absolute_secure_path.exists()); + + if absolute_secure_path.exists() && absolute_secure_path.is_file() { + match fs::read(&absolute_secure_path) { + Ok(mut content) => { + let mime_type = mime_guess::from_path(&absolute_secure_path) + .first_or(mime::APPLICATION_OCTET_STREAM) + .to_string(); + + // Für index.html – injiziere Tag + localStorage-Polyfill + if asset_to_load == "index.html" && mime_type.contains("html") { + // Cache-Key erstellen (extension-host + asset) + let host = uri_ref + .host() + .map_or("unknown".to_string(), |h| h.to_string()); + let cache_key = format!("{}_{}", host, asset_to_load); + + // Cache checken (aus deinem alten Code) + if let Ok(cache_guard) = HTML_CACHE.lock() { + if let Some(cached_content) = cache_guard.get(&cache_key) { + println!("Serving cached HTML for: {}", cache_key); + let content_length = cached_content.len(); + return Response::builder() + .status(200) + .header("Content-Type", &mime_type) + .header("Content-Length", content_length.to_string()) + .header("Accept-Ranges", "bytes") + .header("X-HaexHub-Cache", "HIT") + .header("Access-Control-Allow-Origin", allowed_origin) + .header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + .header("Access-Control-Allow-Headers", "*") + .header("Access-Control-Allow-Credentials", "true") + .body(cached_content.clone()) + .map_err(|e| e.into()); + } } - Err(e) => { - eprintln!( - "Fehler beim Lesen der Datei {}: {}", - absolute_secure_path.display(), - e - ); - let status_code = if e.kind() == std::io::ErrorKind::NotFound { - 404 - } else if e.kind() == std::io::ErrorKind::PermissionDenied { - 403 + + // Nicht gecacht: Modifiziere HTML + if let Ok(html_str) = String::from_utf8(content.clone()) { + // 1. Polyfill injizieren (als ERSTES in ) + let polyfill_script = r#""#; + + // 2. Base-Tag erstellen + let base_tag = format!(r#""#, encode_hex_for_log(&info)); + + // 3. Beide in injizieren: Polyfill zuerst, dann Base-Tag + let modified_html = if let Some(head_pos) = html_str.find("") { + let insert_pos = head_pos + 6; // Nach + format!( + "{}{}{}{}", + &html_str[..insert_pos], + polyfill_script, + base_tag, + &html_str[insert_pos..] + ) } else { - 500 + // Kein gefunden - prepend + format!("{}{}{}", polyfill_script, base_tag, html_str) }; - Response::builder() - .status(status_code) - .body(Vec::new()) - .map_err(|e| e.into()) + content = modified_html.into_bytes(); + + // Cache die modifizierte HTML (aus deinem alten Code) + if let Ok(mut cache_guard) = HTML_CACHE.lock() { + cache_guard.insert(cache_key, content.clone()); + println!("Cached modified HTML for future requests"); + } } } - } else { - eprintln!( - "Asset nicht gefunden oder ist kein File: {}", - absolute_secure_path.display() + + let content_length = content.len(); + println!( + "Liefere {} ({}, {} bytes) ", + absolute_secure_path.display(), + mime_type, + content_length ); Response::builder() - .status(404) + .status(200) + .header("Content-Type", &mime_type) + .header("Content-Length", content_length.to_string()) + .header("Accept-Ranges", "bytes") + .header( + "X-HaexHub-Cache", + if asset_to_load == "index.html" && mime_type.contains("html") { + "MISS" + } else { + "N/A" + }, + ) + .header("Access-Control-Allow-Origin", allowed_origin) + .header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + .header("Access-Control-Allow-Headers", "*") + .header("Access-Control-Allow-Credentials", "true") + .body(content) + .map_err(|e| e.into()) + } + Err(e) => { + eprintln!( + "Fehler beim Lesen der Datei {}: {}", + absolute_secure_path.display(), + e + ); + let status_code = if e.kind() == std::io::ErrorKind::NotFound { + 404 + } else if e.kind() == std::io::ErrorKind::PermissionDenied { + 403 + } else { + 500 + }; + Response::builder() + .status(status_code) + .header("Access-Control-Allow-Origin", allowed_origin) + .header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + .header("Access-Control-Allow-Headers", "*") .body(Vec::new()) .map_err(|e| e.into()) } } - Err(e) => { - eprintln!("Fehler bei der Datenverarbeitung: {}", e); - - Response::builder() - .status(500) - .body(Vec::new()) - .map_err(|e| e.into()) - } + } else { + eprintln!( + "Asset nicht gefunden oder ist kein File: {}", + absolute_secure_path.display() + ); + Response::builder() + .status(404) + .header("Access-Control-Allow-Origin", allowed_origin) + .header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + .header("Access-Control-Allow-Headers", "*") + .body(Vec::new()) + .map_err(|e| e.into()) } } @@ -256,3 +945,131 @@ fn process_hex_encoded_json(hex_input: &str) -> Result Result { + // Return direkt ExtensionInfo (dekodiert) + // 1-3. Bestehende Fallbacks (wie vorher, aber return decoded Info statt hex) + if !origin.is_empty() && origin != "null" { + if let Ok(hex) = parse_from_origin(origin) { + if let Ok(info) = process_hex_encoded_json(&hex) { + cache_extension_info(&info); // Cache setzen + println!("Parsed und gecached aus Origin: {}", hex); + return Ok(info); + } + } + } + + println!("Fallback zu URI-Parsing"); + if let Ok(hex) = parse_from_uri_path(uri_ref) { + if let Ok(info) = process_hex_encoded_json(&hex) { + cache_extension_info(&info); // Cache setzen + println!("Parsed und gecached aus URI: {}", hex); + return Ok(info); + } + } + + println!("Fallback zu Referer-Parsing: {}", referer); + if !referer.is_empty() && referer != "null" { + if let Ok(hex) = parse_from_uri_string(referer) { + if let Ok(info) = process_hex_encoded_json(&hex) { + cache_extension_info(&info); // Cache setzen + println!("Parsed und gecached aus Referer: {}", hex); + return Ok(info); + } + } + } + + // 4. Fallback: Globaler Cache (für Assets in derselben Session) + println!("Fallback zu Cache"); + if let Some(cached_info) = get_cached_extension_info() { + println!( + "Gecached Info verwendet: KeyHash={}, Name={}, Version={}", + cached_info.key_hash, cached_info.name, cached_info.version + ); + return Ok(cached_info); + } + + Err( + "Kein gültiger Hex in Origin, URI, Referer oder Cache gefunden" + .to_string() + .into(), + ) +} + +// NEU: Cache-Helper (Mutex-sicher) +fn cache_extension_info(info: &ExtensionInfo) { + if let Ok(mut cache) = EXTENSION_CACHE.lock() { + *cache = Some(info.clone()); + } +} + +fn get_cached_extension_info() -> Option { + if let Ok(cache) = EXTENSION_CACHE.lock() { + cache.clone() + } else { + None + } +} + +fn parse_hex_from_url_string(url_str: &str) -> Result { + // Suche nach Scheme-Ende (://) + let scheme_end = match url_str.find("://") { + Some(pos) => pos + 3, // Nach "://" + _none => return Err("Kein Scheme in URL".to_string().into()), + }; + + let after_scheme = &url_str[scheme_end..]; + let path_start = match after_scheme.find('/') { + Some(pos) => pos, + _none => return Err("Kein Path in URL".to_string().into()), + }; + + let path = &after_scheme[path_start..]; // z.B. "/7b22.../index.html" + let mut segments = path.split('/').filter(|s| !s.is_empty()); + + let first_segment = match segments.next() { + Some(seg) => seg, + _none => return Err("Kein Path-Segment in URL".to_string().into()), + }; + + validate_and_return_hex(first_segment) +} + +// Vereinfachte parse_from_origin +fn parse_from_origin(origin: &str) -> Result { + parse_hex_from_url_string(origin) +} + +// Vereinfachte parse_from_uri_path +fn parse_from_uri_path(uri_ref: &Uri) -> Result { + let uri_str = uri_ref.to_string(); + parse_hex_from_url_string(&uri_str) +} + +// Vereinfachte parse_from_uri_string (für Referer) +fn parse_from_uri_string(uri_str: &str) -> Result { + parse_hex_from_url_string(uri_str) +} + +// validate_and_return_hex bleibt unverändert (aus letztem Vorschlag) +fn validate_and_return_hex(segment: &str) -> Result { + if segment.is_empty() { + return Err("Kein Extension-Info (hex) im Path".to_string().into()); + } + if segment.len() % 2 != 0 { + return Err("Ungültiger Hex: Ungerade Länge".to_string().into()); + } + if !segment.chars().all(|c| c.is_ascii_hexdigit()) { + return Err("Ungültiger Hex: Ungültige Zeichen".to_string().into()); + } + Ok(segment.to_string()) +} + +fn encode_hex_for_log(info: &ExtensionInfo) -> String { + let json_str = serde_json::to_string(info).unwrap_or_default(); + hex::encode(json_str.as_bytes()) +} diff --git a/src-tauri/src/extension/error.rs b/src-tauri/src/extension/error.rs index 67ca8f4..bed0746 100644 --- a/src-tauri/src/extension/error.rs +++ b/src-tauri/src/extension/error.rs @@ -12,6 +12,7 @@ pub enum ExtensionErrorCode { MutexPoisoned = 1003, Database = 2000, Filesystem = 2001, + FilesystemWithPath = 2004, Http = 2002, Shell = 2003, Manifest = 3000, @@ -60,6 +61,12 @@ pub enum ExtensionError { source: std::io::Error, }, + #[error("Filesystem operation failed at '{path}': {source}")] + FilesystemWithPath { + path: String, + source: std::io::Error, + }, + #[error("HTTP request failed: {reason}")] Http { reason: String }, @@ -109,6 +116,7 @@ impl ExtensionError { ExtensionError::PermissionDenied { .. } => ExtensionErrorCode::PermissionDenied, ExtensionError::Database { .. } => ExtensionErrorCode::Database, ExtensionError::Filesystem { .. } => ExtensionErrorCode::Filesystem, + ExtensionError::FilesystemWithPath { .. } => ExtensionErrorCode::FilesystemWithPath, ExtensionError::Http { .. } => ExtensionErrorCode::Http, ExtensionError::Shell { .. } => ExtensionErrorCode::Shell, ExtensionError::ManifestError { .. } => ExtensionErrorCode::Manifest, @@ -146,6 +154,14 @@ impl ExtensionError { _ => None, } } + + /// Helper to create a filesystem error with path context + pub fn filesystem_with_path>(path: P, source: std::io::Error) -> Self { + Self::FilesystemWithPath { + path: path.into(), + source, + } + } } impl serde::Serialize for ExtensionError { diff --git a/src-tauri/src/extension/mod.rs b/src-tauri/src/extension/mod.rs index 59ccc41..5849fc4 100644 --- a/src-tauri/src/extension/mod.rs +++ b/src-tauri/src/extension/mod.rs @@ -28,7 +28,29 @@ pub fn get_extension_info( } #[tauri::command] -pub fn get_all_extensions(state: State) -> Result, String> { +pub async fn get_all_extensions( + app_handle: AppHandle, + state: State<'_, AppState>, +) -> Result, String> { + // Check if extensions are loaded, if not load them first + let needs_loading = { + let prod_exts = state + .extension_manager + .production_extensions + .lock() + .unwrap(); + let dev_exts = state.extension_manager.dev_extensions.lock().unwrap(); + prod_exts.is_empty() && dev_exts.is_empty() + }; + + if needs_loading { + state + .extension_manager + .load_installed_extensions(&app_handle, &state) + .await + .map_err(|e| format!("Failed to load extensions: {:?}", e))?; + } + let mut extensions = Vec::new(); // Production Extensions @@ -57,18 +79,18 @@ pub fn get_all_extensions(state: State) -> Result, - extension_path: String, + file_bytes: Vec, ) -> Result { state .extension_manager - .preview_extension_internal(extension_path) + .preview_extension_internal(file_bytes) .await } #[tauri::command] pub async fn install_extension_with_permissions( app_handle: AppHandle, - source_path: String, + file_bytes: Vec, custom_permissions: EditablePermissions, state: State<'_, AppState>, ) -> Result { @@ -76,7 +98,7 @@ pub async fn install_extension_with_permissions( .extension_manager .install_extension_with_permissions_internal( app_handle, - source_path, + file_bytes, custom_permissions, &state, ) @@ -177,6 +199,18 @@ pub async fn remove_extension( .await } +#[tauri::command] +pub async fn remove_extension_by_full_id( + app_handle: AppHandle, + full_extension_id: String, + state: State<'_, AppState>, +) -> Result<(), ExtensionError> { + state + .extension_manager + .remove_extension_by_full_id(&app_handle, &full_extension_id, &state) + .await +} + #[tauri::command] pub fn is_extension_installed( extension_id: String, diff --git a/src-tauri/src/extension/permissions/types.rs b/src-tauri/src/extension/permissions/types.rs index d5aa549..ed78da9 100644 --- a/src-tauri/src/extension/permissions/types.rs +++ b/src-tauri/src/extension/permissions/types.rs @@ -7,7 +7,7 @@ use ts_rs::TS; /// Definiert Aktionen, die auf eine Datenbank angewendet werden können. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)] -#[serde(rename_all = "lowercase")] +#[serde(rename_all = "camelCase")] #[ts(export)] pub enum DbAction { Read, @@ -38,9 +38,10 @@ impl FromStr for DbAction { fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "read" => Ok(DbAction::Read), - "read_write" => Ok(DbAction::ReadWrite), + "readwrite" | "read_write" => Ok(DbAction::ReadWrite), "create" => Ok(DbAction::Create), "delete" => Ok(DbAction::Delete), + "alterdrop" | "alter_drop" => Ok(DbAction::AlterDrop), _ => Err(ExtensionError::InvalidActionString { input: s.to_string(), resource_type: "database".to_string(), @@ -51,7 +52,7 @@ impl FromStr for DbAction { /// Definiert Aktionen, die auf das Dateisystem angewendet werden können. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)] -#[serde(rename_all = "lowercase")] +#[serde(rename_all = "camelCase")] #[ts(export)] pub enum FsAction { Read, @@ -76,7 +77,7 @@ impl FromStr for FsAction { fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "read" => Ok(FsAction::Read), - "read_write" => Ok(FsAction::ReadWrite), + "readwrite" | "read_write" => Ok(FsAction::ReadWrite), _ => Err(ExtensionError::InvalidActionString { input: s.to_string(), resource_type: "filesystem".to_string(), diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index cd0cde1..021d5da 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -81,6 +81,7 @@ pub fn run() { extension::is_extension_installed, extension::preview_extension, extension::remove_extension, + extension::remove_extension_by_full_id, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index e67f0c3..10584fc 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -9,6 +9,7 @@ "beforeBuildCommand": "pnpm generate", "frontendDist": "../dist" }, + "app": { "windows": [ { @@ -19,25 +20,40 @@ ], "security": { "csp": { - "default-src": ["'self'", "http://tauri.localhost"], + "default-src": ["'self'", "http://tauri.localhost", "haex-extension:"], "script-src": [ "'self'", "http://tauri.localhost", + "haex-extension:", "'wasm-unsafe-eval'" ], - "style-src": ["'self'", "http://tauri.localhost", "'unsafe-inline'"], + "style-src": [ + "'self'", + "http://tauri.localhost", + "haex-extension:", + "'unsafe-inline'" + ], "connect-src": [ "'self'", "http://tauri.localhost", + "haex-extension:", "ipc:", - "http://ipc.localhost" + "http://ipc.localhost", + "ws://localhost:*" ], - "img-src": ["'self'", "http://tauri.localhost", "data:", "blob:"], - "font-src": ["'self'", "http://tauri.localhost"], + "img-src": [ + "'self'", + "http://tauri.localhost", + "haex-extension:", + "data:", + "blob:" + ], + "font-src": ["'self'", "http://tauri.localhost", "haex-extension:"], "object-src": ["'none'"], - "media-src": ["'self'", "http://tauri.localhost"], - "frame-src": ["'none'"], - "frame-ancestors": ["'none'"] + "media-src": ["'self'", "http://tauri.localhost", "haex-extension:"], + "frame-src": ["haex-extension:"], + "frame-ancestors": ["'none'"], + "base-uri": ["'self'"] }, "assetProtocol": { "enable": true, diff --git a/src/app.vue b/src/app.vue index 04084e2..d997303 100644 --- a/src/app.vue +++ b/src/app.vue @@ -9,6 +9,9 @@