Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 15 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Source for the [Migrate to WordPress.com](https://wordpress.org/plugins/wpcom-mi
- `plugin/connection/` — `Connection` (package setup, connect, disconnect), `Connect_Page` (the connection section of the screen), `bootstrap.php`.
- `bin/build.sh` — writes `build/wpcom-migration/` and `build/wpcom-migration.zip`.
- `bin/check-autoload-manifest.php` — asserts which classes the ZIP publishes through the Jetpack autoloader.
- `tests/e2e/` — Playground blueprints and request scripts: one scenario per credential state, one that drives the Reprint migration screen, two that provision through the REST routes (with a public key, and with a shared secret on a host made to look like it lacks OpenSSL), one that drives the WordPress.com connection, one that checks the admin menu, and one that checks the menu on a network.
- `tests/e2e/` — Playground blueprints and request scripts: one scenario per credential state, two that drive the screens (with a public key, and with a shared secret), two that provision through the REST routes (with a public key, and with a shared secret on a host made to look like it lacks OpenSSL), one that drives the WordPress.com connection, one that checks the admin menu, and one that checks the menu on a network.

## Build

Expand All @@ -24,44 +24,41 @@ The build runs `composer install` in `plugin/`, copies the tree to a staging dir

To activate a source checkout directly (without a build), run `composer install --no-dev --working-dir=plugin` first; without `plugin/vendor/` the plugin activates but the exporter and the WordPress.com connection are absent.

## The Reprint migration screen
## The screens

`wp-admin/admin.php?page=wpcom-migration-status` (the plugin's one menu entry; `manage_options` to view; the `administrator` role to connect, disconnect, or provision through the routes; single-site only). The screen shows one mode at a time:
`wp-admin/admin.php?page=wpcom-migration-status` is the Reprint migration screen. It tells the administrator where the site stands in the migration and offers the next step: connect the site to WordPress.com or continue on WordPress.com. Viewing it needs `manage_options`. Connecting, disconnecting, and provisioning through the routes need the `administrator` role. It works on single sites only, and shows one mode at a time:

| Mode | When | Shows |
|---|---|---|
| The exporter runs on single sites only | Multisite | Nothing |
| The export credential no longer matches this site | The site's salts changed | *Connect WordPress.com*, or *Go to WordPress.com* and *Disconnect* when connected |
| Pill: *Site ready to migrate* | The exporter is on | *Go to WordPress.com* |
| Pill: *Connected to WordPress.com* | Logged in; the exporter is off | *Go to WordPress.com*, a *Disconnect* link |
| Pill: *Set up by WordPress.com* | Provisioned with an application password; the exporter is off | *Go to WordPress.com* |
| Pill: *Site ready to migrate* | A credential is stored, so the exporter is on | *Go to WordPress.com* |
| Pill: *Connected to WordPress.com* | Logged in; no credential yet | *Go to WordPress.com*, a *Disconnect* link |
| Log in with your WordPress.com account to connect this site | Fresh site | *Connect WordPress.com* |

`Settings_Page::mode()` picks the first that fits, top to bottom. *Go to WordPress.com* links to `https://my.wordpress.com/sites`; the `wpcom_migration_continue_url` filter changes that.

Every activation sends the administrator to the main screen on their next wp-admin page load, except a bulk activation, which stays on the plugin list.
`wp-admin/admin.php?page=wpcom-migration-manual` sets up Reprint by hand; see [Setting up Reprint manually](#setting-up-reprint-manually).

The old BlogVault screen at `wp-admin/admin.php?page=wpcom-migration` is no longer in the sidebar. It stays reachable at its URL.
`wp-admin/admin.php?page=wpcom-migration` is the BlogVault screen the plugin had before Reprint.

Until the WordPress.com side of Reprint ships, WordPress.com decides which of the two screens is main: `Main_Screen` asks `GET https://public-api.wordpress.com/wpcom/v2/migration-plugin-config` for `{ "main_screen": "reprint" | "blogvault", "ttl": <seconds> }` on the first wp-admin page load after its stored answer (`wpcom_migration_main_screen`) expires. With `blogvault`, the old screen gets the sidebar row, the *Settings* link and the activation redirect, and the Reprint screen is reachable only at its URL. A failed fetch keeps the stored answer, or uses `blogvault` when there is none, and retries after five minutes. Networks never ask.
Until the WordPress.com side of Reprint ships, WordPress.com decides which of the Reprint and BlogVault screens is the main one: `Main_Screen` asks `GET https://public-api.wordpress.com/wpcom/v2/migration-plugin-config` for `{ "main_screen": "reprint" | "blogvault", "ttl": <seconds> }` on the first wp-admin page load after its stored answer (`wpcom_migration_main_screen`) expires. A failed fetch keeps the stored answer, or uses `blogvault` when there is none, and retries after five minutes. The main screen gets the plugin's one sidebar row, the *Settings* link on the plugin list, and the activation redirect: every activation sends the administrator to it on their next wp-admin page load, except a bulk activation, which stays on the plugin list. The other screen stays reachable at its URL, as does the manual one. A brand whitelabelled with `hide` or `hide_from_menu` gets no sidebar row for either screen.

On a network the old screen keeps its row in the network admin, and the *Settings* link and the activation redirect lead there; subsites get no row. A brand whitelabelled with `hide` or `hide_from_menu` takes away the Reprint screen's row too; the screen stays reachable at its URL.
Networks never ask. On a network the BlogVault screen keeps its row in the network admin, and the *Settings* link and the activation redirect lead there; subsites get no row.

## Setting up Reprint manually

`wp-admin/admin.php?page=wpcom-migration-manual` (`administrator` role, single-site only). Nothing links to it; support gives out the URL. It holds the export secret form and a *Remove secret* button; on a host with `openssl_verify()`, a public key textarea (*Enroll key*, PEM or one line, replacing any enrolled key) and a table with the enrolled key's id and a *Remove* button; the exporter toggle, the export URL and the WordPress.com blog ID. Removing either credential turns the exporter off and leaves the other in place. Deactivating the plugin disconnects and discards the secret and the exporter state.
`wp-admin/admin.php?page=wpcom-migration-manual` (`administrator` role, single-site only). It is a debugging screen, and nothing links to it. It holds the credential the host verifies: on a host with `openssl_verify()`, a public key textarea (*Enroll key*, PEM or one line, replacing any enrolled key) and a table with the enrolled key's id and a *Remove* button; elsewhere, the export secret form and a *Remove secret* button. Below it, the export URL and the WordPress.com blog ID. Deactivating the plugin disconnects and discards both credentials.

## How WordPress.com provisions the exporter

Two lanes end at the same three routes: `POST /wp-json/wpcom-migration/v1/reprint/install-public-key {"public_key"}` → `{ "key_id", "export_url" }`, `POST …/rotate-export-secret` → `{ "secret", "export_url" }` and `POST …/enable-export` → `{ "enabled_at", "export_url" }`. All require an administrator, authenticated either by core (an application password) or by a Jetpack user token, and all answer 501 on a network. Reprint transfers over https only.
WordPress.com reaches the site in one of two ways, described below: with an application password, or through a Jetpack connection. Either way it calls one of two routes: `POST /wp-json/wpcom-migration/v1/reprint/install-public-key {"public_key"}` → `{ "key_id", "export_url" }` or `POST …/rotate-export-secret` → `{ "secret", "export_url" }`. Both require an administrator, authenticated by core (an application password) or by a Jetpack user token, and both answer 501 on a network.

Which credential WordPress.com sets up depends on the host. Where `openssl_verify()` exists, Reprint signs with keys: WordPress.com installs its public key (PEM or one line of base64, RSA, 3072 bits or more; anything else is a 400 `wpcom_migration_invalid_public_key`), and `rotate-export-secret` answers 501 `wpcom_migration_secret_auth_unsupported`. Elsewhere it rotates the secret, and `install-public-key` answers 501 `wpcom_migration_key_auth_unsupported`. The site holds one key at a time; installing another replaces it. `enable-export` answers 409 `wpcom_migration_no_credential` until either credential is stored and valid. Export requests are verified by Reprint's `RequestAuthenticator`, which accepts whichever of the two the host supports.
Which credential WordPress.com sets up depends on the host. Where `openssl_verify()` exists, Reprint signs with keys: WordPress.com installs its public key (PEM or one line of base64, RSA, 3072 bits or more; anything else is a 400 `wpcom_migration_invalid_public_key`), and `rotate-export-secret` answers 501 `wpcom_migration_secret_auth_unsupported`. Elsewhere it rotates the secret, and `install-public-key` answers 501 `wpcom_migration_key_auth_unsupported`. The site holds one key at a time; installing another replaces it. The exporter is on while either credential is stored and valid; there is no separate switch and no time limit, since the plugin is removed once the migration is done. Export requests are verified by Reprint's `RequestAuthenticator`, which accepts whichever of the two the host supports.

**Application password** — where `GET /wp-json/` lists `authentication.application-passwords` (core: `is_ssl()` true and no plugin has switched them off). WordPress.com sends the administrator once to `wp-admin/authorize-application.php?app_name=Migrate+to+WordPress.com&app_id=<uuid>&success_url=…&reject_url=…`; core redirects to `success_url` with `site_url`, `user_login` and `password`. WordPress.com then checks the password with `GET /wp-json/wp/v2/users/me` (a 401 here means the server strips the `Authorization` header — Apache CGI without the rewrite rule WordPress 5.6 added; saving Permalinks regenerates it), installs and activates the plugin (`POST /wp-json/wp/v2/plugins {"slug":"wpcom-migration","status":"active"}`; an older copy is replaced by `PUT …/plugins/wpcom-migration/wpcom_migration {"status":"inactive"}`, `DELETE`, then the `POST`, since core has no update route), calls the two routes, exports, and revokes the password (`GET …/users/me/application-passwords/introspect`, then `DELETE …/application-passwords/<uuid>`). No login on the site.
**Application password** — where `GET /wp-json/` lists `authentication.application-passwords` (core: `is_ssl()` true and no plugin has switched them off). WordPress.com sends the administrator once to `wp-admin/authorize-application.php?app_name=Migrate+to+WordPress.com&app_id=<uuid>&success_url=…&reject_url=…`; core redirects to `success_url` with `site_url`, `user_login` and `password`. WordPress.com then checks the password with `GET /wp-json/wp/v2/users/me` (a 401 here means the server strips the `Authorization` header — Apache CGI without the rewrite rule WordPress 5.6 added; saving Permalinks regenerates it), installs and activates the plugin (`POST /wp-json/wp/v2/plugins {"slug":"wpcom-migration","status":"active"}`; an older copy is replaced by `PUT …/plugins/wpcom-migration/wpcom_migration {"status":"inactive"}`, `DELETE`, then the `POST`, since core has no update route), calls the route for the host's credential, exports, and revokes the password (`GET …/users/me/application-passwords/introspect`, then `DELETE …/application-passwords/<uuid>`). No login on the site.

**Jetpack connection** — everywhere else, including sites where Wordfence or another security plugin has disabled application passwords. The administrator installs the plugin from the plugin directory and presses *Connect WordPress.com* on the screen; the connection package registers the site and sends them to WordPress.com to authorize, then back. A site that already runs Jetpack is connected at once for the account that connected Jetpack — the connection is shared. WordPress.com then calls the routes signed with the user token.

Connection events fire `wpcom_migration_connection_event`, exporter events `wpcom_migration_reprint_export_event`; none carries a token, secret or signature.
**Jetpack connection** — everywhere else, including sites where Wordfence or another security plugin has disabled application passwords. The administrator installs the plugin from the plugin directory and presses *Connect WordPress.com* on the screen; the connection package registers the site and sends them to WordPress.com to authorize, then back. A site that already runs Jetpack is connected at once for the account that connected Jetpack — the connection is shared. WordPress.com then calls the route signed with the user token.

## Checks

Expand All @@ -79,11 +76,9 @@ Publishing to wp.org is not automated.

## Known limitations

- Running this plugin next to `reprint-server-wp` is unsupported. Both ship the same package; a request that loads classes from both copies can fatal on the package's path-required function files.
- The reprint client appends `&reprint-api` to any URL that lacks it. If `reprint-server-wp` is active and loads first, it answers on `reprint-api` before this plugin runs.
- Sites on placeholder salts get the write veto but not the salt binding: `wp_salt()` stores its own salt in `wp_options`, where whoever can write the credential can read it.
- The connection package requires PHP 7.4, so the plugin does too.
- Reprint transfers over https only; a site that cannot serve https cannot be migrated this way.
- The Jetpack connection package (`automattic/jetpack-connection`) requires PHP 7.4, so the plugin does too.
- `wp/v2/plugins` installs from wordpress.org by slug only, so the version with the exporter must be published there before WordPress.com can install it.

## Security
Expand Down
2 changes: 1 addition & 1 deletion plugin/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"automattic/jetpack-autoloader": "^6",
"automattic/jetpack-config": "^4.0",
"automattic/jetpack-connection": "^9.1",
"wp-php-toolkit/reprint-server": "0.10.13"
"wp-php-toolkit/reprint-server": "0.11.0"
},
"autoload": {
"classmap": [
Expand Down
18 changes: 11 additions & 7 deletions plugin/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion plugin/connection/class-connect-page.php
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ public function render_disconnect_link() {
<button type="submit" name="wpcom_migration_disconnect_submit" class="wpcom-migration-link wpcom-migration-link--quiet"><?php esc_html_e( 'Disconnect', 'wpcom-migration' ); ?></button>
</p>
<p class="wpcom-migration-description">
<?php esc_html_e( 'Removes this site\'s connection to WordPress.com.', 'wpcom-migration' ); ?>
<?php echo esc_html( wptexturize( __( 'Removes this site\'s connection to WordPress.com.', 'wpcom-migration' ) ) ); ?>
</p>
</form>
<?php
Expand Down
Loading
Loading