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
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,20 @@ To activate a source checkout directly (without a build), run `composer install

## The Reprint migration screen

`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). It carries the old main screen's design: the WordPress mark, a serif heading and one centred column. The screen shows one mode at a time — one sentence saying where the site stands, at most one button, and the links that fit:
`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:

| 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 | *Log in with WordPress.com*, or *Continue on WordPress.com* when connected |
| The exporter is on until *time* | A migration is running | *Continue on WordPress.com* and *Disconnect*, when connected |
| Connected as *email* | Logged in; the migration has not started | *Continue on WordPress.com*, a *Disconnect* link |
| This site is set up and ready | Provisioned with an application password | A *Log in with WordPress.com* link |
| Log in with your WordPress.com account | Fresh site | *Log in with WordPress.com* |
| 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* |
| 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. The *Continue* link's target is filtered by `wpcom_migration_continue_url`.
`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.

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

Expand All @@ -57,7 +59,7 @@ Which credential WordPress.com sets up depends on the host. Where `openssl_verif

**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.

**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 *Log in with 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.
**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.

Expand Down
57 changes: 18 additions & 39 deletions plugin/connection/class-connect-page.php
Original file line number Diff line number Diff line change
Expand Up @@ -180,25 +180,23 @@ private function redirect_with_notice( $result, $code = '' ) {
}

/**
* Renders the login form: the screen's primary button, or a plain link.
*
* @param bool $primary Whether the submit is the primary button.
* Renders the login form, with the terms it agrees to.
*/
public function render_connect_button( $primary ) {
public function render_connect_button() {
?>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<input type="hidden" name="action" value="<?php echo esc_attr( self::CONNECT_ACTION ); ?>" />
<?php wp_nonce_field( self::CONNECT_ACTION ); ?>
<p>
<button type="submit" name="wpcom_migration_connect_submit" class="<?php echo $primary ? 'wpcom-migration-button' : 'wpcom-migration-link'; ?>">
<?php esc_html_e( 'Log in with WordPress.com', 'wpcom-migration' ); ?>
<button type="submit" name="wpcom_migration_connect_submit" class="wpcom-migration-button">
<?php esc_html_e( 'Connect WordPress.com', 'wpcom-migration' ); ?>
</button>
</p>
<p class="wpcom-migration-description">
<?php
printf(
/* translators: %s: link to the WordPress.com terms of service. */
esc_html__( 'By connecting you agree to the %s.', 'wpcom-migration' ),
esc_html__( 'By connecting, you agree to the %s', 'wpcom-migration' ),
'<a href="https://wordpress.com/tos/" target="_blank" rel="noreferrer">' . esc_html__( 'WordPress.com Terms of Service', 'wpcom-migration' ) . '</a>'
);
?>
Expand All @@ -208,16 +206,15 @@ public function render_connect_button( $primary ) {
}

/**
* Renders the link to WordPress.com: the screen's primary button, or a
* plain link. It starts nothing on this site.
*
* @param bool $primary Whether it is the primary button.
* Renders the link to WordPress.com as the screen's button. It starts
* nothing on this site.
*/
public function render_continue_button( $primary ) {
public function render_continue_button() {
?>
<p>
<a<?php echo $primary ? ' class="wpcom-migration-button"' : ''; ?> target="_top" href="<?php echo esc_url( self::continue_url() ); ?>">
<?php esc_html_e( 'Continue on WordPress.com', 'wpcom-migration' ); ?>
<a class="wpcom-migration-button" target="_top" href="<?php echo esc_url( self::continue_url() ); ?>">
<?php esc_html_e( 'Go to WordPress.com', 'wpcom-migration' ); ?>
<svg class="wpcom-migration-button__icon" width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true" focusable="false"><path d="M1.5 8.5l7-7M3 1.5h5.5V7" /></svg>
</a>
</p>
<?php
Expand All @@ -232,30 +229,15 @@ public function render_disconnect_link() {
<input type="hidden" name="action" value="<?php echo esc_attr( self::DISCONNECT_ACTION ); ?>" />
<?php wp_nonce_field( self::DISCONNECT_ACTION ); ?>
<p>
<span class="wpcom-migration-description">
<button type="submit" name="wpcom_migration_disconnect_submit" class="wpcom-migration-link wpcom-migration-link--delete"><?php esc_html_e( 'Disconnect', 'wpcom-migration' ); ?></button>
<?php esc_html_e( 'Removes this site\'s WordPress.com connection and the export secret WordPress.com installed.', 'wpcom-migration' ); ?>
</span>
<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' ); ?>
</p>
</form>
<?php
}

/**
* The WordPress.com email address of the connected user, when the package
* has it.
*
* @return string|null
*/
public static function connected_email() {
$user_data = Connection::connected_wpcom_user();
if ( is_array( $user_data ) && ! empty( $user_data['email'] ) ) {
return (string) $user_data['email'];
}

return null;
}

/**
* Where the user goes to start the migration. A plain link; it starts
* nothing on this site.
Expand All @@ -264,14 +246,11 @@ public static function connected_email() {
*/
public static function continue_url() {
/**
* Filters the WordPress.com URL the connected screen links to.
* Filters the WordPress.com URL the screen's button links to.
*
* @param string $url Default: the WordPress.com site-migration setup, with this site as the source.
* @param string $url Default: the user's sites on WordPress.com.
*/
return apply_filters(
'wpcom_migration_continue_url',
'https://wordpress.com/setup/site-migration?from=' . rawurlencode( home_url() )
);
return apply_filters( 'wpcom_migration_continue_url', 'https://my.wordpress.com/sites' );
}

/**
Expand Down
111 changes: 47 additions & 64 deletions plugin/reprint/class-settings-page.php
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ public function enqueue_assets( $hook_suffix ) {
wp_enqueue_style(
self::STYLE_HANDLE,
plugins_url( 'reprint/settings-page.css', $this->plugin_file ),
array( 'wpcom-migration-variables', 'wpcom-migration-fonts', 'dashicons' ),
array( 'wpcom-migration-variables', 'wpcom-migration-fonts' ),
$version
);
}
Expand All @@ -220,8 +220,7 @@ public function remove_admin_notices() {
}

/**
* Renders the screen: the old main screen's design, with the mode where
* that screen had its email form.
* Renders the screen: one centred column holding the mode.
*/
public function render_page() {
if ( ! current_user_can( 'manage_options' ) ) {
Expand All @@ -238,17 +237,9 @@ public function render_page() {
echo '</div>';
}
?>
<header class="wpcom-migration-header">
<div class="wpcom-migration-header__wpcom-logo">
<span class="dashicons dashicons-wordpress-alt" aria-hidden="true"></span>
</div>
</header>

<div class="wpcom-migration-container">
<main class="wpcom-migration-content">
<h1><?php esc_html_e( 'Migrate your site to WordPress.com', 'wpcom-migration' ); ?></h1>
<p><?php esc_html_e( 'Get ready for better speed, security, and support. WordPress.com copies your posts, pages, media and settings across for you.', 'wpcom-migration' ); ?></p>
<?php $this->render_mode( $mode, $state, $user_connected ); ?>
<?php $this->render_mode( $mode, $user_connected ); ?>
</main>
</div>
<?php
Expand Down Expand Up @@ -287,111 +278,103 @@ public static function mode( array $state, $user_connected ) {
}

/**
* Renders the mode: one sentence stating where the site stands, and the
* controls that fit. Connection controls render only when a section is
* attached.
* Renders the mode: a status pill where one fits, the heading, where the
* site stands, and the controls that fit. Connection controls render only
* when a section is attached.
*
* @param string $mode One of the MODE_* constants.
* @param array $state Exporter::get_state().
* @param bool $user_connected Whether the current user is connected to WordPress.com.
*/
private function render_mode( $mode, array $state, $user_connected ) {
private function render_mode( $mode, $user_connected ) {
$section = $this->connection_section;

switch ( $mode ) {
case self::MODE_BLOCKED:
$this->render_pill_and_heading();
$this->render_sentence( __( 'The exporter runs on single sites only, so this site can\'t be migrated from here.', 'wpcom-migration' ) );
return;

case self::MODE_BROKEN:
$this->render_pill_and_heading();
$this->render_sentence( __( 'The export credential no longer matches this site — its security salts changed. Start the migration again on WordPress.com.', 'wpcom-migration' ) );
if ( null !== $section && $user_connected ) {
$section->render_continue_button( true );
$section->render_continue_button();
$section->render_disconnect_link();
} elseif ( null !== $section ) {
$section->render_connect_button( true );
$section->render_connect_button();
}
return;

case self::MODE_READY:
$this->render_sentence(
sprintf(
/* translators: %s: time of day. */
__( 'The exporter is on until %s. The migration runs from WordPress.com; each export keeps it on for another hour.', 'wpcom-migration' ),
self::window_closes_at( $state )
)
);
if ( null !== $section && $user_connected ) {
$section->render_continue_button( false );
$section->render_disconnect_link();
$this->render_pill_and_heading( __( 'Site ready to migrate', 'wpcom-migration' ), 'info' );
$this->render_sentence( __( 'Your site is now ready to be copied by WordPress.com. Your site stays online and unchanged while it\'s copied.', 'wpcom-migration' ) );
$this->render_sentence( __( 'Keep this plugin active until the migration is done. We email you when it\'s finished.', 'wpcom-migration' ) );
if ( null !== $section ) {
$section->render_continue_button();
}
return;

case self::MODE_CONNECTED_WAITING:
$email = null !== $section ? \Automattic\WPCOM_Migration\Connect_Page::connected_email() : null;
if ( null !== $email ) {
$this->render_sentence_html(
sprintf(
/* translators: %s: WordPress.com account email address, in bold. */
esc_html__( 'Connected as %s. WordPress.com sets up the exporter when the migration starts.', 'wpcom-migration' ),
'<strong>' . esc_html( $email ) . '</strong>'
)
);
} else {
$this->render_sentence( __( 'Connected to WordPress.com. WordPress.com sets up the exporter when the migration starts.', 'wpcom-migration' ) );
}
$this->render_pill_and_heading( __( 'Connected to WordPress.com', 'wpcom-migration' ), 'success' );
$this->render_ready_to_migrate();
if ( null !== $section ) {
$section->render_continue_button( true );
$section->render_continue_button();
$section->render_disconnect_link();
}
return;

case self::MODE_PROVISIONED_WAITING:
$this->render_sentence_html(
'<strong>' . esc_html__( 'This site is set up and ready.', 'wpcom-migration' ) . '</strong> '
. esc_html__( 'The exporter stays off until the migration starts on WordPress.com.', 'wpcom-migration' )
);
$this->render_pill_and_heading( __( 'Set up by WordPress.com', 'wpcom-migration' ), 'info' );
$this->render_ready_to_migrate();
if ( null !== $section ) {
$section->render_connect_button( false );
$section->render_continue_button();
}
return;

case self::MODE_NEEDS_CONNECTING:
default:
$this->render_sentence( __( 'Log in with your WordPress.com account so WordPress.com can read this site and migrate it. Nothing is copied until you start the migration there.', 'wpcom-migration' ) );
$this->render_pill_and_heading();
$this->render_sentence( __( 'WordPress.com copies your posts, pages, media, themes, plugins, and settings. Your site stays online and unchanged while we copy it.', 'wpcom-migration' ) );
$this->render_sentence( __( 'Log in with your WordPress.com account to connect this site. Nothing is copied until you start the migration on WordPress.com.', 'wpcom-migration' ) );
if ( null !== $section ) {
$section->render_connect_button( true );
$section->render_connect_button();
}
return;
}
}

/**
* Renders the mode's one sentence.
* Renders the heading, with a status pill above it when given one.
*
* @param string $text Plain text.
* @param string $pill Pill text, or '' for none.
* @param string $tone success (green) or info (blue).
*/
private function render_sentence( $text ) {
$this->render_sentence_html( esc_html( $text ) );
private function render_pill_and_heading( $pill = '', $tone = 'info' ) {
if ( '' !== $pill ) {
echo '<p class="wpcom-migration-pill wpcom-migration-pill--' . esc_attr( $tone ) . '">' . esc_html( $pill ) . '</p>';
}
echo '<h1>' . esc_html__( 'Migrate your site to WordPress.com', 'wpcom-migration' ) . '</h1>';
}

/**
* Renders the mode's one sentence from markup the caller has escaped.
*
* @param string $html Escaped HTML.
* Renders the sentence the two waiting modes share.
*/
private function render_sentence_html( $html ) {
echo '<p class="wpcom-migration-guidance">' . $html . '</p>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped by the caller.
private function render_ready_to_migrate() {
$this->render_sentence(
sprintf(
/* translators: %s: the site's address, without the scheme. */
__( '%s is ready to migrate. The migration runs from WordPress.com, so there\'s nothing else to do here.', 'wpcom-migration' ),
untrailingslashit( preg_replace( '#^https?://#i', '', home_url() ) )
)
);
}

/**
* The time of day the exporter turns itself off, in the site's format.
* Renders one paragraph of the mode.
*
* @param array $state Exporter::get_state(), with the window open.
* @return string
* @param string $text Plain text.
*/
private static function window_closes_at( array $state ) {
$expires = $state['window_expires_at'] + (int) ( get_option( 'gmt_offset' ) * HOUR_IN_SECONDS );
return date_i18n( get_option( 'time_format' ), $expires );
private function render_sentence( $text ) {
echo '<p class="wpcom-migration-guidance">' . esc_html( $text ) . '</p>';
}
}
Loading
Loading