Add deeplink for importing a site from a backup URL - #3618
Conversation
## Summary Adds a new `wp-studio://import-backup?url=<url>` deeplink that downloads a backup file over https, validates it, and opens the Add Site modal pre-routed to the backup import step with the file ready to import. An optional `&name=<filename>` parameter overrides the filename derived from the URL. ## Why Studio already supports deeplinks for OAuth, sync-connect, and blueprint-based site creation, but there was no way for an external site (e.g. WordPress.com or a third-party host) to hand a user off into Studio with a specific backup file queued for import. Users had to download the backup manually, open Studio, start an Add Site flow, and re-select the file from disk. This closes that gap so a single click can take a user from a "Download backup" link into a populated Add Site → Import from a backup flow. Because the deeplink is triggerable from any web page, email, or IM, the inbound URL is fully attacker-controlled, and the downloaded backup carries PHP plugins/themes that Studio executes via WordPress Playground after import — so the URL has to be handled defensively. ## How The main-process handler (`apps/studio/src/lib/deeplink/handlers/import-backup.ts`) parses the URL directly — the value from `URLSearchParams.get()` is already percent-decoded, so it is deliberately not decoded a second time; re-decoding would let an attacker double-encode characters like `?` to shift the query boundary past a future allowlist check (the Apache CVE-2021-41773 shape). It rejects any scheme other than `https:` so a network MITM cannot swap the payload of a cleartext download, then downloads the backup to a sanitized path under the OS temp directory, validates the extension against `ACCEPTED_IMPORT_FILE_TYPES`, confirms the downloaded file is non-empty, and emits a new `import-backup-from-deeplink` IPC event with the path/name/size. On failure it cleans up the temp file and surfaces an error dialog with an "Open Studio Logs" shortcut; network errors get a connection-specific message. The renderer hook `useImportBackupDeeplink` listens for that event and feeds the file reference into the Add Site form, then flips an `isDeeplinkFlow` flag so the existing navigator routing logic in `add-site/index.tsx` jumps straight to `/backup/create`. Because the file is downloaded by the main process, it never passes through a browser `File` instance. To avoid a separate code path through `importFile`, `use-import-export.tsx` now accepts an `ImportSource = File | DeeplinkBackupFile` union and branches on `isDeeplinkBackupFile` to skip `getPathForFile` when the path is already known. For these deeplink imports it also passes `removeBackupOnComplete` so the main process deletes the downloaded temp file once the import settles, on both success and failure. `useAddSite` and the Add Site modal were updated to thread the wider type through. ## Testing - [ ] Trigger `wp-studio://import-backup?url=<https-url-to-a-valid-.zip-or-.tar.gz>` (e.g. via `open` on macOS) and confirm Studio opens, downloads the file, and the Add Site modal opens at the "Import from a backup" step with the backup name shown. - [ ] Trigger the same deeplink with `&name=my-export.tar.gz` and confirm the supplied name is used in the modal. - [ ] Trigger with an `http://` or `file://` URL and confirm the import is rejected with "Please check the link and try again." - [ ] Trigger with a URL whose path ends in `.txt` (or any extension outside `ACCEPTED_IMPORT_FILE_TYPES`) and confirm the error dialog appears with "Please check the link and try again." - [ ] Trigger with an unreachable host (e.g. `https://does-not-exist.invalid/site.zip`) and confirm the network-specific error message appears, and that "Open Studio Logs" opens the log file. - [ ] Trigger while Studio is minimized and confirm the window is restored and focused before the modal opens. - [ ] `npm test -- apps/studio/src/lib/deeplink/tests/import-backup.test.ts` passes. - [ ] `npx eslint --fix` on modified files and `npm run typecheck` are clean.
e37c09a to
230cc95
Compare
|
Nice defensive work on the URL handling. One gap stands out, plus a smaller follow-up — both worth closing before this lands, since an imported backup runs PHP via Playground. 1. The
|
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Pushed both changes to this branch in 835c349 (host allow-list + the Heads-up: pushing required the branch to be current with |
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
📊 Performance Test ResultsComparing 9e64fd2 vs trunk app-size
site-editor
site-startup
Results are median values from multiple test runs. Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff) |
|
@borkweb I'm marking this as a draft as there are some conflicts to solve. Feel free to pass for review when those are fixed. |
Summary
Adds a
wp-studio://import-backup?url=<url>deeplink that downloads a backup over https, validates it, and opens the Add Site modal at the import step with the file ready to go. Optional&name=<filename>overrides the filename derived from the URL.Why
There was no way for an external site (WordPress.com, a third-party host) to hand a user into Studio with a backup queued for import — they had to download it, open Studio, and re-select the file by hand. The deeplink is triggerable from any web page, email, or IM, so the URL is attacker-controlled, and backups run PHP via Playground on import — hence the defensive URL handling below.
How
handlers/import-backup.ts): parses the URL without re-decoding (URLSearchParams.get()already decodes it; a second decode would allow double-encoding past a future allowlist — the CVE-2021-41773 shape), rejects non-https:schemes (blocks MITM payload swaps), downloads to a sanitized temp path, validates extension and non-empty size, then emitsimport-backup-from-deeplink. Failures clean up the temp file and show an error dialog.useImportBackupDeeplinkfeeds the file into the Add Site form and routes to/backup/create. Since the file never passes through a browserFile,use-import-export.tsxtakes anImportSource = File | DeeplinkBackupFileunion and passesremoveBackupOnCompleteso the temp download is deleted after import.Testing
.zip/.tar.gzURL → Studio opens at the backup import step with the name shown.&name=my-export.tar.gz→ supplied name is used.http://orfile://URL → rejected with "Please check the link and try again.".txt) → same error dialog.npm test -- apps/studio/src/lib/deeplink/tests/import-backup.test.tspasses.Related issues