PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 4.9.0
WP STAGING – WordPress Backup, Restore, Migration & Clone v4.9.0
4.9.1 4.9.0 4.8.1 trunk 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.10.0 3.2.0 3.3.1 3.3.2 3.3.3 3.4.1 3.4.3 3.5.0 3.6.0 3.7.1 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.8.6 3.8.7 3.9.0 3.9.1 3.9.2 3.9.3 3.9.4 4.0.0 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4 4.2.0 4.2.1 4.3.0 4.3.1 4.3.2 4.4.0 4.5.0 4.6.0 4.7.0 4.7.1 4.7.2 4.7.3 4.8.0
wp-staging / Backup / BackupValidator.php
wp-staging / Backup Last commit date
Ajax 1 month ago BackgroundProcessing 1 year ago Dto 1 week ago Entity 1 week ago Exceptions 1 year ago FileHeader 1 month ago Interfaces 6 months ago Job 1 month ago Request 1 year ago Service 1 week ago Storage 1 month ago Task 1 week ago Traits 10 months ago Utils 3 months ago AfterRestore.php 5 months ago BackupDeleter.php 2 months ago BackupDownload.php 8 months ago BackupFileIndex.php 1 year ago BackupGlitchReason.php 1 year ago BackupHeader.php 1 month ago BackupRepairer.php 6 months ago BackupRetentionHandler.php 1 month ago BackupScheduler.php 1 week ago BackupServiceProvider.php 1 month ago BackupValidator.php 6 months ago FileHeader.php 1 week ago FileHeaderAttribute.php 2 years ago WithBackupIdentifier.php 1 year ago
BackupValidator.php
279 lines
1 <?php
2
3 namespace WPStaging\Backup;
4
5 use WPStaging\Backup\Entity\BackupMetadata;
6 use WPStaging\Backup\Exceptions\BackupRuntimeException;
7 use WPStaging\Backup\Service\BackupsFinder;
8 use WPStaging\Backup\Task\Tasks\JobRestore\RestoreRequirementsCheckTask;
9 use WPStaging\Framework\Filesystem\FileObject;
10 use WPStaging\Framework\Utils\Strings;
11
12 use function WPStaging\functions\debug_log;
13
14 /**
15 * Validates backup file integrity and structure
16 *
17 * This class performs comprehensive validation checks on backup files including:
18 * - File index validation (verifying the list of files in the backup matches metadata)
19 * - Multipart backup validation (checking all parts exist with correct sizes)
20 * - Backup version compatibility checks
21 * - Detection of missing or corrupted backup parts
22 * - Verification of file index first line format
23 *
24 * The validator maintains lists of validation issues (missing parts, size mismatches)
25 * that can be retrieved for display to users. It works with BackupMetadata to access
26 * backup structure information and ensures backups are restorable before restoration attempts.
27 */
28 class BackupValidator
29 {
30 /** @var string[] */
31 const LINE_BREAKS = [
32 "\r",
33 "\n",
34 "\r\n",
35 "\n\r",
36 PHP_EOL,
37 ];
38
39 /** @var BackupsFinder */
40 private $backupsFinder;
41
42 /** @var array */
43 protected $missingPartIssues = [];
44
45 /** @var array */
46 protected $partSizeIssues = [];
47
48 /** @var string */
49 protected $backupDir;
50
51 /** @var array */
52 protected $existingParts = [];
53
54 /** @var string */
55 protected $error = '';
56
57 /** @var Strings */
58 private $strings;
59
60 public function __construct(BackupsFinder $backupsFinder, Strings $strings)
61 {
62 $this->partSizeIssues = [];
63 $this->missingPartIssues = [];
64 $this->backupsFinder = $backupsFinder;
65 $this->backupDir = '';
66 $this->strings = $strings;
67 }
68
69 /** @return array */
70 public function getMissingPartIssues()
71 {
72 return $this->missingPartIssues;
73 }
74
75 /** @return array */
76 public function getPartSizeIssues()
77 {
78 return $this->partSizeIssues;
79 }
80
81 /** @return string */
82 public function getErrorMessage()
83 {
84 return $this->error;
85 }
86
87 /**
88 * @param FileObject $file
89 * @param BackupMetadata $metadata
90 * @return bool
91 */
92 public function validateFileIndex(FileObject $file, BackupMetadata $metadata)
93 {
94 // Early bail if not wpstg file
95 if ($file->getExtension() !== 'wpstg') {
96 return true;
97 }
98
99 $start = $metadata->getHeaderStart();
100 $end = $metadata->getHeaderEnd();
101 $backupFile = $this->strings->maskBackupFilename($file->getFilename());
102 if ($end - $start < 4) {
103 $error = sprintf(esc_html('File Index of %s not found!'), $backupFile);
104 debug_log($error);
105 $this->error = $error;
106
107 return false;
108 }
109
110 if (!$this->validateFileIndexFirstLine($file, $metadata)) {
111 return false;
112 }
113
114 $file->fseek($start);
115 $count = 0;
116 while ($file->valid() && $file->ftell() < $end) {
117 $line = $file->readAndMoveNext();
118 if (empty($line) || in_array($line, self::LINE_BREAKS)) {
119 continue;
120 }
121
122 $count++;
123 }
124
125 $totalFiles = $metadata->getTotalFiles();
126 if ($count !== $totalFiles && !$metadata->getIsMultipartBackup()) {
127 $error = sprintf(esc_html('File Index of %s is invalid! Actual number of files in the backup index: %s. Expected number of files: %s.'), $backupFile, $count, $totalFiles);
128 $this->error = $error;
129 debug_log($error);
130
131 return false;
132 }
133
134 if (!$metadata->getIsMultipartBackup()) {
135 return true;
136 }
137
138 $totalFiles = $metadata->getMultipartMetadata()->getTotalFiles();
139 if ($count !== $totalFiles && $metadata->getIsMultipartBackup()) {
140 $error = sprintf(esc_html('File Index of %s multipart backup is invalid! Actual number of files in the backup index: %s. Expected number of files: %s.'), $backupFile, $count, $totalFiles);
141 $this->error = $error;
142 debug_log($error);
143
144 return false;
145 }
146
147 return true;
148 }
149
150 /**
151 * @param FileObject $file
152 * @param BackupMetadata $metadata
153 * @return bool
154 */
155 public function validateFileIndexFirstLine(FileObject $file, BackupMetadata $metadata): bool
156 {
157 $version = $metadata->getBackupVersion();
158 if (version_compare($version, BackupHeader::MIN_BACKUP_VERSION, '>=')) {
159 return true;
160 }
161
162 $start = $metadata->getHeaderStart();
163 $file->fseek($start - 1);
164
165 if (!$file->valid()) {
166 return true;
167 }
168
169 $line = $file->readAndMoveNext();
170 if (in_array($line, self::LINE_BREAKS)) {
171 $line = $file->readAndMoveNext(); // first line is break line, that's fine, move to next then!
172 }
173
174 $backupFile = $this->strings->maskBackupFilename($file->getFilename());
175 if (!$this->strings->startsWith($line, 'wpstg_')) {
176 $error = sprintf(esc_html('File Index of %s is invalid! The file index first line does not begin with `wpstg_`. The current first line is: %s.'), $backupFile, $line);
177 $this->error = $error;
178 debug_log($error);
179
180 return false;
181 }
182
183 return true;
184 }
185
186 /**
187 * @return bool
188 * @throws BackupRuntimeException
189 */
190 public function checkIfSplitBackupIsValid(BackupMetadata $metadata): bool
191 {
192 $this->partSizeIssues = [];
193 $this->missingPartIssues = [];
194
195 // Early bail if not split backup
196 if (!$metadata->getIsMultipartBackup()) {
197 return true;
198 }
199
200 $this->backupDir = wp_normalize_path($this->backupsFinder->getBackupsDirectory());
201
202 $splitMetadata = $metadata->getMultipartMetadata();
203
204 foreach ($splitMetadata->getPluginsParts() as $part) {
205 $this->validatePart($part, 'plugins');
206 }
207
208 foreach ($splitMetadata->getThemesParts() as $part) {
209 $this->validatePart($part, 'themes');
210 }
211
212 foreach ($splitMetadata->getUploadsParts() as $part) {
213 $this->validatePart($part, 'uploads');
214 }
215
216 foreach ($splitMetadata->getMuPluginsParts() as $part) {
217 $this->validatePart($part, 'muplugins');
218 }
219
220 foreach ($splitMetadata->getOthersParts() as $part) {
221 $this->validatePart($part, 'others');
222 }
223
224 foreach ($splitMetadata->getOthersParts() as $part) {
225 $this->validatePart($part, 'otherWpRoot');
226 }
227
228 foreach ($splitMetadata->getDatabaseParts() as $part) {
229 $this->validatePart($part, 'database');
230 }
231
232 return empty($this->partSizeIssues) && empty($this->missingPartIssues);
233 }
234
235 /**
236 * @param BackupMetadata $metadata
237 * @return bool
238 */
239 public function isUnsupportedBackupVersion(BackupMetadata $metadata): bool
240 {
241 $isCreatedOnPro = $metadata->getCreatedOnPro();
242 $version = $metadata->getWpstgVersion();
243 if (!$isCreatedOnPro) {
244 return false;
245 }
246
247 return version_compare($version, RestoreRequirementsCheckTask::BETA_VERSION_LIMIT_PRO, '<');
248 }
249
250 /**
251 * @param string $part contains part name
252 * @param string $type (plugins|themes|uploads|muplugins|others|database)
253 *
254 * @return void
255 */
256 private function validatePart(string $part, string $type)
257 {
258 $path = $this->backupDir . str_replace($this->backupDir, '', wp_normalize_path(untrailingslashit($part)));
259 if (!file_exists($path)) {
260 $this->missingPartIssues[] = [
261 'name' => $part,
262 'type' => $type,
263 ];
264
265 return;
266 }
267
268 $metadata = new BackupMetadata();
269 $metadata = $metadata->hydrateByFilePath($path);
270
271 if (filesize($path) !== $metadata->getMultipartMetadata()->getPartSize()) {
272 $this->partSizeIssues[] = $part;
273 return;
274 }
275
276 $this->existingParts[] = $part;
277 }
278 }
279