diff --git a/README.md b/README.md
index 91e59b2f..0d70362c 100644
--- a/README.md
+++ b/README.md
@@ -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
@@ -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": }` 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": }` 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=&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/`). 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=&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/`). 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
@@ -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
diff --git a/plugin/composer.json b/plugin/composer.json
index 28845f56..98eec8ba 100644
--- a/plugin/composer.json
+++ b/plugin/composer.json
@@ -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": [
diff --git a/plugin/composer.lock b/plugin/composer.lock
index dd243a08..4048e695 100644
--- a/plugin/composer.lock
+++ b/plugin/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "4b89aa46ef6e3fecde290380e8e01762",
+ "content-hash": "b0f08b481b4e392fc2bd7939f2b63053",
"packages": [
{
"name": "automattic/jetpack-a8c-mc-stats",
@@ -650,16 +650,16 @@
},
{
"name": "wp-php-toolkit/reprint-server",
- "version": "v0.10.13",
+ "version": "v0.11.0",
"source": {
"type": "git",
- "url": "https://github.com/wp-php-toolkit/reprint-server",
- "reference": "2b298f2af6d15bc34d51ad4bc965d4a4c51765ea"
+ "url": "https://github.com/wp-php-toolkit/reprint-server.git",
+ "reference": "00e4dd6812dad5fcd5aed85878322c1f08023af5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/wp-php-toolkit/reprint-server/zipball/2b298f2af6d15bc34d51ad4bc965d4a4c51765ea",
- "reference": "2b298f2af6d15bc34d51ad4bc965d4a4c51765ea",
+ "url": "https://api.github.com/repos/wp-php-toolkit/reprint-server/zipball/00e4dd6812dad5fcd5aed85878322c1f08023af5",
+ "reference": "00e4dd6812dad5fcd5aed85878322c1f08023af5",
"shasum": ""
},
"require": {
@@ -684,7 +684,11 @@
"GPL-2.0-or-later"
],
"description": "Streaming export engine used by the Reprint Server WordPress plugin",
- "time": "2026-10-01T13:01:25+00:00"
+ "support": {
+ "issues": "https://github.com/wp-php-toolkit/reprint-server/issues",
+ "source": "https://github.com/wp-php-toolkit/reprint-server/tree/v0.11.0"
+ },
+ "time": "2026-10-08T19:38:46+00:00"
}
],
"packages-dev": [],
diff --git a/plugin/connection/class-connect-page.php b/plugin/connection/class-connect-page.php
index d4bd8a45..870c9833 100644
--- a/plugin/connection/class-connect-page.php
+++ b/plugin/connection/class-connect-page.php
@@ -232,7 +232,7 @@ public function render_disconnect_link() {
-
+
get_current_user_id() ) );
@@ -270,8 +248,6 @@ private static function discard_credential( $option, $hash_option, $event ) {
/**
* Stores a shared secret together with its salt-keyed hash.
*
- * Saving a secret does not touch the export window.
- *
* @param string $secret The new secret.
* @return bool Whether the secret and a matching hash are now stored.
*/
@@ -295,7 +271,7 @@ public static function store_secret( $secret ) {
* key stored before.
*
* Takes the one-line form PublicKeyServer::assert_valid_public_key()
- * returns. Saving a key touches neither the secret nor the export window.
+ * returns. Saving a key leaves any secret in place.
*
* @param string $public_key The one-line public key.
* @return bool Whether the key and a matching hash are now stored.
@@ -320,8 +296,8 @@ public static function store_public_key( $public_key ) {
* stores its own salt in wp_options, where whoever can write the credential
* can read it, so the hashes add no protection there.
*
- * The option name prefixes the message, so copying the window timestamp
- * and its hash into the secret options does not make a secret that
+ * The option name prefixes the message, so copying one credential and
+ * its hash into the other's options does not make a credential that
* verifies.
*
* @param string $hash_option The option the hash is stored in.
@@ -349,60 +325,20 @@ private static function credential_hash_matches( $hash_option, $value ) {
return hash_equals( self::compute_credential_hash( $hash_option, $value ), $stored_hash );
}
- /**
- * Whether the current export window is open.
- *
- * @param int|null $now Unix time to compare against, or null for now.
- * @return bool
- */
- public static function is_export_window_open( $now = null ) {
- $enabled_at = (int) get_option( self::ENABLED_OPTION, 0 );
- $now = null === $now ? time() : (int) $now;
-
- return $enabled_at > 0
- && $enabled_at <= $now + self::CLOCK_SKEW
- && ( $now - $enabled_at ) <= HOUR_IN_SECONDS
- && self::credential_hash_matches( self::ENABLED_HASH_OPTION, $enabled_at );
- }
-
- /**
- * Opens the export window by stamping the enabled option with the current
- * time and hashing the stamp.
- *
- * @return int The unix time the window was opened at.
- */
- public static function open_export_window() {
- $now = time();
-
- // Value then hash: a crash between them leaves a mismatch, which reads
- // as closed. Each skips an unchanged value, so a busy client costs at
- // most two writes per elapsed second.
- self::write_option( self::ENABLED_OPTION, $now );
- self::write_option( self::ENABLED_HASH_OPTION, self::compute_credential_hash( self::ENABLED_HASH_OPTION, $now ) );
-
- return $now;
- }
-
- /**
- * Closes the export window.
- */
- public static function close_export_window() {
- delete_option( self::ENABLED_OPTION );
- delete_option( self::ENABLED_HASH_OPTION );
- }
-
/**
* The state the screens render.
*
+ * The credential is the one this host verifies: the public key where
+ * Reprint requires keys, the secret elsewhere. The other one, if stored,
+ * counts for nothing.
+ *
* @return array {
- * @type bool $has_secret Whether a secret is stored.
- * @type bool $secret_valid Whether its hash matches under the current salt.
- * @type bool $has_public_key Whether a public key is stored.
- * @type bool $public_key_valid Whether its hash matches under the current salt.
- * @type bool $has_credential Whether either is stored.
- * @type bool $credential_valid Whether either is valid.
- * @type bool $window_open Whether the export window is open.
- * @type int|null $window_expires_at Unix time the window lapses, or null when closed.
+ * @type bool $has_secret Whether a secret is stored.
+ * @type bool $secret_valid Whether its hash matches under the current salt.
+ * @type bool $has_public_key Whether a public key is stored.
+ * @type bool $public_key_valid Whether its hash matches under the current salt.
+ * @type bool $has_credential Whether the credential is stored.
+ * @type bool $credential_valid Whether it is valid, which turns the exporter on.
* }
*/
public static function get_state() {
@@ -412,17 +348,15 @@ public static function get_state() {
$public_key = get_option( self::PUBLIC_KEY_OPTION, '' );
$has_public_key = is_string( $public_key ) && '' !== $public_key;
$public_key_valid = $has_public_key && self::credential_hash_matches( self::PUBLIC_KEY_HASH_OPTION, $public_key );
- $window_open = self::is_export_window_open();
+ $key_auth = Utils::key_auth_required();
return array(
- 'has_secret' => $has_secret,
- 'secret_valid' => $secret_valid,
- 'has_public_key' => $has_public_key,
- 'public_key_valid' => $public_key_valid,
- 'has_credential' => $has_secret || $has_public_key,
- 'credential_valid' => $secret_valid || $public_key_valid,
- 'window_open' => $window_open,
- 'window_expires_at' => $window_open ? (int) get_option( self::ENABLED_OPTION, 0 ) + HOUR_IN_SECONDS : null,
+ 'has_secret' => $has_secret,
+ 'secret_valid' => $secret_valid,
+ 'has_public_key' => $has_public_key,
+ 'public_key_valid' => $public_key_valid,
+ 'has_credential' => $key_auth ? $has_public_key : $has_secret,
+ 'credential_valid' => $key_auth ? $public_key_valid : $secret_valid,
);
}
@@ -446,9 +380,7 @@ public function handle_request() {
// Any origin: the client may run in a browser (Playground) from
// deployments we cannot know ahead of time, and origin is no boundary
// when every request must be signed anyway. Preflights come before
- // the signature check because browsers send them without credentials, and
- // before the window check so a client whose window has closed can
- // reach the 409 below.
+ // the signature check because browsers send them without credentials.
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
$request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( $_SERVER['REQUEST_METHOD'] ) : '';
if ( 'OPTIONS' === $request_method ) {
@@ -460,25 +392,16 @@ public function handle_request() {
return;
}
- // Without a valid signature a closed window answers nothing, so an idle
- // site stays indistinguishable from one that never had the feature.
- $state = self::get_state();
- $window_open = $state['window_open'];
-
+ // Without a credential the request answers nothing, so a site that was
+ // never set up stays indistinguishable from one without the feature.
+ $state = self::get_state();
if ( ! $state['has_credential'] ) {
- if ( ! $window_open ) {
- return;
- }
- $this->error( 503, 'Export not configured. Start the migration on WordPress.com.' );
return;
}
// A credential this class did not hash under the current salt is no
// credential at all, so it never reaches signature verification.
if ( ! $state['credential_valid'] ) {
- if ( ! $window_open ) {
- return;
- }
self::record_event( 'credential_hash_mismatch' );
$this->error( 503, 'Export credential invalidated: the stored credential does not match this site\'s salts. Start the migration again on WordPress.com.' );
return;
@@ -494,27 +417,14 @@ public function handle_request() {
$auth_failure = $this->verify_signature( $secret, $public_keys );
if ( null !== $auth_failure ) {
- if ( ! $window_open ) {
- return;
- }
- // Reprint's client maps the reason to its message, and reads
- // not_configured as an answer only from a 503.
- $status = RequestAuthenticator::REASON_NOT_CONFIGURED === $auth_failure['reason'] ? 503 : 403;
+ // Reprint's client maps the reason to its message, and reads the
+ // two not-set-up reasons as an answer only from a 503.
+ $unconfigured = array( RequestAuthenticator::REASON_NOT_CONFIGURED, RequestAuthenticator::REASON_NO_KEYS_ENROLLED );
+ $status = in_array( $auth_failure['reason'], $unconfigured, true ) ? 503 : 403;
$this->error( $status, $auth_failure['message'], $auth_failure['reason'] );
return;
}
- // Signature checks out, so say which state this is: still here, only
- // needing re-arming, rather than gone.
- if ( ! $window_open ) {
- $this->error( 409, 'Export window closed. Enable the exporter on the settings screen.' );
- return;
- }
-
- // An export spans many requests and can run past the hour, so keep the
- // window open while a client is working.
- self::open_export_window();
-
try {
$this->serve_export();
} catch ( \InvalidArgumentException $exception ) {
@@ -602,6 +512,9 @@ protected function send_cors_headers() {
/**
* Sends a JSON error response and terminates.
*
+ * The body carries Reprint's auth_version, without which its client
+ * reports the plugin as outdated.
+ *
* @param int $code HTTP status code.
* @param string $message Error description.
* @param string|null $reason Machine-readable reason code, or null for none.
@@ -612,8 +525,9 @@ protected function error( $code, $message, $reason = null ) {
'reason' => $message,
);
$body = array(
- 'error' => $message,
- 'code' => $code,
+ 'error' => $message,
+ 'code' => $code,
+ 'auth_version' => Utils::AUTH_VERSION,
);
if ( null !== $reason ) {
$context['reason_code'] = $reason;
diff --git a/plugin/reprint/class-manual-page.php b/plugin/reprint/class-manual-page.php
index 34785cf4..067f1953 100644
--- a/plugin/reprint/class-manual-page.php
+++ b/plugin/reprint/class-manual-page.php
@@ -1,9 +1,9 @@
authorize( self::SAVE_SECRET_ACTION );
+ if ( Utils::key_auth_required() ) {
+ $this->redirect_with_notice( 'secret_unsupported' );
+ }
+
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Checked in authorize().
$secret = sanitize_text_field( wp_unslash( $_POST[ self::SECRET_FIELD ] ?? '' ) );
@@ -240,31 +228,7 @@ public function handle_save_secret() {
}
/**
- * Turns the exporter on or off and redirects back with a result notice.
- */
- public function handle_save_enabled() {
- $this->authorize( self::SAVE_ENABLED_ACTION );
-
- $state = Exporter::get_state();
- if ( ! $state['credential_valid'] ) {
- $this->redirect_with_notice( 'not_configured' );
- }
-
- // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Checked in authorize().
- if ( isset( $_POST[ self::ENABLED_FIELD ] ) ) {
- Exporter::open_export_window();
- Exporter::record_event( 'window_opened', array( 'user_id' => get_current_user_id() ) );
- $this->redirect_with_notice( 'enabled' );
- }
-
- Exporter::close_export_window();
- Exporter::record_event( 'window_closed', array( 'user_id' => get_current_user_id() ) );
- $this->redirect_with_notice( 'disabled' );
- }
-
- /**
- * Removes the secret, turns the exporter off, and redirects back with a
- * result notice.
+ * Removes the secret and redirects back with a result notice.
*/
public function handle_discard_secret() {
$this->authorize( self::DISCARD_SECRET_ACTION );
@@ -315,8 +279,7 @@ public function handle_save_public_key() {
}
/**
- * Removes the public key, turns the exporter off, and redirects back with
- * a result notice.
+ * Removes the public key and redirects back with a result notice.
*/
public function handle_remove_public_key() {
$this->authorize( self::REMOVE_PUBLIC_KEY_ACTION );
@@ -346,10 +309,10 @@ private function authorize( $action ) {
/**
* Redirects back to the screen with a result code and exits.
*
- * @param string $result One of saved, unchanged, enabled, disabled,
- * discarded, not_configured, storage_failure,
- * key_saved, key_unchanged, key_removed,
- * key_invalid, key_unsupported,
+ * @param string $result One of saved, unchanged, discarded,
+ * not_configured, storage_failure,
+ * secret_unsupported, key_saved, key_unchanged,
+ * key_removed, key_invalid, key_unsupported,
* key_storage_failure.
*/
private function redirect_with_notice( $result ) {
@@ -358,9 +321,9 @@ private function redirect_with_notice( $result ) {
}
/**
- * Renders the screen: the secret form; the public key form and table; the
- * exporter toggle and export URL once a credential is valid; the blog ID
- * when connected.
+ * Renders the screen: the public key form and table where this host
+ * verifies keys, the secret form elsewhere; the export URL once a
+ * credential is valid; the blog ID when connected.
*/
public function render_page() {
if ( ! in_array( 'administrator', wp_get_current_user()->roles, true ) ) {
@@ -378,13 +341,14 @@ public function render_page() {
echo '