$plugin->runner(), fn (): Buffer => $plugin->buffer() ); add_action('init', fn () => Schedule::sync($config)); add_action('admin_menu', [$admin, 'menu']); add_filter('plugin_action_links_' . plugin_basename(dirname(__DIR__) . '/freshet-backup.php'), [$admin, 'actionLinks']); add_action('admin_post_' . Admin::ACTION, [$admin, 'runNow']); add_action('admin_post_' . Admin::CONNECT, [$admin, 'connect']); add_action('admin_post_' . Admin::DISCONNECT, [$admin, 'disconnect']); add_action('admin_post_' . Admin::INTERVAL, [$admin, 'setInterval']); if ($config->enabled) { add_filter('cron_schedules', fn (array $schedules): array => Schedule::intervals($schedules, $config)); add_action(Schedule::HOOK, fn () => $plugin->execute('schedule')); add_action('admin_notices', [$admin, 'notices']); add_action('admin_init', fn () => $plugin->catchUp()); } return $plugin; } /** * The run WP-Cron did not make, made on this wp-admin load instead. * * This adds no way in. `admin_init` fires on a wp-admin page load, the * request has to belong to a signed-in user who may press Run now, and the * work is exactly what that button already does — there is no route, no AJAX action and * no anonymous path to it. Health hands out one catch-up per interval, so a * site whose dumps keep dying does not start one on every click. * * The dump itself happens on `shutdown`, after the page has been sent (and * after the connection is released where the server can do that), so * wp-admin never waits for it. */ public function catchUp(): void { // Not an anonymous request, not a signed-in user who could not press // Run now themselves, not AJAX, not wp-cron.php (which is already // running the schedule), and not while WordPress is installing or // updating itself — a dump has no business starting mid-file-swap. if (wp_doing_ajax() || wp_doing_cron() || wp_installing() || !is_user_logged_in() || !current_user_can(Admin::CAPABILITY)) { return; } // Nor on the update screens. A run there would start on shutdown, after // an update of this plugin had replaced its files under the process // still holding the old ones, and load the new classes into it. if (in_array($GLOBALS['pagenow'] ?? '', ['update.php', 'update-core.php'], true)) { return; } // The screen's own actions start their own run; do not race them. $action = isset($_REQUEST['action']) ? sanitize_key(wp_unslash($_REQUEST['action'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read only to stand down, never to act on. if (str_starts_with($action, 'freshet_backup_')) { return; } if (!$this->health()->claim()) { return; } add_action('shutdown', function (): void { if (function_exists('fastcgi_finish_request')) { fastcgi_finish_request(); } $this->execute('catch-up'); Schedule::advance($this->config); }, 99); } private function health(): Health { return new Health($this->config, new Status()); } /** @return array{outcome: string, message: string} */ public function execute(string $trigger): array { if (function_exists('set_time_limit')) { set_time_limit(0); } ignore_user_abort(true); return $this->runner()->run($trigger); } public function runner(): Runner { if ($this->runner !== null) { return $this->runner; } global $wpdb; $host = self::host(); $destination = null; $dropbox = $this->config->dropbox(); if ($dropbox !== null) { $destination = new DropboxDestination( new WpTransport(DropboxDestination::HOSTS), $dropbox['key'], $dropbox['secret'], $dropbox['token'], $this->config->dropboxFolder($host) ); } return $this->runner = new Runner( $this->config, $this->buffer(), new Dumper(new WpdbDatabase($wpdb), (string) $wpdb->base_prefix, $this->config->allTables), $destination, new Status(), new Lock(), self::loopback(), $host ); } public function buffer(): Buffer { return Buffer::resolve( $this->config->dir, ABSPATH, WP_CONTENT_DIR, site_url(), content_url(), self::host(), static function (): string { $suffix = (string) get_option(self::SUFFIX_OPTION, ''); if ($suffix === '') { $suffix = strtolower(wp_generate_password(12, false, false)); update_option(self::SUFFIX_OPTION, $suffix, false); } return $suffix; } ); } /** The transport for the backup folder's privacy check: this site's own hosts only. */ public static function loopback(): Transport { $hosts = array_values(array_unique(array_filter([ (string) wp_parse_url(site_url(), PHP_URL_HOST), (string) wp_parse_url(home_url(), PHP_URL_HOST), (string) wp_parse_url(content_url(), PHP_URL_HOST), ]))); return new WpTransport($hosts, false, true); } public static function host(): string { return strtolower((string) wp_parse_url(home_url(), PHP_URL_HOST)); } }