/, * inside the WordPress installation. * * Wherever it ends up, the deny files are written, and if the folder sits * anywhere a URL can reach, a probe file is requested over HTTP before every * dump. A probe that comes back, or a check that cannot finish, means no dump * is written: the check fails closed. * * Before 1.1.1 the default could be a folder beside the web root, * /freshet-backups/. That is $legacy: dumps still waiting there * are moved into this folder by adopt(), so the next send picks them up. */ final class Buffer { public const EXTENSION = '.sql.gz'; private const UNREACHABLE = [401, 403, 404, 410]; private const DENY_FILES = [ '.htaccess' => "# Freshet Backup: nothing in this folder is served.\n\n\tRequire all denied\n\n\n\tOrder allow,deny\n\tDeny from all\n\n", 'web.config' => "\n\n\t\n\t\t\n\t\t\t\n\t\t\n\t\n\n", 'index.php' => "name, '/') . '-\d{8}-\d{6}' . preg_quote(self::EXTENSION, '/') . '$/'; } public function newPath(int $time): string { return $this->dir . '/' . $this->name . '-' . gmdate('Ymd-His', $time) . self::EXTENSION; } /** * Make the folder ready for a dump, or say why it is not. * * @return string|null null when a dump may be written; otherwise the reason, for the screen */ public function prepare(?Transport $loopback): ?string { if (!is_dir($this->dir) && !wp_mkdir_p($this->dir)) { return sprintf( /* translators: %s: folder path */ __('No backup was written: the backup folder %s could not be created. Create it yourself and make it writable by the web server, or set FRESHET_BACKUP_DIR in wp-config.php to a folder that is.', 'freshet-backup'), $this->dir ); } if (!wp_is_writable($this->dir)) { return sprintf( /* translators: %s: folder path */ __('No backup was written: the backup folder %s is not writable by the web server. Fix its permissions, or set FRESHET_BACKUP_DIR in wp-config.php to a folder that is.', 'freshet-backup'), $this->dir ); } foreach (self::DENY_FILES as $file => $contents) { if (!is_file($this->dir . '/' . $file) && file_put_contents($this->dir . '/' . $file, $contents) === false) { return sprintf( /* translators: 1: file name, 2: folder path */ __('No backup was written: the file %1$s that keeps %2$s from being downloaded could not be saved there. Fix the folder\'s permissions and try again.', 'freshet-backup'), $file, $this->dir ); } } return $this->url === null ? null : $this->probe($loopback); } /** @return list finished dumps in this folder, oldest first */ public function files(): array { $files = []; foreach ((array) scandir($this->dir) as $entry) { if (is_string($entry) && preg_match($this->filePattern(), $entry)) { $files[] = $this->dir . '/' . $entry; } } sort($files, SORT_STRING); return $files; } /** @return list the files removed, so at most $keep remain */ public function prune(int $keep): array { $files = $this->files(); $gone = array_slice($files, 0, max(0, count($files) - $keep)); foreach ($gone as $file) { wp_delete_file($file); } return $gone; } /** Leftovers from a run that died mid-write. Only called under the run lock. */ public function clearPartials(): void { foreach ((array) glob($this->dir . '/*' . self::EXTENSION . '.part') as $file) { if (is_string($file)) { wp_delete_file($file); } } } /** * The wp-config--