ready();
$schedule = Destinations::schedule();
$about = null;
$error = '';
if ( $connected ) {
try {
$about = $dest->about();
} catch ( Remote_Exception $e ) {
$error = $e->getMessage();
}
}
if ( Secrets::is_unreadable( Destinations::OPTION ) || Secrets::is_unreadable( Google_Drive::TOKEN_OPTION ) ) {
echo '
' . esc_html__( 'The saved cloud storage settings can no longer be decrypted (the security keys in wp-config.php changed). Set them up again.', 'accessnow-site-replica' ) . '
';
}
?>
label() ) );
?>
'own' ) );
?>
' . esc_html__( 'Google Cloud console', 'accessnow-site-replica' ) . ''
);
?>
__( 'Saved', 'accessnow-site-replica' ),
'failed' => __( 'Failed', 'accessnow-site-replica' ),
'cancelled' => __( 'Cancelled', 'accessnow-site-replica' ),
'skipped' => __( 'Skipped', 'accessnow-site-replica' ),
);
echo '' . esc_html( $labels[ $last['status'] ] ?? $last['status'] ) . ' ';
/* translators: %s: time ago. */
echo esc_html( sprintf( __( 'Last background backup: %s', 'accessnow-site-replica' ), Admin::ago( (int) $last['time'] ) ) );
if ( 'completed' === $last['status'] && ! empty( $last['bytes'] ) ) {
echo ' · ' . esc_html( size_format( (int) $last['bytes'] ) );
}
echo '
';
if ( '' !== (string) $last['message'] && 'completed' !== $last['status'] ) {
echo '' . esc_html( $last['message'] ) . '
';
}
}
$next = wp_next_scheduled( Cron_Driver::HOOK_SCHEDULE );
if ( $next ) {
/* translators: %s: date and time. */
echo '' . esc_html( sprintf( __( 'Next scheduled backup: %s', 'accessnow-site-replica' ), wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $next ) ) ) . '
';
}
}
/**
* Schedule form.
*
* @param array $s Schedule.
* @param bool $connected Drive connected.
*/
private static function schedule_form( array $s, $connected ) {
global $wp_locale;
?>
find( $drive->top(), $drive->root_name(), true );
if ( $root ) {
foreach ( $drive->children( $root['id'], true ) as $f ) {
$sites[ (string) $f['name'] ] = (string) $f['id'];
}
}
} catch ( Remote_Exception $e ) {
echo '' . esc_html( $e->getMessage() ) . '
';
return;
}
$mine = Cloud::folder_path( home_url() )[1];
if ( '' === $shown || ! isset( $sites[ $shown ] ) ) {
$shown = $mine;
}
if ( count( $sites ) > 1 || ( $sites && ! isset( $sites[ $mine ] ) ) ) {
echo '';
foreach ( array_keys( $sites ) as $name ) {
$url = add_query_arg( 'site', rawurlencode( $name ), admin_url( 'admin.php?page=' . self::SLUG ) ) . '#srp-cloud-list';
echo $name === $shown ? '' . esc_html( $name ) . ' ' : '' . esc_html( $name ) . ' ';
echo $name === $mine ? ' (' . esc_html__( 'this site', 'accessnow-site-replica' ) . ') ' : '';
echo ' ';
}
echo '
';
}
if ( ! isset( $sites[ $shown ] ) ) {
echo '' . esc_html__( 'No backups of this site here yet.', 'accessnow-site-replica' ) . '
';
return;
}
try {
$list = Cloud::list_folder( $drive, $sites[ $shown ] );
} catch ( Remote_Exception $e ) {
echo '' . esc_html( $e->getMessage() ) . '
';
return;
}
if ( ! $list ) {
echo '' . esc_html__( 'No backups here.', 'accessnow-site-replica' ) . '
';
return;
}
$active = Job_Store::active();
?>
isset( S3::PROVIDERS[ $provider ] ) ? $provider : 'custom',
'region' => preg_replace( '/[^a-z0-9-]/', '', strtolower( $field( 'region' ) ) ),
'endpoint' => '' !== $endpoint ? esc_url_raw( preg_match( '#^https?://#', $endpoint ) ? $endpoint : 'https://' . $endpoint, array( 'http', 'https' ) ) : '',
'bucket' => preg_replace( '/[^a-z0-9.\-_]/', '', strtolower( $field( 'bucket' ) ) ),
'access_key' => preg_replace( '/\s+/', '', $field( 'access_key' ) ),
'folder' => '' !== $field( 'folder' ) ? str_replace( '/', '-', $field( 'folder' ) ) : 'Site Replica',
);
$secret = isset( $_POST['secret_key'] ) ? trim( (string) wp_unslash( $_POST['secret_key'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- A secret: any character is allowed; stored encrypted, never shown.
if ( '' !== $secret ) {
$fields['secret_key'] = $secret;
}
Destinations::update( 's3', $fields );
Destinations::set_active( 's3' );
$s3 = new S3();
if ( ! $s3->ready() ) {
Admin::back( self::SLUG, __( 'Saved. Fill in the bucket, keys and (for R2 and other providers) the endpoint.', 'accessnow-site-replica' ), 'warning' );
}
try {
$s3->about();
} catch ( Remote_Exception $e ) {
Admin::back( self::SLUG, __( 'Saved, but the test failed:', 'accessnow-site-replica' ) . ' ' . $e->getMessage(), 'error' );
}
Logger::activity( 'cloud', 'S3 storage set up: ' . $s3->label() );
Cron_Driver::reschedule();
$host = (string) wp_parse_url( $s3->base_url(), PHP_URL_HOST );
if ( 0 === strpos( $s3->base_url(), 'http://' ) && ! \AccessNow\Replica\Environment::is_local_host( $host ) ) {
/* translators: %s: storage and bucket. */
Admin::back( self::SLUG, sprintf( __( 'Connected to %s, but over plain http: backups travel unencrypted. Use an https:// endpoint if the provider offers one.', 'accessnow-site-replica' ), $s3->label() ), 'warning' );
}
/* translators: %s: storage and bucket. */
Admin::back( self::SLUG, sprintf( __( 'Connected to %s. Backups go there now.', 'accessnow-site-replica' ), $s3->label() ) );
}
/**
* Saves the Google app.
*/
public static function handle_drive_app() {
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_drive_app' );
$id = isset( $_POST['client_id'] ) ? trim( sanitize_text_field( wp_unslash( $_POST['client_id'] ) ) ) : '';
$secret = isset( $_POST['client_secret'] ) ? trim( sanitize_text_field( wp_unslash( $_POST['client_secret'] ) ) ) : '';
if ( ! preg_match( '/^[\w.-]+\.apps\.googleusercontent\.com$/', $id ) ) {
Admin::back( self::SLUG, __( 'That doesn’t look like a Google OAuth client ID. Copy the Client ID shown for your OAuth client in the Google Cloud console.', 'accessnow-site-replica' ), 'error' );
}
$fields = array(
'client_id' => $id,
'redirect' => 'own',
);
if ( '' !== $secret ) {
$fields['client_secret'] = $secret;
}
Destinations::update( 'drive', $fields );
Admin::back( self::SLUG, __( 'Google app saved. Now connect Google Drive.', 'accessnow-site-replica' ) );
}
/**
* Takes over the old WP Replica plugin's Google app (and its access, when it still works).
*/
public static function handle_drive_legacy() {
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_drive_legacy' );
$had_token = Destinations::import_legacy();
if ( $had_token ) {
try {
$about = ( new Google_Drive() )->about();
Logger::activity( 'cloud', 'Google Drive connected (app and access taken over from WP Replica) as ' . $about['email'] );
/* translators: %s: Google account e-mail. */
Admin::back( self::SLUG, sprintf( __( 'Google Drive connected as %s, using the app and access from WP Replica.', 'accessnow-site-replica' ), $about['email'] ) );
} catch ( Remote_Exception $e ) {
Google_Drive::disconnect();
Admin::back( self::SLUG, __( 'The Google app from WP Replica is set up, but its old access no longer works. Connect Google Drive to grant access again.', 'accessnow-site-replica' ), 'warning' );
}
}
Admin::back( self::SLUG, __( 'The Google app from WP Replica is set up. Now connect Google Drive.', 'accessnow-site-replica' ) );
}
/**
* Sends the browser to Google's consent screen.
*/
public static function handle_drive_connect() {
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_drive_connect' );
$drive = new Google_Drive();
if ( ! $drive->has_app() ) {
Admin::back( self::SLUG, __( 'Set up the Google app first.', 'accessnow-site-replica' ), 'error' );
}
$state = Secrets::b64url_encode( random_bytes( 24 ) );
$verifier = Secrets::b64url_encode( random_bytes( 48 ) );
set_transient(
'accessnow_replica_oauth_' . get_current_user_id(),
array(
'state' => $state,
'verifier' => $verifier,
),
15 * MINUTE_IN_SECONDS
);
$challenge = Secrets::b64url_encode( hash( 'sha256', $verifier, true ) );
wp_redirect( $drive->auth_url( $state, $challenge ) ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- Google's consent screen.
exit;
}
/**
* Google sends the browser back here (hidden page): checks the state and saves the tokens.
*/
public static function oauth_return() {
if ( ! current_user_can( Admin::CAP ) ) {
wp_die( esc_html__( 'You are not allowed to do that.', 'accessnow-site-replica' ), 403 );
}
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- The OAuth state (random, per user, single use) is the CSRF check.
$state = isset( $_GET['state'] ) ? sanitize_text_field( wp_unslash( $_GET['state'] ) ) : '';
$code = isset( $_GET['code'] ) ? sanitize_text_field( wp_unslash( $_GET['code'] ) ) : '';
$err = isset( $_GET['error'] ) ? sanitize_text_field( wp_unslash( $_GET['error'] ) ) : '';
// phpcs:enable WordPress.Security.NonceVerification.Recommended
$saved = get_transient( 'accessnow_replica_oauth_' . get_current_user_id() );
delete_transient( 'accessnow_replica_oauth_' . get_current_user_id() );
if ( '' !== $err ) {
/* translators: %s: Google's error code. */
Admin::back( self::SLUG, sprintf( __( 'Google Drive was not connected (%s).', 'accessnow-site-replica' ), $err ), 'error' );
}
if ( ! is_array( $saved ) || '' === $state || ! hash_equals( (string) $saved['state'], $state ) || '' === $code ) {
Admin::back( self::SLUG, __( 'The answer from Google could not be matched to your request (it expired or came from elsewhere). Try connecting again.', 'accessnow-site-replica' ), 'error' );
}
try {
$drive = new Google_Drive();
$drive->exchange_code( $code, (string) $saved['verifier'] );
$about = $drive->about();
} catch ( Remote_Exception $e ) {
Admin::back( self::SLUG, $e->getMessage(), 'error' );
}
Logger::activity( 'cloud', 'Google Drive connected as ' . $about['email'] );
Cron_Driver::reschedule();
/* translators: %s: Google account e-mail. */
Admin::back( self::SLUG, sprintf( __( 'Google Drive connected as %s.', 'accessnow-site-replica' ), $about['email'] ) );
}
/**
* Disconnects Google Drive.
*/
public static function handle_drive_disconnect() {
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_drive_disconnect' );
Google_Drive::disconnect();
Logger::activity( 'cloud', 'Google Drive disconnected' );
Admin::back( self::SLUG, __( 'Google Drive disconnected. Backups already in Drive are kept.', 'accessnow-site-replica' ) );
}
/**
* Saves the schedule.
*/
public static function handle_schedule_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_schedule_save' );
$input = array();
foreach ( array( 'frequency', 'time', 'day', 'what', 'keep', 'part_mb', 'notify', 'email' ) as $k ) {
if ( isset( $_POST[ $k ] ) ) {
$input[ $k ] = sanitize_text_field( wp_unslash( $_POST[ $k ] ) );
}
}
Destinations::save_schedule( $input );
$next = Cron_Driver::reschedule();
if ( null === $next ) {
Admin::back( self::SLUG, __( 'Schedule saved: scheduled backups are off.', 'accessnow-site-replica' ) );
}
$message = sprintf(
/* translators: %s: date and time. */
__( 'Schedule saved. The next backup runs %s.', 'accessnow-site-replica' ),
wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $next )
);
$ready = Destination::current()->ready();
Admin::back( self::SLUG, $ready ? $message : $message . ' ' . __( 'Set up where backups go first, or it will not run.', 'accessnow-site-replica' ), $ready ? 'success' : 'warning' );
}
/**
* Deletes a backup from Drive.
*/
public static function handle_cloud_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_cloud_delete' );
$id = isset( $_POST['id'] ) ? sanitize_text_field( wp_unslash( $_POST['id'] ) ) : '';
if ( ! preg_match( '/^[\w-]{10,200}$/', $id ) && ! preg_match( '#^[^/]{1,100}/[^/]{1,200}/\d{8}-\d{6}-[\w.-]+/$#', $id ) ) {
Admin::back( self::SLUG, __( 'Unknown backup.', 'accessnow-site-replica' ), 'error' );
}
try {
Destination::current()->delete( $id );
} catch ( Remote_Exception $e ) {
Admin::back( self::SLUG, $e->getMessage(), 'error' );
}
Logger::activity( 'cloud', 'Backup deleted from ' . Destination::current()->label() );
Admin::back( self::SLUG, __( 'Backup deleted.', 'accessnow-site-replica' ) );
}
}