admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( Ajax_Driver::NONCE ), 'jobNonce' => wp_create_nonce( Ajax_Driver::JOB_NONCE ), 'loginUrl' => wp_login_url(), 'i18n' => array( 'relogin' => __( 'The site now runs the restored backup, and this browser is not logged in to it. The restore is finishing in the background. Log in with an administrator account from the backup, then come back to this page to keep it or roll it back.', 'accessnow-site-replica' ), 'login' => __( 'Log in', 'accessnow-site-replica' ), 'confirmCancel' => __( 'Cancel this job? A partial backup is kept but marked incomplete.', 'accessnow-site-replica' ), 'lost' => __( 'Lost contact with this site. Retrying…', 'accessnow-site-replica' ), 'stopped' => __( 'Stopped: this site stopped answering. Check your connection, then press Resume.', 'accessnow-site-replica' ), 'leave' => __( 'A job is running. It will pause if you leave this page (you can resume it later).', 'accessnow-site-replica' ), 'copied' => __( 'Copied', 'accessnow-site-replica' ), /* translators: %1$d: number of backups, %2$s: total size. */ 'selected' => __( '%1$d selected (%2$s)', 'accessnow-site-replica' ), /* translators: %1$d: number of backups, %2$s: total size. */ 'bulkConfirm' => __( 'Delete %1$d backups (%2$s) permanently?', 'accessnow-site-replica' ), 'bulkNewest' => __( 'This includes the newest backup of a site: the next backup of that site will download everything again.', 'accessnow-site-replica' ), 'from' => _x( 'from', 'pull backup from a site', 'accessnow-site-replica' ), 'expired' => __( 'This page lost its link to the job (it was opened again elsewhere). Reload the page to keep following it: the job keeps its progress.', 'accessnow-site-replica' ), /* translators: %s: time left, e.g. "5 minutes". */ 'eta' => __( 'about %s left', 'accessnow-site-replica' ), /* translators: %s: how long, e.g. "40 seconds". */ 'working' => __( 'still working, %s since the last update', 'accessnow-site-replica' ), 'planTitle' => __( 'Review the restore', 'accessnow-site-replica' ), 'planIntro' => __( 'Nothing on this site has changed yet. The backup is imported into temporary tables first; this site is only switched at the end, and you can roll back afterwards.', 'accessnow-site-replica' ), 'planFrom' => __( 'From', 'accessnow-site-replica' ), 'planInto' => __( 'Into (this site)', 'accessnow-site-replica' ), 'planBackup' => __( 'Backup made', 'accessnow-site-replica' ), 'planDatabase' => __( 'Database', 'accessnow-site-replica' ), 'planFiles' => __( 'Files', 'accessnow-site-replica' ), 'planReplace' => __( 'Addresses and paths that will be changed', 'accessnow-site-replica' ), 'planMoved' => __( 'Tables that exist only on this site and will be removed (kept until you choose Keep):', 'accessnow-site-replica' ), 'planRemoved' => __( 'Plugins that exist only on this site and will be removed (Roll back returns them):', 'accessnow-site-replica' ), 'planKept' => __( 'Media and must-use plugins that exist only on this site are kept.', 'accessnow-site-replica' ), 'optKeepLogin' => __( 'Keep me logged in (carry my account over)', 'accessnow-site-replica' ), 'optDiscourage' => __( 'Discourage search engines from indexing this copy', 'accessnow-site-replica' ), 'optMail' => __( 'Block all outgoing email from this copy', 'accessnow-site-replica' ), 'optSsl' => __( 'Deactivate SSL-forcing plugins (this site uses http)', 'accessnow-site-replica' ), 'optGuid' => __( 'Leave post GUIDs unchanged (recommended when moving a live site)', 'accessnow-site-replica' ), 'optFiles' => __( 'Restore files (uploads, plugins, themes)', 'accessnow-site-replica' ), /* translators: %s: this site's host name. */ 'typeHost' => __( 'This replaces a live site. Type %s to confirm:', 'accessnow-site-replica' ), 'start' => __( 'Replace this site with the backup', 'accessnow-site-replica' ), 'keep' => __( 'Keep the restored site', 'accessnow-site-replica' ), 'rollback' => __( 'Roll back', 'accessnow-site-replica' ), 'openSite' => __( 'Open the site in a new tab', 'accessnow-site-replica' ), 'confirmKeep' => __( 'Keep the restored site and delete the previous version?', 'accessnow-site-replica' ), 'confirmRoll' => __( 'Put the previous site back exactly as it was?', 'accessnow-site-replica' ), 'rows' => __( 'rows', 'accessnow-site-replica' ), 'tables' => __( 'tables', 'accessnow-site-replica' ), /* translators: %s: size, e.g. 12 MB. */ 'uploadedN' => __( '%s uploaded', 'accessnow-site-replica' ), /* translators: %s: size, e.g. 12 MB. */ 'downloaded' => __( '%s downloaded', 'accessnow-site-replica' ), /* translators: 1: current step, 2: number of steps. */ 'step' => __( 'step %1$s of %2$s', 'accessnow-site-replica' ), /* translators: %s: number of files. */ 'file1' => _n( '%s file', '%s files', 1, 'accessnow-site-replica' ), /* translators: %s: number of files. */ 'fileN' => _n( '%s file', '%s files', 2, 'accessnow-site-replica' ), /* translators: %s: number of rows. */ 'row1' => _n( '%s row', '%s rows', 1, 'accessnow-site-replica' ), /* translators: %s: number of rows. */ 'rowN' => _n( '%s row', '%s rows', 2, 'accessnow-site-replica' ), /* translators: %s: number of warnings. */ 'warning1' => _n( '%s warning', '%s warnings', 1, 'accessnow-site-replica' ), /* translators: %s: number of warnings. */ 'warningN' => _n( '%s warning', '%s warnings', 2, 'accessnow-site-replica' ), /* translators: %s: number of unchanged files. */ 'unchangedN' => __( '%s unchanged', 'accessnow-site-replica' ), /* translators: %s: seconds. */ 'retryingIn' => __( 'retrying in %s s', 'accessnow-site-replica' ), 'status' => array( 'queued' => __( 'Starting', 'accessnow-site-replica' ), 'running' => __( 'Running', 'accessnow-site-replica' ), 'waiting' => __( 'Waiting to retry', 'accessnow-site-replica' ), 'paused' => __( 'Paused', 'accessnow-site-replica' ), 'failed' => __( 'Stopped with an error', 'accessnow-site-replica' ), 'cancelled' => __( 'Cancelled', 'accessnow-site-replica' ), 'completed' => __( 'Completed', 'accessnow-site-replica' ), 'awaiting' => __( 'Waiting for you', 'accessnow-site-replica' ), ), 'types' => array( 'pull' => __( 'Pull backup', 'accessnow-site-replica' ), 'clone' => __( 'Clone into this site', 'accessnow-site-replica' ), 'restore' => __( 'Restore', 'accessnow-site-replica' ), 'backup' => __( 'Back up this site', 'accessnow-site-replica' ), 'cloud' => __( 'Cloud backup', 'accessnow-site-replica' ), 'cloud_db' => __( 'Database backup to the cloud', 'accessnow-site-replica' ), 'import' => __( 'Download from the cloud', 'accessnow-site-replica' ), ), 'backgroundHint' => __( 'This backup runs on the server: you can close this page. You will find the result here and, if you chose so, by e-mail.', 'accessnow-site-replica' ), ), ) ); } /* ------------------------------------------------------------------------- * Pages * ---------------------------------------------------------------------- */ /** * Page wrapper start. * * @param string $title Title. */ public static function open( $title ) { echo '

' . esc_html( $title ) . '

'; try { // Only a folder that exists can be served; opening a page must not create it. if ( Storage::exists() && true === Storage::web_exposed() ) { echo '

' . esc_html__( 'The backup folder can be downloaded from the web on this server (it ignores the folder’s .htaccess protection). Ask your host to stop serving that folder, or set ACCESSNOW_REPLICA_STORAGE_DIR in wp-config.php to a folder the web server does not serve, before making backups here.', 'accessnow-site-replica' ) . '

'; } } catch ( \RuntimeException $e ) { echo '

' . esc_html( $e->getMessage() ) . '

'; } self::mail_guard_notice(); } /** * Says so while this copy is blocking its own outgoing email. * * A restore can install a guard that stops a copied site emailing the original's customers. * That is the right default for a copy, and a mystery if nobody says it is there — so it is * said, on this plugin's own pages, with the way to undo it. */ private static function mail_guard_notice() { if ( ! in_array( 'accessnow-replica-mail-guard.php', array_map( 'basename', wp_get_mu_plugins() ), true ) ) { return; } echo '

' . esc_html__( 'This copy is not sending email. A restore installed a guard so a copied site cannot email the original site’s people. Delete wp-content/mu-plugins/accessnow-replica-mail-guard.php to allow email again.', 'accessnow-site-replica' ) . '

'; } /** * Page wrapper end. */ public static function close() { echo '
'; } /** * Dashboard. */ public static function page_dashboard() { self::open( __( 'Site Replica', 'accessnow-site-replica' ) ); $active = Job_Store::active(); if ( $active ) { self::job_panel( $active ); } $sets = Set_Store::all(); ?>

' . esc_html__( 'Pulling is ON', 'accessnow-site-replica' ) . '' : '' . esc_html__( 'Pulling is off', 'accessnow-site-replica' ) . ''; ?>

' . esc_html__( 'Saved connections can no longer be decrypted (the security keys in wp-config.php changed). Add the connections again.', 'accessnow-site-replica' ) . '

'; } ?>

$conn ) : ?>

