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' ) . '

'; } ?>

has_app() ) : ?>

label() ) ); ?>

label() ); ?>

>

'own' ) ); ?>
  1. ' . 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' ) ); } }