runner = $runner; $this->buffer = $buffer; } public static function url(array $args = []): string { $url = admin_url('options-general.php?page=' . self::PAGE); foreach ($args as $key => $value) { $url = add_query_arg($key, $value, $url); } return $url; } public function menu(): void { $hook = add_options_page( __('Freshet Backup', 'freshet-backup'), __('Freshet Backup', 'freshet-backup'), self::CAPABILITY, self::PAGE, [$this, 'render'] ); if ($hook !== false) { add_action('load-' . $hook, [$this, 'enqueueStyle']); } } /** * "Settings" before Deactivate on the plugin's Plugins-screen row, for * whoever may open the screen it points at. * * @param array $links * * @return array */ public function actionLinks(array $links): array { if (!current_user_can(self::CAPABILITY)) { return $links; } return ['settings' => sprintf( '%s', esc_url(self::url()), esc_html__('Settings', 'freshet-backup') )] + $links; } /** * The Freshet brand strip's rules, on this screen only. Hooked from the * screen's own load- action, so it is registered while the menu is built * and adds nothing to the plugin's boot. */ public function enqueueStyle(): void { wp_register_style('freshet-backup-admin', false, [], FRESHET_BACKUP_VERSION); wp_enqueue_style('freshet-backup-admin'); wp_add_inline_style('freshet-backup-admin', ' .frst-header { background: #fff; border-bottom: 1px solid #dcdcde; margin: 0 0 0 -20px; padding: 16px 20px 0; } .frst-header__row { display: flex; align-items: center; gap: 10px; padding-bottom: 14px; } .frst-header__mark { width: 28px; height: 28px; flex: none; } .frst-header__title { font-size: 16px; font-weight: 600; color: #1d2327; margin: 0; padding: 0; } .frst-header__version { font-size: 11px; color: #646970; background: #f0f0f1; border-radius: 10px; padding: 2px 8px; } .frst-header__meta { margin-left: auto; display: flex; align-items: center; gap: 14px; font-size: 13px; } .frst-header .nav-tab-wrapper { border-bottom: 0; padding: 0; margin: 0; } @media screen and (max-width: 782px) { .frst-header { margin-left: -10px; } } .freshet-backup-wrap { margin-top: 1.5em; } .freshet-backup-wrap .fb-panel { background: #fff; border: 1px solid #c3c4c7; box-shadow: 0 1px 1px rgba(0,0,0,.04); margin: 0 0 24px; } .freshet-backup-wrap .fb-panel__head { display: flex; align-items: flex-start; gap: 12px; padding: 16px 20px; border-bottom: 1px solid #f0f0f1; } .freshet-backup-wrap .fb-panel__title { font-size: 15px; font-weight: 600; color: #1d2327; margin: 0; padding: 0; line-height: 1.4; } .freshet-backup-wrap .fb-panel__text { margin: 2px 0 0; color: #50575e; } .freshet-backup-wrap .fb-panel__note { margin: 0; padding: 12px 20px; border-top: 1px solid #f0f0f1; } .freshet-backup-wrap .fb-panel__foot { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 12px 20px; border-top: 1px solid #f0f0f1; background: #f6f7f7; } .freshet-backup-wrap .fb-panel__foot .description { flex-basis: 100%; margin: 0; } .freshet-backup-wrap .fb-dot { flex: none; width: 10px; height: 10px; margin-top: 6px; border-radius: 50%; background: #8c8f94; } .freshet-backup-wrap .fb-dot--ok { background: #00a32a; } .freshet-backup-wrap .fb-dot--wait { background: #dba617; } .freshet-backup-wrap .fb-dot--bad { background: #d63638; } .freshet-backup-wrap .fb-facts { margin: 0; } .freshet-backup-wrap .fb-facts th { width: 180px; padding: 12px 10px 12px 20px; } .freshet-backup-wrap .fb-facts td { padding: 12px 20px 12px 10px; } .freshet-backup-wrap .fb-facts tr + tr > * { border-top: 1px solid #f0f0f1; } .freshet-backup-wrap .fb-steps { list-style: none; margin: 0; padding: 0; } .freshet-backup-wrap .fb-step { display: flex; gap: 14px; margin: 0; padding: 16px 20px; } .freshet-backup-wrap .fb-step + .fb-step { border-top: 1px solid #f0f0f1; } .freshet-backup-wrap .fb-step__num { flex: none; display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: 50%; background: #1122ff; color: #fff; font-weight: 600; } .freshet-backup-wrap .fb-step--last .fb-step__num { background: #f0f0f1; color: #50575e; } .freshet-backup-wrap .fb-step__body { flex: 1; min-width: 0; } .freshet-backup-wrap .fb-step__title { font-size: 14px; font-weight: 600; color: #1d2327; margin: 4px 0 2px; } .freshet-backup-wrap .fb-step__text { margin: 0 0 10px; color: #50575e; } .freshet-backup-wrap .fb-step--last .fb-step__text { margin-bottom: 0; } .freshet-backup-wrap .fb-field { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; } .freshet-backup-wrap .fb-field input[type=text] { flex: 1 1 16em; max-width: 25em; margin: 0; } .freshet-backup-wrap .fb-field select, .freshet-backup-wrap .fb-field .button { margin: 0; } .freshet-backup-wrap .fb-table { border-collapse: collapse; margin: 0 0 24px; } .freshet-backup-wrap .fb-table th { width: 200px; padding: 10px 10px 10px 0; text-align: left; vertical-align: top; font-weight: 600; color: #1d2327; line-height: 1.3; } .freshet-backup-wrap .fb-table td { padding: 10px; vertical-align: top; line-height: 1.5; } .freshet-backup-wrap .fb-table th:has(label) { vertical-align: middle; } @media screen and (max-width: 782px) { .freshet-backup-wrap .fb-table th, .freshet-backup-wrap .fb-table td { display: block; width: auto; padding: 8px 0 0; } .freshet-backup-wrap .fb-panel__head, .freshet-backup-wrap .fb-step, .freshet-backup-wrap .fb-panel__note, .freshet-backup-wrap .fb-panel__foot { padding-left: 12px; padding-right: 12px; } .freshet-backup-wrap .fb-facts th, .freshet-backup-wrap .fb-facts td { width: auto; padding-left: 12px; padding-right: 12px; } .freshet-backup-wrap .fb-facts th { padding-bottom: 0; } .freshet-backup-wrap .fb-facts tr + tr > td { border-top: 0; } .freshet-backup-wrap .fb-field input[type=text] { max-width: none; } } '); } public function runNow(): void { $this->guard(self::ACTION); if (!$this->config->enabled) { wp_die( esc_html__('Backups are off on this site, so nothing ran. Connect Dropbox on the Freshet Backup screen to turn them on, or set FRESHET_BACKUP_ENABLED to true in wp-config.php.', 'freshet-backup'), esc_html__('Backups are off', 'freshet-backup'), ['response' => 409, 'back_link' => true] ); } if (function_exists('set_time_limit')) { set_time_limit(0); } ignore_user_abort(true); ($this->runner)()->run('manual'); wp_safe_redirect(self::url(['ran' => '1'])); exit; } /** The pasted code comes back: trade it for a token and write the block, or show the block to paste. */ public function connect(): void { $this->guard(self::CONNECT); if ($this->config->dropboxConfigured()) { $this->refuse(409, __('This site already connects to Dropbox through your own app in wp-config.php (the FRESHET_BACKUP_DROPBOX_* constants), and those take precedence. Remove them first to connect here.', 'freshet-backup')); } $key = $this->connectKey(); if ($key === '') { $this->refuse(409, __('Connecting from this screen is not available in this version. The readme explains how to connect with your own Dropbox app.', 'freshet-backup')); } $code = isset($_POST['code']) ? sanitize_text_field(wp_unslash($_POST['code'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified in guard() above. if ($code === '') { $this->refuse(400, __('No code was pasted. Paste the code Dropbox showed after you pressed Allow, then press Finish connecting.', 'freshet-backup')); } $verifier = (string) get_transient(self::VERIFIER . get_current_user_id()); if ($verifier === '') { $this->refuse(400, __('This connection attempt expired after an hour. Reload the Freshet Backup screen, press Connect Dropbox again and paste the new code.', 'freshet-backup')); } delete_transient(self::VERIFIER . get_current_user_id()); try { $token = Authorization::exchange($this->dropbox(), $key, $code, $verifier); $block = WpConfig::block(WpConfig::hosts(site_url(), home_url()), [ 'FRESHET_BACKUP_CONNECT_APP_KEY' => $key, 'FRESHET_BACKUP_CONNECT_TOKEN' => $token, ]); } catch (DestinationError | WpConfigError $e) { $this->refuse(400, $e->getMessage()); } $why = $this->edit(static fn (string $source): string => WpConfig::place($source, $block)); if ($why !== null) { $this->page( __('Paste this into wp-config.php', 'freshet-backup'), '

' . esc_html__('Dropbox is connected, but the plugin could not save the connection in wp-config.php:', 'freshet-backup') . ' ' . esc_html($why) . '

' . '

' . esc_html__('Add these lines to wp-config.php yourself, just above the line that says "That\'s all, stop editing!", then reload the Freshet Backup screen. They give access to your Dropbox, so do not share them by email or chat.', 'freshet-backup') . '

' . '
' . esc_html($block) . '
' ); } wp_safe_redirect(self::url(['notice' => 'connected'])); exit; } /** Withdraw the token at Dropbox, then take the block out of wp-config.php. */ public function disconnect(): void { $this->guard(self::DISCONNECT); if (!$this->config->connected()) { $this->refuse(409, __('There is no connection from this screen to remove. If you connected your own Dropbox app, remove its constants from wp-config.php.', 'freshet-backup')); } $revoked = true; try { (new DropboxDestination($this->dropbox(), $this->config->connectAppKey, '', $this->config->connectToken, ''))->revoke(); } catch (DestinationError) { $revoked = false; } $why = $this->edit(static function (string $source): string { if (!WpConfig::has($source)) { throw new WpConfigError(__('the Freshet Backup lines are not in wp-config.php itself; they may be in a file that wp-config.php loads.', 'freshet-backup')); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- caught by edit() and escaped where it is printed. } return WpConfig::remove($source); }); if ($why !== null) { $this->page( __('Remove the Freshet Backup lines from wp-config.php', 'freshet-backup'), '

' . esc_html($revoked ? __('Dropbox has withdrawn this site\'s access.', 'freshet-backup') : __('Dropbox could not confirm that this site\'s access was withdrawn. Remove "Freshet Backup" under Connected apps in your Dropbox account settings.', 'freshet-backup')) . '

' . '

' . esc_html__('The plugin could not edit wp-config.php:', 'freshet-backup') . ' ' . esc_html($why) . '

' . '

' . esc_html(sprintf( /* translators: 1: opening marker, 2: closing marker */ __('Delete everything from %1$s to %2$s yourself, then reload the Freshet Backup screen.', 'freshet-backup'), WpConfig::BEGIN, WpConfig::END )) . '

' ); } wp_safe_redirect(self::url(['notice' => $revoked ? 'disconnected' : 'unrevoked'])); exit; } /** Store the chosen interval and move the event to it at once. */ public function setInterval(): void { $this->guard(self::INTERVAL); if ($this->config->intervalLocked) { $this->refuse(409, __('How often backups run is set in wp-config.php (FRESHET_BACKUP_INTERVAL), so it cannot be changed here. Change it there.', 'freshet-backup')); } $seconds = isset($_POST['interval']) ? (int) sanitize_text_field(wp_unslash($_POST['interval'])) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified in guard() above. if (!in_array($seconds, Config::INTERVAL_CHOICES, true)) { $this->refuse(400, __('That interval is not one the screen offers. Reload the screen and pick one from the list.', 'freshet-backup')); } update_option(Config::INTERVAL_OPTION, $seconds, false); $config = $this->config->withInterval($seconds); add_filter('cron_schedules', static fn (array $schedules): array => Schedule::intervals($schedules, $config), 20); Schedule::sync($config); wp_safe_redirect(self::url(['notice' => 'interval'])); exit; } public function notices(): void { if (!current_user_can(self::CAPABILITY)) { return; } $screen = function_exists('get_current_screen') ? get_current_screen() : null; if ($screen !== null && str_ends_with((string) $screen->id, '_page_' . self::PAGE)) { return; // the screen itself shows it in full } // Both, when both apply: the overdue reading says how long nothing has // worked, the status says what the last attempt ran into. Neither // sentence is the other's. $problems = array_filter([$this->health()->problem(), $this->status->problem()]); if ($problems === []) { return; } printf( '

%s %s %s

', esc_html__('Freshet Backup:', 'freshet-backup'), esc_html(implode(' ', $problems)), esc_url(self::url()), esc_html__('Open the backup screen', 'freshet-backup') ); } public function render(): void { if (!current_user_can(self::CAPABILITY)) { return; } // The strip is printed BEFORE `.wrap`: that is what makes it sit flush // at the top and run the full width of the screen. WordPress moves every // `.notice` to just after `.wp-header-end`, so with the anchor inside // `.wrap` notices land under the strip, in the panel. `.wrap` keeps an // h1 for the admin's own heading logic; the strip's title is visual. $this->renderHeader(); echo '
'; printf( '

%s


', esc_html__('Freshet Backup', 'freshet-backup') ); $last = $this->status->get(); // phpcs:disable WordPress.Security.NonceVerification.Recommended -- display flags set by our own redirects; each selects a fixed sentence and none is echoed. if (isset($_GET['ran']) && isset($last['outcome'])) { $class = $last['outcome'] === 'ok' ? 'notice-success' : 'notice-error'; printf('

%s

', esc_attr($class), esc_html((string) ($last['message'] ?? ''))); } $notice = isset($_GET['notice']) ? sanitize_key(wp_unslash($_GET['notice'])) : ''; // phpcs:enable WordPress.Security.NonceVerification.Recommended if (in_array($notice, self::NOTICES, true)) { printf( '

%s

', esc_attr($notice === 'unrevoked' ? 'notice-warning' : 'notice-success'), esc_html(match ($notice) { 'connected' => __('Dropbox is connected and backups are on. Press Run backup now to make the first one.', 'freshet-backup'), 'disconnected' => __('Dropbox is disconnected. Backups already in Dropbox stay there.', 'freshet-backup'), 'unrevoked' => __('Dropbox is disconnected from this site, but Dropbox could not confirm it removed the site\'s access. To be sure, remove "Freshet Backup" under Connected apps in your Dropbox account settings.', 'freshet-backup'), default => __('Schedule saved.', 'freshet-backup'), }) ); } $overdue = $this->health()->problem(); if ($overdue !== null) { printf( '

%s %s

', esc_html__('No recent backup.', 'freshet-backup'), esc_html($overdue) ); } // One panel, two faces: the steps that connect Dropbox while it is not // connected, then the status of the backups once they are on. if ($this->config->dropbox() === null) { $this->renderConnect(); } if ($this->config->enabled || $this->config->dropbox() !== null) { $this->renderStatus($last); } echo '

' . esc_html__('Schedule', 'freshet-backup') . '

'; $this->renderInterval(); if ($this->config->enabled) { $this->renderStorage(); } echo '
'; } /** * What started the last run, as the sentence after it. This is the reading * that tells whether WP-Cron is firing: after a catch-up the event is moved * forward, so a future "Next run" alone proves nothing, but a run the * catch-up had to make says outright that WP-Cron did not. */ private static function startedBy(string $trigger): string { $said = match ($trigger) { 'schedule' => __('Started on schedule.', 'freshet-backup'), 'catch-up' => __('Started by the catch-up when wp-admin was opened, because WP-Cron had not run it.', 'freshet-backup'), 'manual' => __('Started with Run backup now.', 'freshet-backup'), default => '', }; return $said === '' ? '' : ' ' . $said; } /** * The Freshet brand strip: mark, name, version, Docs and Support. One * screen, so no tab row under it; the row's own bottom padding is the * strip's lower edge. */ private function renderHeader(): void { ?>

'; $this->panelHead( 'off', __('Not connected', 'freshet-backup'), $this->config->enabled ? __('Backups stay on this server only.', 'freshet-backup') : __('Backups are off. Connect Dropbox below to turn them on.', 'freshet-backup') ); $key = $this->connectKey(); if ($key === '') { echo '

' . esc_html__('Connecting from this screen is not available in this version. The readme explains how to connect with your own Dropbox app.', 'freshet-backup') . '

'; return; } $verifier = (string) get_transient(self::VERIFIER . get_current_user_id()); if ($verifier === '') { $verifier = Authorization::verifier(); set_transient(self::VERIFIER . get_current_user_id(), $verifier, HOUR_IN_SECONDS); } echo '
    '; echo '
  1. '; echo '

    ' . esc_html__('Allow access at Dropbox', 'freshet-backup') . '

    '; echo '

    ' . esc_html__('Dropbox opens in a new tab. Sign in to the account that should hold the backups and press Allow. Freshet Backup can only see its own folder there.', 'freshet-backup') . '

    '; printf( '%s', esc_url(Authorization::url($key, $verifier)), esc_html__('Connect Dropbox', 'freshet-backup') ); echo '
  2. '; echo '
  3. '; echo '

    '; echo '

    ' . esc_html__('Dropbox shows a code after you press Allow.', 'freshet-backup') . '

    '; echo '
    '; echo ''; wp_nonce_field(self::CONNECT); echo ''; submit_button(__('Finish connecting', 'freshet-backup'), 'secondary', 'submit', false); echo '
  4. '; echo '
  5. '; echo '

    ' . esc_html__('Connected', 'freshet-backup') . '

    '; echo '

    ' . esc_html__('Backups turn on and run on the schedule below. You can make the first one right away.', 'freshet-backup') . '

    '; echo '
  6. '; echo '
'; printf( '

%s

', esc_html(sprintf( /* translators: %s: host names */ __('The connection is saved in wp-config.php and works on %s only, so a copy of this site elsewhere does not back up into your Dropbox.', 'freshet-backup'), implode(', ', WpConfig::hosts(site_url(), home_url())) )) ); echo ''; } /** * The status panel: one line that says whether all is well, the facts * behind it, and the actions grouped at its foot. * * @param array $last */ private function renderStatus(array $last): void { $dropbox = $this->config->dropbox(); $health = $this->health(); $problems = $this->config->enabled ? array_filter([$health->problem(), $this->status->problem()]) : []; $when = static fn (int $time): string => wp_date(get_option('date_format') . ' ' . get_option('time_format'), $time); echo '
'; if (!$this->config->enabled) { $this->panelHead('off', __('Backups are off', 'freshet-backup'), __('Dropbox is connected, but FRESHET_BACKUP_ENABLED in wp-config.php switches backups off. Set it to true, or remove it, to turn them on.', 'freshet-backup')); } elseif ($problems !== []) { $this->panelHead('bad', __('Backups need attention', 'freshet-backup'), __('Health, below, says what went wrong and what to do.', 'freshet-backup')); } elseif ($health->lastSuccess() === null) { $this->panelHead('wait', __('No backup yet', 'freshet-backup'), __('Press Run backup now to make the first one, or wait for the schedule.', 'freshet-backup')); } else { $this->panelHead('ok', __('Backups are running', 'freshet-backup'), sprintf( /* translators: %s: a length of time, e.g. "3 hours" */ __('The last one ran %s ago.', 'freshet-backup'), human_time_diff((int) $health->lastSuccess()) )); } $folder = $this->config->dropboxFolder(Plugin::host()); $where = sprintf( /* translators: %s: Dropbox folder */ __('Backups are saved in %s, inside the app\'s folder in Dropbox.', 'freshet-backup'), $folder === '' ? '/' : $folder ); $rows = [ __('Dropbox', 'freshet-backup') => match ($dropbox['source'] ?? null) { 'constants' => __('Connected through your own Dropbox app in wp-config.php, which takes precedence over this screen.', 'freshet-backup') . ' ' . $where, 'connected' => __('Connected.', 'freshet-backup') . ' ' . $where, default => __('Not connected. Backups stay on this server only.', 'freshet-backup'), }, ]; if ($this->config->enabled) { $next = wp_next_scheduled(Schedule::HOOK); $rows += [ __('Schedule', 'freshet-backup') => $next ? sprintf( $health->late() > 0 /* translators: 1: e.g. "Every day", 2: a date and time */ ? __('%1$s. The backup due %2$s is still waiting: WordPress starts scheduled tasks only when someone visits the site.', 'freshet-backup') /* translators: 1: e.g. "Every day", 2: a date and time */ : __('%1$s. Next run: %2$s.', 'freshet-backup'), ucfirst(Schedule::describe($this->config->interval)), $when((int) $next) ) : __('The first backup is scheduled on the next page load.', 'freshet-backup'), __('Last run', 'freshet-backup') => isset($last['outcome']) ? sprintf( /* translators: 1: a date and time, 2: what happened */ __('%1$s — %2$s', 'freshet-backup'), $when((int) ($last['time'] ?? 0)), (string) ($last['message'] ?? '') ) . (isset($last['bytes']) ? ' ' . sprintf( /* translators: 1: file size, 2: number of tables, 3: seconds */ __('(%1$s compressed, %2$d tables, %3$s s)', 'freshet-backup'), size_format((int) $last['bytes'], 1), (int) ($last['tables'] ?? 0), (string) ($last['seconds'] ?? '') ) : '') . self::startedBy((string) ($last['trigger'] ?? '')) : __('Never.', 'freshet-backup'), __('Health', 'freshet-backup') => match (true) { // One reading, not both: when backups are overdue the Last run row above // already carries what the last attempt ran into. $problems !== [] => (string) reset($problems), $health->late() > 0 => __('Fine. The scheduled backup is a little late, which is normal. You will see a warning here if it stays late.', 'freshet-backup'), $health->lastSuccess() === null => __('Nothing to report until the first backup has run.', 'freshet-backup'), default => __('Healthy. The next backup is scheduled.', 'freshet-backup'), }, ]; } echo ''; foreach ($rows as $label => $value) { printf('', esc_html($label), esc_html($value)); } echo ''; $notes = []; echo '
'; if ($this->config->enabled) { $this->form(self::ACTION, __('Run backup now', 'freshet-backup'), 'primary'); } if ($this->config->connected()) { if (($dropbox['source'] ?? null) === 'constants') { $this->form(self::DISCONNECT, __('Disconnect it', 'freshet-backup'), 'secondary'); $notes[] = __('wp-config.php also holds a connection made on this screen. It is not used while your own app is set up.', 'freshet-backup'); } else { $this->form(self::DISCONNECT, __('Disconnect Dropbox', 'freshet-backup'), 'secondary'); $notes[] = __('Disconnecting removes this site\'s access to your Dropbox. Backups already in Dropbox stay there.', 'freshet-backup'); } } foreach ($notes as $note) { echo '

' . esc_html($note) . '

'; } echo '
'; } /** Where the copies are kept on this server, and what goes into them. */ private function renderStorage(): void { $buffer = ($this->buffer)(); $rows = [ __('Tables', 'freshet-backup') => $this->config->allTables ? __('Every table in the database (FRESHET_BACKUP_ALL_TABLES).', 'freshet-backup') : sprintf( /* translators: %s: database table prefix */ __('Tables starting with %s.', 'freshet-backup'), $GLOBALS['wpdb']->base_prefix ), __('Local folder', 'freshet-backup') => $buffer->dir, ]; echo '

' . esc_html__('Storage', 'freshet-backup') . '

'; echo ''; foreach ($rows as $label => $value) { printf('', esc_html($label), esc_html($value)); } echo ''; } /** A panel's first line: a state dot, a verdict, and one sentence under it. */ private function panelHead(string $state, string $title, string $text): void { printf( '

%s

%s

', esc_attr($state), esc_html($title), esc_html($text) ); } private function renderInterval(): void { if ($this->config->intervalLocked) { echo '

' . esc_html(sprintf( /* translators: %s: e.g. "every day" */ __('Backups run %s. This is set in wp-config.php (FRESHET_BACKUP_INTERVAL), so it cannot be changed here.', 'freshet-backup'), Schedule::describe($this->config->interval) )) . '

'; return; } echo ''; echo ''; } private function form(string $action, string $label, string $type): void { echo '
'; echo ''; wp_nonce_field($action); submit_button($label, $type, 'submit', false); echo '
'; } /** The app a Connect uses: your own key alone (no secret), else the Freshet Backup app. */ private function connectKey(): string { return $this->config->dropboxAppKey !== '' ? $this->config->dropboxAppKey : Authorization::FRESHET_APP_KEY; } private function dropbox(): Transport { return $this->dropbox ?? new Http\WpTransport(DropboxDestination::HOSTS); } /** Whether a backup has gone missing. Built per call, so a catch-up run earlier in the request is seen. */ private function health(): Health { return $this->health ?? new Health($this->config, $this->status); } /** * Change wp-config.php through $change. WpConfig::write() checks the new * file, swaps it in whole and reads it back; no copy of it is kept. * * @param callable(string): string $change * @return string|null why the file was not changed, or null when it was */ private function edit(callable $change): ?string { $path = WpConfig::path($this->abspath); if ($path === null) { return __('it is not where WordPress looks for it.', 'freshet-backup'); } try { $source = (string) file_get_contents($path); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- a local file. WpConfig::write($path, $change($source)); } catch (WpConfigError $e) { return $e->getMessage(); } return null; } private function guard(string $action): void { if (!current_user_can(self::CAPABILITY)) { wp_die( esc_html__('Only an administrator can do this.', 'freshet-backup'), esc_html__('Not allowed', 'freshet-backup'), ['response' => 403] ); } $nonce = isset($_POST['_wpnonce']) ? sanitize_text_field(wp_unslash($_POST['_wpnonce'])) : ''; if (!wp_verify_nonce($nonce, $action)) { wp_die( esc_html__('This request has expired. Go back, reload the screen and try again.', 'freshet-backup'), esc_html__('Request expired', 'freshet-backup'), ['response' => 403, 'back_link' => true] ); } } private function refuse(int $status, string $message): never { wp_die(esc_html($message), esc_html__('Freshet Backup', 'freshet-backup'), ['response' => (int) $status, 'back_link' => true]); } /** A page of its own, for what must be shown once and never stored: the block to paste. */ private function page(string $title, string $html): never { $html .= '

' . esc_html__('Back to Freshet Backup', 'freshet-backup') . '

'; wp_die(wp_kses_post($html), esc_html($title), ['response' => 200]); } }