max / goingson
5 files changed,
+20 insertions,
-15 deletions
| @@ -1836,7 +1836,7 @@ dependencies = [ | |||
| 1836 | 1836 | ||
| 1837 | 1837 | [[package]] | |
| 1838 | 1838 | name = "goingson-core" | |
| 1839 | - | version = "0.4.0" | |
| 1839 | + | version = "0.4.2" | |
| 1840 | 1840 | dependencies = [ | |
| 1841 | 1841 | "async-trait", | |
| 1842 | 1842 | "chrono", | |
| @@ -1854,7 +1854,7 @@ dependencies = [ | |||
| 1854 | 1854 | ||
| 1855 | 1855 | [[package]] | |
| 1856 | 1856 | name = "goingson-db-sqlite" | |
| 1857 | - | version = "0.4.0" | |
| 1857 | + | version = "0.4.2" | |
| 1858 | 1858 | dependencies = [ | |
| 1859 | 1859 | "argon2", | |
| 1860 | 1860 | "async-trait", |
| @@ -8,7 +8,7 @@ default-members = ["src-tauri"] | |||
| 8 | 8 | resolver = "2" | |
| 9 | 9 | ||
| 10 | 10 | [workspace.package] | |
| 11 | - | version = "0.4.0" | |
| 11 | + | version = "0.4.2" | |
| 12 | 12 | edition = "2024" | |
| 13 | 13 | license-file = "LICENSE" | |
| 14 | 14 |
| @@ -329,10 +329,17 @@ const api = { | |||
| 329 | 329 | }, | |
| 330 | 330 | ||
| 331 | 331 | // Import — native CSV/TSV import (entity type auto-detected from headers) | |
| 332 | + | // plus vCard/iCalendar import. Must be ONE object: a second `import:` key | |
| 333 | + | // would shadow this one and silently break whichever block lost the merge | |
| 334 | + | // (it previously broke the native CSV preview/execute path). | |
| 332 | 335 | import: { | |
| 333 | 336 | preview: (filePath, options = {}) => invoke('preview_import', { input: { filePath, options } }), // Dry-run parse | |
| 334 | 337 | execute: (filePath, options = {}, selectedIndices = []) => | |
| 335 | 338 | invoke('execute_import', { input: { filePath, options, selectedIndices } }), // Create entities in DB | |
| 339 | + | previewVcf: (filePath) => invoke('preview_vcf', { filePath }), | |
| 340 | + | importVcf: (filePath) => invoke('import_vcf', { filePath }), | |
| 341 | + | previewIcs: (filePath) => invoke('preview_ics', { filePath }), | |
| 342 | + | importIcs: (filePath) => invoke('import_ics', { filePath }), | |
| 336 | 343 | }, | |
| 337 | 344 | ||
| 338 | 345 | // Attachments — file attachments on tasks and projects | |
| @@ -348,14 +355,6 @@ const api = { | |||
| 348 | 355 | fileSize: (filePath) => invoke('get_file_size', { filePath }), | |
| 349 | 356 | }, | |
| 350 | 357 | ||
| 351 | - | // External Import — vCard contacts and iCalendar events | |
| 352 | - | import: { | |
| 353 | - | previewVcf: (filePath) => invoke('preview_vcf', { filePath }), | |
| 354 | - | importVcf: (filePath) => invoke('import_vcf', { filePath }), | |
| 355 | - | previewIcs: (filePath) => invoke('preview_ics', { filePath }), | |
| 356 | - | importIcs: (filePath) => invoke('import_ics', { filePath }), | |
| 357 | - | }, | |
| 358 | - | ||
| 359 | 358 | // Time Tracking — start/stop timer, sessions, summaries | |
| 360 | 359 | timeTracking: { | |
| 361 | 360 | startTimer: (taskId) => invoke('start_timer', { taskId }), |
| @@ -432,9 +432,15 @@ pub async fn sync_subscribe( | |||
| 432 | 432 | } | |
| 433 | 433 | }; | |
| 434 | 434 | ||
| 435 | - | // Open in default browser | |
| 436 | - | if let Err(e) = open::that(&response.checkout_url) { | |
| 437 | - | tracing::warn!(error = %e, "Failed to open browser, returning URL"); | |
| 435 | + | // Open in default browser. Route through the same scheme validator as | |
| 436 | + | // open_external_url so a malicious/compromised sync server can't return a | |
| 437 | + | // non-http(s) URL (e.g. file://) that the OS opener would act on. | |
| 438 | + | if crate::commands::window::is_external_http_url(&response.checkout_url) { | |
| 439 | + | if let Err(e) = open::that(&response.checkout_url) { | |
| 440 | + | tracing::warn!(error = %e, "Failed to open browser, returning URL"); | |
| 441 | + | } | |
| 442 | + | } else { | |
| 443 | + | tracing::warn!("Refusing to open non-http(s) checkout URL"); | |
| 438 | 444 | } | |
| 439 | 445 | ||
| 440 | 446 | Ok(response.checkout_url) |
| @@ -159,7 +159,7 @@ pub async fn open_external_url( | |||
| 159 | 159 | /// Whether `url` is an `http`/`https` URL safe to hand to the system opener. | |
| 160 | 160 | /// Rejects every other scheme (`file:`, `javascript:`, `data:`, custom-handler | |
| 161 | 161 | /// schemes) and scheme-relative or path inputs. | |
| 162 | - | fn is_external_http_url(url: &str) -> bool { | |
| 162 | + | pub(crate) fn is_external_http_url(url: &str) -> bool { | |
| 163 | 163 | let lower = url.to_ascii_lowercase(); | |
| 164 | 164 | lower.starts_with("https://") || lower.starts_with("http://") | |
| 165 | 165 | } |