Skip to content

Add deeplink for importing a site from a backup URL#3618

Draft
borkweb wants to merge 1 commit into
trunkfrom
feature/import-backup
Draft

Add deeplink for importing a site from a backup URL#3618
borkweb wants to merge 1 commit into
trunkfrom
feature/import-backup

Conversation

@borkweb
Copy link
Copy Markdown
Member

@borkweb borkweb commented May 26, 2026

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

  • Main process (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 emits import-backup-from-deeplink. Failures clean up the temp file and show an error dialog.
  • Renderer: useImportBackupDeeplink feeds the file into the Add Site form and routes to /backup/create. Since the file never passes through a browser File, use-import-export.tsx takes an ImportSource = File | DeeplinkBackupFile union and passes removeBackupOnComplete so the temp download is deleted after import.

Testing

  • Valid https .zip/.tar.gz URL → Studio opens at the backup import step with the name shown.
  • &name=my-export.tar.gz → supplied name is used.
  • http:// or file:// URL → rejected with "Please check the link and try again."
  • Unsupported extension (.txt) → same error dialog.
  • Unreachable host → connection-specific error; "Open Studio Logs" works.
  • Minimized Studio → window restored/focused before the modal opens.
  • npm test -- apps/studio/src/lib/deeplink/tests/import-backup.test.ts passes.

Related issues

## 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.
@borkweb borkweb force-pushed the feature/import-backup branch from e37c09a to 230cc95 Compare May 26, 2026 11:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant