PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backups, Restore, Migration & Clone / 4.9.5
WP STAGING – WordPress Backups, Restore, Migration & Clone v4.9.5
4.9.5 4.9.4 4.9.3 4.9.2 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 6 days ago BackgroundProcessing 1 year ago Dto 1 month ago Entity 1 week ago Exceptions 1 year ago FileHeader 2 months ago Interfaces 8 months ago Job 3 months ago Request 1 year ago Service 1 month ago Storage 2 months ago Task 6 days ago Traits 11 months ago Utils 6 days ago AfterRestore.php 6 months ago BackupDeleter.php 6 days ago BackupDownload.php 10 months ago BackupFileIndex.php 1 year ago BackupGlitchReason.php 1 year ago BackupHeader.php 2 months ago BackupRepairer.php 8 months ago BackupRetentionHandler.php 2 months ago BackupScheduler.php 1 month ago BackupServiceProvider.php 3 months ago BackupValidator.php 6 days ago FileHeader.php 1 month ago FileHeaderAttribute.php 2 years ago WithBackupIdentifier.php 6 days ago
BackupValidator.php
267 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\Task\Tasks\JobRestore\RestoreRequirementsCheckTask;
8 use WPStaging\Backup\Utils\BackupPathResolver;
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 array */
40 protected $missingPartIssues = [];
41
42 /** @var array */
43 protected $partSizeIssues = [];
44
45 /** @var string */
46 protected $backupFilename = '';
47
48 /** @var array */
49 protected $existingParts = [];
50
51 /** @var string */
52 protected $error = '';
53
54 /** @var Strings */
55 private $strings;
56
57 /** @var BackupPathResolver */
58 private $backupPathResolver;
59
60 public function __construct(Strings $strings, BackupPathResolver $backupPathResolver)
61 {
62 $this->partSizeIssues = [];
63 $this->missingPartIssues = [];
64 $this->strings = $strings;
65 $this->backupPathResolver = $backupPathResolver;
66 }
67
68 /** @return array */
69 public function getMissingPartIssues()
70 {
71 return $this->missingPartIssues;
72 }
73
74 /** @return array */
75 public function getPartSizeIssues()
76 {
77 return $this->partSizeIssues;
78 }
79
80 /** @return string */
81 public function getErrorMessage()
82 {
83 return $this->error;
84 }
85
86 /**
87 * @param FileObject $file
88 * @param BackupMetadata $metadata
89 * @return bool
90 */
91 public function validateFileIndex(FileObject $file, BackupMetadata $metadata)
92 {
93 // Early bail if not wpstg file
94 if ($file->getExtension() !== 'wpstg') {
95 return true;
96 }
97
98 $start = $metadata->getHeaderStart();
99 $end = $metadata->getHeaderEnd();
100 $backupFile = $this->strings->maskBackupFilename($file->getFilename());
101 if ($end - $start < 4) {
102 $error = sprintf(esc_html('File Index of %s not found!'), $backupFile);
103 debug_log($error);
104 $this->error = $error;
105
106 return false;
107 }
108
109 if (!$this->validateFileIndexFirstLine($file, $metadata)) {
110 return false;
111 }
112
113 $file->fseek($start);
114 $count = 0;
115 while ($file->valid() && $file->ftell() < $end) {
116 $line = $file->readAndMoveNext();
117 if (empty($line) || in_array($line, self::LINE_BREAKS)) {
118 continue;
119 }
120
121 $count++;
122 }
123
124 $totalFiles = $metadata->getTotalFiles();
125 if ($count !== $totalFiles && !$metadata->getIsMultipartBackup()) {
126 $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);
127 $this->error = $error;
128 debug_log($error);
129
130 return false;
131 }
132
133 if (!$metadata->getIsMultipartBackup()) {
134 return true;
135 }
136
137 $totalFiles = $metadata->getMultipartMetadata()->getTotalFiles();
138 if ($count !== $totalFiles && $metadata->getIsMultipartBackup()) {
139 $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);
140 $this->error = $error;
141 debug_log($error);
142
143 return false;
144 }
145
146 return true;
147 }
148
149 /**
150 * @param FileObject $file
151 * @param BackupMetadata $metadata
152 * @return bool
153 */
154 public function validateFileIndexFirstLine(FileObject $file, BackupMetadata $metadata): bool
155 {
156 $version = $metadata->getBackupVersion();
157 if (version_compare($version, BackupHeader::MIN_BACKUP_VERSION, '>=')) {
158 return true;
159 }
160
161 $start = $metadata->getHeaderStart();
162 $file->fseek($start - 1);
163
164 if (!$file->valid()) {
165 return true;
166 }
167
168 $line = $file->readAndMoveNext();
169 if (in_array($line, self::LINE_BREAKS)) {
170 $line = $file->readAndMoveNext(); // first line is break line, that's fine, move to next then!
171 }
172
173 $backupFile = $this->strings->maskBackupFilename($file->getFilename());
174 if (!$this->strings->startsWith($line, 'wpstg_')) {
175 $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);
176 $this->error = $error;
177 debug_log($error);
178
179 return false;
180 }
181
182 return true;
183 }
184
185 /**
186 * @param BackupMetadata $metadata
187 * @param string $backupFilename Filename of the backup the listed parts must belong to.
188 * @return bool
189 * @throws BackupRuntimeException
190 */
191 public function checkIfSplitBackupIsValid(BackupMetadata $metadata, string $backupFilename): bool
192 {
193 $this->partSizeIssues = [];
194 $this->missingPartIssues = [];
195
196 // Early bail if not split backup
197 if (!$metadata->getIsMultipartBackup()) {
198 return true;
199 }
200
201 $this->backupFilename = $backupFilename;
202
203 $splitMetadata = $metadata->getMultipartMetadata();
204
205 $partsByType = [
206 'plugins' => $splitMetadata->getPluginsParts(),
207 'themes' => $splitMetadata->getThemesParts(),
208 'uploads' => $splitMetadata->getUploadsParts(),
209 'muplugins' => $splitMetadata->getMuPluginsParts(),
210 'others' => $splitMetadata->getOthersParts(),
211 'otherWpRoot' => $splitMetadata->getOtherWpRootParts(),
212 'database' => $splitMetadata->getDatabaseParts(),
213 ];
214
215 foreach ($partsByType as $type => $parts) {
216 foreach ($parts as $part) {
217 $this->validatePart($part, $type);
218 }
219 }
220
221 return empty($this->partSizeIssues) && empty($this->missingPartIssues);
222 }
223
224 /**
225 * @param BackupMetadata $metadata
226 * @return bool
227 */
228 public function isUnsupportedBackupVersion(BackupMetadata $metadata): bool
229 {
230 $isCreatedOnPro = $metadata->getCreatedOnPro();
231 $version = $metadata->getWpstgVersion();
232 if (!$isCreatedOnPro) {
233 return false;
234 }
235
236 return version_compare($version, RestoreRequirementsCheckTask::BETA_VERSION_LIMIT_PRO, '<');
237 }
238
239 /**
240 * @param string $part
241 * @param string $type
242 * @return void
243 */
244 private function validatePart(string $part, string $type)
245 {
246 $path = $this->backupPathResolver->resolveBackupPartPath($part, $this->backupFilename);
247 if ($path === '' || !file_exists($path)) {
248 $this->missingPartIssues[] = [
249 'name' => $part,
250 'type' => $type,
251 ];
252
253 return;
254 }
255
256 $metadata = new BackupMetadata();
257 $metadata = $metadata->hydrateByFilePath($path);
258
259 if (filesize($path) !== $metadata->getMultipartMetadata()->getPartSize()) {
260 $this->partSizeIssues[] = $part;
261 return;
262 }
263
264 $this->existingParts[] = $part;
265 }
266 }
267