get( 'set_id' ) === $m['set_id']; $is_new = in_array( $m['set_id'], $newest, true ); ?>
>

' . esc_html( sprintf( __( '%s unchanged (linked)', 'accessnow-site-replica' ), number_format_i18n( (int) $m['files']['linked'] ) ) ) . ''; } } else { echo '—'; } ?>

$key ) : ?> 0 && $key['expires'] < time(); ?>

' . esc_html__( 'Revoked', 'accessnow-site-replica' ) . ''; } elseif ( $expired ) { echo '' . esc_html__( 'Expired', 'accessnow-site-replica' ) . ''; } else { echo $key['expires'] ? esc_html( wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), (int) $key['expires'] ) ) : esc_html__( 'Never', 'accessnow-site-replica' ); } ?>

' . esc_html__( 'Jobs', 'accessnow-site-replica' ) . ''; $jobs = Job_Store::recent( 15 ); if ( ! $jobs ) { echo '

' . esc_html__( 'No jobs yet.', 'accessnow-site-replica' ) . '

'; } foreach ( $jobs as $job ) { $summary = Job_Runner::summary( $job ); echo '
' . esc_html( self::type_label( $summary['type'] ) ) . ' · ' . esc_html( self::status_label( $summary['status'] ) ) . ' · ' . esc_html( wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $summary['created'] ) ) . ' ' . esc_html( $summary['id'] ) . ''; if ( $summary['error'] ) { echo '

' . esc_html( $summary['error'] ) . '

'; } echo '
' . esc_html( implode( "\n", Logger::job_tail( $job->id(), 200 ) ) ) . '
'; } echo '

' . esc_html__( 'Activity', 'accessnow-site-replica' ) . '

'; self::log_table( Logger::entries( Logger::site_dir() . 'activity.jsonl', 100 ) ); // On a site that is being pulled, the requests from the other site are logged here. $audit = Logger::entries( Logger::site_dir() . 'source-audit.jsonl', 100 ); if ( $audit ) { echo '

' . esc_html__( 'Pull requests from other sites', 'accessnow-site-replica' ) . '

'; self::log_table( $audit ); } self::close(); } /** * Settings. */ public static function page_settings() { self::open( __( 'Settings', 'accessnow-site-replica' ) ); $s = Settings::all(); ?>

__( 'Pull backup', 'accessnow-site-replica' ), 'clone' => __( 'Clone into this site', 'accessnow-site-replica' ), 'restore' => __( 'Restore', 'accessnow-site-replica' ), 'backup' => __( 'Back up this site', 'accessnow-site-replica' ), 'cloud' => __( 'Cloud backup', 'accessnow-site-replica' ), 'cloud_db' => __( 'Database backup to the cloud', 'accessnow-site-replica' ), 'import' => __( 'Download from the cloud', 'accessnow-site-replica' ), ); return $labels[ $type ] ?? ucfirst( (string) $type ); } /** * Live job panel (driven by admin.js). * * @param Job $job Job. */ public static function job_panel( Job $job ) { $token = Job_Store::reissue_token( $job ); $summary = Job_Runner::summary( $job ); printf( '
', esc_attr( $job->id() ), esc_attr( $token ), esc_attr( (string) wp_json_encode( $summary ) ) ); self::job_template(); } /** * Markup template for job panels. */ public static function job_template() { static $printed = false; if ( $printed ) { return; } $printed = true; ?> ' . esc_html__( 'Nothing logged yet.', 'accessnow-site-replica' ) . '

'; return; } echo ''; foreach ( $entries as $e ) { $context = isset( $e['context'] ) ? $e['context'] : array(); echo ''; } echo '
' . esc_html( wp_date( 'Y-m-d H:i:s', (int) $e['time'] ) ) . '' . esc_html( $e['type'] ) . '' . esc_html( $e['message'] ); if ( $context ) { echo ' ' . esc_html( (string) wp_json_encode( $context, JSON_UNESCAPED_SLASHES ) ) . ''; } echo '
'; } /* ------------------------------------------------------------------------- * Form handlers * ---------------------------------------------------------------------- */ /** * Redirects back with a notice. * * @param string $page Page slug. * @param string $message Message. * @param string $type success|error|warning. */ public static function back( $page, $message, $type = 'success' ) { set_transient( 'accessnow_replica_notice_' . get_current_user_id(), array( 'message' => $message, 'type' => $type, ), 120 ); wp_safe_redirect( admin_url( 'admin.php?page=' . $page ) ); exit; } /** * Shows a pending notice on our pages. */ public static function notices() { // Only on this plugin's own screens. The notice is the result of an action taken here and // is set immediately before redirecting back here, but if that redirect is interrupted it // would otherwise appear on whatever screen loads next — somebody else's. $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen || false === strpos( (string) $screen->id, self::MENU ) ) { return; } $notice = get_transient( 'accessnow_replica_notice_' . get_current_user_id() ); if ( ! $notice ) { return; } delete_transient( 'accessnow_replica_notice_' . get_current_user_id() ); wp_admin_notice( esc_html( $notice['message'] ), array( 'type' => $notice['type'], 'dismissible' => true, ) ); } /** * Enable or disable the source. */ public static function handle_source_toggle() { if ( ! current_user_can( Admin::CAP ) ) { wp_die( esc_html__( 'You are not allowed to do that.', 'accessnow-site-replica' ), 403 ); } check_admin_referer( 'accessnow_replica_source_toggle' ); $enable = ! empty( $_POST['enable'] ); Connection_Keys::set_enabled( $enable ); self::back( 'site-replica-source', $enable ? __( 'Pulling is on. Generate a key for the computer that will make backups.', 'accessnow-site-replica' ) : __( 'Pulling is off. Existing keys stop working until you turn it on again.', 'accessnow-site-replica' ) ); } /** * Create a key. */ public static function handle_key_create() { if ( ! current_user_can( Admin::CAP ) ) { wp_die( esc_html__( 'You are not allowed to do that.', 'accessnow-site-replica' ), 403 ); } check_admin_referer( 'accessnow_replica_key_create' ); $label = isset( $_POST['label'] ) ? sanitize_text_field( wp_unslash( $_POST['label'] ) ) : ''; $days = isset( $_POST['days'] ) ? absint( $_POST['days'] ) : 7; $ips = isset( $_POST['ips'] ) ? array_filter( array_map( 'trim', explode( ',', sanitize_text_field( wp_unslash( $_POST['ips'] ) ) ) ) ) : array(); foreach ( $ips as $ip ) { $addr = explode( '/', $ip, 2 )[0]; if ( false === @inet_pton( $addr ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged /* translators: %s: address. */ self::back( 'site-replica-source', sprintf( __( '"%s" is not a valid IP address or range.', 'accessnow-site-replica' ), $ip ), 'error' ); } } $key = Connection_Keys::create( $label, min( 3650, $days ), $ips ); set_transient( 'accessnow_replica_new_key_' . get_current_user_id(), $key['blob'], 600 ); if ( ! Connection_Keys::source_enabled() ) { Connection_Keys::set_enabled( true ); } self::back( 'site-replica-source', __( 'Key created and pulling turned on.', 'accessnow-site-replica' ) ); } /** * Revoke a key. */ public static function handle_key_revoke() { if ( ! current_user_can( Admin::CAP ) ) { wp_die( esc_html__( 'You are not allowed to do that.', 'accessnow-site-replica' ), 403 ); } check_admin_referer( 'accessnow_replica_key_revoke' ); Connection_Keys::revoke( isset( $_POST['kid'] ) ? sanitize_key( wp_unslash( $_POST['kid'] ) ) : '' ); self::back( 'site-replica-source', __( 'Key revoked. It stops working immediately.', 'accessnow-site-replica' ) ); } /** * Delete a key record. */ public static function handle_key_delete() { if ( ! current_user_can( Admin::CAP ) ) { wp_die( esc_html__( 'You are not allowed to do that.', 'accessnow-site-replica' ), 403 ); } check_admin_referer( 'accessnow_replica_key_delete' ); Connection_Keys::delete( isset( $_POST['kid'] ) ? sanitize_key( wp_unslash( $_POST['kid'] ) ) : '' ); self::back( 'site-replica-source', __( 'Key deleted.', 'accessnow-site-replica' ) ); } /** * Add a connection: parse the key, say hello, save. */ public static function handle_conn_add() { if ( ! current_user_can( Admin::CAP ) ) { wp_die( esc_html__( 'You are not allowed to do that.', 'accessnow-site-replica' ), 403 ); } check_admin_referer( 'accessnow_replica_conn_add' ); $blob = isset( $_POST['blob'] ) ? (string) wp_unslash( $_POST['blob'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Parsed and validated below. $parsed = Connections::parse_blob( preg_replace( '/\s+/', '', $blob ) ); if ( is_wp_error( $parsed ) ) { self::back( 'site-replica-pull', $parsed->get_error_message(), 'error' ); } if ( untrailingslashit( $parsed['url'] ) === untrailingslashit( home_url() ) ) { self::back( 'site-replica-pull', __( 'That key belongs to this site. Paste it into the site that will make the backup.', 'accessnow-site-replica' ), 'error' ); } try { $client = new Remote_Site( $parsed ); $result = $client->call( 'hello', array( 'client' => ACCESSNOW_REPLICA_VERSION ), array( 'ms' => 8000, 'bytes' => 1048576 ) ); $facts = Remote_Site::header( $result ); } catch ( Remote_Exception $e ) { self::back( 'site-replica-pull', $e->getMessage(), 'error' ); } $existed = null !== Connections::find_by_url( (string) $parsed['url'] ); $parsed['endpoints'] = $client->learn_endpoints( $facts['endpoints'] ?? array() ); $parsed['transport'] = $client->transport(); $parsed['clock_offset'] = $client->clock_offset(); $parsed['name'] = $facts['site']['name'] ?? $parsed['name']; $parsed['last_hello'] = time(); $parsed['facts'] = array( 'wp' => $facts['site']['wp_version'] ?? '', 'php' => $facts['php']['version'] ?? '', 'db' => $facts['db']['version'] ?? '', 'prefix' => $facts['site']['prefix'] ?? '', ); Connections::save( $parsed ); Logger::activity( 'pull', 'Connected to ' . $parsed['url'] ); self::back( 'site-replica-pull', $existed /* translators: %s: site. */ ? sprintf( __( 'New key saved for %s. You can pull a backup now.', 'accessnow-site-replica' ), $parsed['url'] ) /* translators: %s: site. */ : sprintf( __( 'Connected to %s. You can pull a backup now.', 'accessnow-site-replica' ), $parsed['url'] ) ); } /** * Remove a connection. */ public static function handle_conn_delete() { if ( ! current_user_can( Admin::CAP ) ) { wp_die( esc_html__( 'You are not allowed to do that.', 'accessnow-site-replica' ), 403 ); } check_admin_referer( 'accessnow_replica_conn_delete' ); Connections::delete( isset( $_POST['id'] ) ? sanitize_key( wp_unslash( $_POST['id'] ) ) : '' ); self::back( 'site-replica-pull', __( 'Connection removed.', 'accessnow-site-replica' ) ); } /** * Delete a backup set. */ public static function handle_set_delete() { if ( ! current_user_can( Admin::CAP ) ) { wp_die( esc_html__( 'You are not allowed to do that.', 'accessnow-site-replica' ), 403 ); } check_admin_referer( 'accessnow_replica_set_delete' ); $id = isset( $_POST['id'] ) ? sanitize_text_field( wp_unslash( $_POST['id'] ) ) : ''; $active = Job_Store::active(); if ( $active && $active->get( 'set_id' ) === $id ) { self::back( 'site-replica-backups', __( 'That backup is still being made. Cancel the job first.', 'accessnow-site-replica' ), 'error' ); } Set_Store::delete( $id ); self::back( 'site-replica-backups', __( 'Backup deleted.', 'accessnow-site-replica' ) ); } /** * Delete several backup sets. */ public static function handle_set_delete_bulk() { if ( ! current_user_can( Admin::CAP ) ) { wp_die( esc_html__( 'You are not allowed to do that.', 'accessnow-site-replica' ), 403 ); } check_admin_referer( 'accessnow_replica_set_delete_bulk' ); $ids = isset( $_POST['ids'] ) && is_array( $_POST['ids'] ) ? array_map( 'sanitize_text_field', wp_unslash( $_POST['ids'] ) ) : array(); $ids = array_values( array_unique( array_filter( $ids, array( Set_Store::class, 'valid_id' ) ) ) ); if ( ! $ids ) { self::back( 'site-replica-backups', __( 'No backups were selected.', 'accessnow-site-replica' ), 'error' ); } @set_time_limit( 600 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, Squiz.PHP.DiscouragedFunctions.Discouraged -- Deleting large backups (tens of thousands of files) takes a while. $active = Job_Store::active(); $deleted = 0; $kept = 0; foreach ( $ids as $id ) { if ( $active && $active->get( 'set_id' ) === $id ) { ++$kept; // Still being made or restored. continue; } Set_Store::delete( $id ); ++$deleted; } /* translators: %d: number of backups. */ $message = sprintf( _n( '%d backup deleted.', '%d backups deleted.', $deleted, 'accessnow-site-replica' ), $deleted ); if ( $kept ) { $message .= ' ' . __( 'One backup was kept because a job is using it.', 'accessnow-site-replica' ); } self::back( 'site-replica-backups', $message ); } /** * Save settings. */ public static function handle_settings_save() { if ( ! current_user_can( Admin::CAP ) ) { wp_die( esc_html__( 'You are not allowed to do that.', 'accessnow-site-replica' ), 403 ); } check_admin_referer( 'accessnow_replica_settings_save' ); Settings::save( wp_unslash( $_POST ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitised per field in Settings::save(). self::back( 'site-replica-settings', __( 'Settings saved.', 'accessnow-site-replica' ) ); } /* ------------------------------------------------------------------------- * Helpers * ---------------------------------------------------------------------- */ /** * "5 minutes ago". * * @param int $time Timestamp. */ public static function ago( $time ) { /* translators: %s: time difference. */ return sprintf( __( '%s ago', 'accessnow-site-replica' ), human_time_diff( $time ) ); } /** * Display date of a set. * * @param array $m Manifest. */ private static function set_date( array $m ) { $time = strtotime( (string) ( $m['created_at'] ?? '' ) ); return $time ? wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $time ) : $m['set_id']; } /** * Storage folder for display. */ private static function storage_display() { if ( ! Storage::exists() ) { return __( '(not created yet: a private folder in wp-content, made when the first backup starts)', 'accessnow-site-replica' ); } try { return untrailingslashit( Storage::dir() ); } catch ( \RuntimeException $e ) { return __( '(unavailable)', 'accessnow-site-replica' ); } } /** * Human status. * * @param string $status Status. */ public static function status_label( $status ) { $labels = array( 'queued' => __( 'Starting', 'accessnow-site-replica' ), 'running' => __( 'Running', 'accessnow-site-replica' ), 'waiting' => __( 'Waiting to retry', 'accessnow-site-replica' ), 'paused' => __( 'Paused', 'accessnow-site-replica' ), 'failed' => __( 'Stopped with an error', 'accessnow-site-replica' ), 'cancelled' => __( 'Cancelled', 'accessnow-site-replica' ), 'completed' => __( 'Completed', 'accessnow-site-replica' ), ); return isset( $labels[ $status ] ) ? $labels[ $status ] : $status; } }