PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 4.5.0
WP STAGING – WordPress Backup, Restore, Migration & Clone v4.5.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 / BackupHeader.php
wp-staging / Backup Last commit date
Ajax 5 months ago BackgroundProcessing 1 year ago Dto 6 months ago Entity 5 months ago Exceptions 1 year ago Interfaces 6 months ago Job 5 months ago Request 1 year ago Service 5 months ago Storage 8 months ago Task 5 months ago Traits 10 months ago AfterRestore.php 5 months ago BackupDeleter.php 6 months ago BackupDownload.php 8 months ago BackupFileIndex.php 1 year ago BackupGlitchReason.php 1 year ago BackupHeader.php 8 months ago BackupRepairer.php 6 months ago BackupRetentionHandler.php 1 year ago BackupScheduler.php 5 months ago BackupServiceProvider.php 10 months ago BackupValidator.php 6 months ago FileHeader.php 8 months ago FileHeaderAttribute.php 2 years ago WithBackupIdentifier.php 1 year ago
BackupHeader.php
439 lines
1 <?php
2
3 namespace WPStaging\Backup;
4
5 use WPStaging\Backup\Traits\EncodingErrorHandler;
6 use WPStaging\Framework\Filesystem\FileObject;
7 use WPStaging\Framework\Utils\DataEncoder;
8 use WPStaging\Framework\Utils\Version;
9
10 /**
11 * Backup Header class
12 * It will generate the header of the backup file
13 * and read the header of the backup file
14 * and update the header of the backup file
15 */
16 class BackupHeader
17 {
18 use EncodingErrorHandler;
19
20 /** @var string */
21 const WPSTG_SQL_BACKUP_DUMP_HEADER = "-- WP Staging SQL Backup Dump\n";
22
23 /**
24 * In Length
25 * @var int
26 */
27 const HEADER_SIZE = 512;
28
29 /**
30 * @var string
31 */
32 const HEADER_IN_USE_HEX_FORMAT = '48888';
33
34 /**
35 * File magic
36 * should not exceed 8 characters
37 * @var string
38 */
39 const MAGIC = "wpstg";
40
41 /**
42 * Magic size in length
43 * @var int
44 */
45 const MAGIC_SIZE = 8;
46
47 /**
48 * Minimum Backup version that support this new header
49 *
50 * @var string
51 */
52 const MIN_BACKUP_VERSION = '2.0.0';
53
54 /**
55 * Backup version
56 * Should not exceed 4-bytes unsigned limit 4294967295
57 * In the format X.Y.Z
58 * Where X is the major version and can be upto 429495 :)
59 * Where Y is the minor version and can be upto 99
60 * Where Z is the patch version and can be upto 99
61 *
62 * @var string
63 */
64 const BACKUP_VERSION = '2.0.0';
65
66 /**
67 * Original string should not exceed 64 characters for consistency
68 * Generated from running bin2hex(str_pad("originalString", 64, "\0", STR_PAD_RIGHT)) to 128 characters hex string
69 * To retrieve original string run hex2bin(this.constant)
70 * @var string
71 */
72 const COPYRIGHT_TEXT = '57502053746167696e672066696c6520666f726d61742062792052656e65204865726d656e617520262048617373616e20536861666971756520323032342f30';
73
74 /**
75 * Copyright text size
76 * @var int
77 */
78 const COPYRIGHT_TEXT_SIZE = 128;
79
80 /**
81 * @var string
82 */
83 private $magic;
84
85 /**
86 * @var int
87 */
88 private $backupVersion;
89
90 /**
91 * @var int
92 */
93 private $filesIndexStartOffset = 0;
94
95 /**
96 * @var int
97 */
98 private $filesIndexEndOffset = 0;
99
100 /**
101 * @var int
102 */
103 private $metadataStartOffset = 0;
104
105 /**
106 * @var int
107 */
108 private $metadataEndOffset = 0;
109
110 /**
111 * @var string
112 */
113 private $copyrightText;
114
115 /** @var DataEncoder */
116 private $encoder;
117
118 /** @var Version */
119 private $versionUtil;
120
121 /**
122 * BackupHeader constructor.
123 * @param DataEncoder $encoder
124 * @param Version $versionUtil
125 */
126 public function __construct(DataEncoder $encoder, Version $versionUtil)
127 {
128 $this->encoder = $encoder;
129 $this->versionUtil = $versionUtil;
130 $this->backupVersion = $this->versionUtil->convertStringFormatToIntFormat(self::BACKUP_VERSION);
131 }
132
133 /**
134 * Log encoding errors for backup header
135 *
136 * @param string $errorMessage The error message from DataEncoder
137 * @return void
138 */
139 private function logBackupHeaderEncodingError(string $errorMessage)
140 {
141 $context = [
142 'backupVersion' => $this->backupVersion,
143 'filesIndexStartOffset' => $this->filesIndexStartOffset,
144 'filesIndexEndOffset' => $this->filesIndexEndOffset,
145 'metadataStartOffset' => $this->metadataStartOffset,
146 'metadataEndOffset' => $this->metadataEndOffset,
147 ];
148
149 $this->logEncodingErrorWithContext(
150 $errorMessage,
151 $context,
152 'DataEncoder error in BackupHeader::getHeader(): %s. Using fallback values to continue backup.'
153 );
154 }
155
156 /**
157 * Apply fallback values for backup header properties that might be null
158 *
159 * @return void
160 */
161 private function applyBackupHeaderFallbackValues()
162 {
163 if ($this->backupVersion === null) {
164 $this->backupVersion = $this->versionUtil->convertStringFormatToIntFormat(self::BACKUP_VERSION);
165 }
166
167 if ($this->filesIndexStartOffset === null) {
168 $this->filesIndexStartOffset = 0;
169 }
170
171 if ($this->filesIndexEndOffset === null) {
172 $this->filesIndexEndOffset = 0;
173 }
174
175 if ($this->metadataStartOffset === null) {
176 $this->metadataStartOffset = 0;
177 }
178
179 if ($this->metadataEndOffset === null) {
180 $this->metadataEndOffset = 0;
181 }
182 }
183
184 /**
185 * Get backup version in XYYZZ integer format
186 *
187 * Where ZZ is the patch version from 00 to 99
188 * Where YY is the minor version from 00 to 99
189 * Where X is the major version from 0 to 429495
190 *
191 * @return int
192 */
193 public function getBackupVersion(): int
194 {
195 return $this->backupVersion;
196 }
197
198 /**
199 * Get backup version in X.Y.Z string format
200 *
201 * Where Z is the patch version from 0 to 99
202 * Where Y is the minor version from 0 to 99
203 * Where X is the major version from 0 to 429495
204 *
205 * @return string
206 */
207 public function getFormattedBackupVersion(): string
208 {
209 return $this->versionUtil->convertIntFormatToStringFormat($this->backupVersion);
210 }
211
212 public function getMetadataStartOffset(): int
213 {
214 return $this->metadataStartOffset;
215 }
216
217 public function setMetadataStartOffset(int $metadataStartOffset): BackupHeader
218 {
219 $this->metadataStartOffset = $metadataStartOffset;
220 return $this;
221 }
222
223 public function getMetadataEndOffset(): int
224 {
225 return $this->metadataEndOffset;
226 }
227
228 public function setMetadataEndOffset(int $metadataEndOffset): BackupHeader
229 {
230 $this->metadataEndOffset = $metadataEndOffset;
231 return $this;
232 }
233
234 public function getFilesIndexStartOffset(): int
235 {
236 return $this->filesIndexStartOffset;
237 }
238
239 public function setFilesIndexStartOffset(int $filesIndexStartOffset): BackupHeader
240 {
241 $this->filesIndexStartOffset = $filesIndexStartOffset;
242 return $this;
243 }
244
245 public function getFilesIndexEndOffset(): int
246 {
247 return $this->filesIndexEndOffset;
248 }
249
250 public function setFilesIndexEndOffset(int $filesIndexEndOffset): BackupHeader
251 {
252 $this->filesIndexEndOffset = $filesIndexEndOffset;
253 return $this;
254 }
255
256 /**
257 * @param string $backupFilePath
258 * @return BackupHeader
259 *
260 * @throws \RuntimeException
261 */
262 public function readFromPath(string $backupFilePath): BackupHeader
263 {
264 if (!file_exists($backupFilePath)) {
265 throw new \RuntimeException('Backup file not found');
266 }
267
268 $file = new FileObject($backupFilePath, FileObject::MODE_READ);
269 return $this->readFromFileObject($file);
270 }
271
272 /**
273 * @param FileObject $file
274 * @return BackupHeader
275 *
276 * @throws \RuntimeException
277 */
278 public function readFromFileObject(FileObject $file): BackupHeader
279 {
280 if ($file->getSize() < self::HEADER_SIZE) {
281 throw new \RuntimeException('Invalid v2 format backup file');
282 }
283
284 $file->seek(0);
285 $rawHeader = $file->fread(self::HEADER_SIZE);
286
287 return $this->setupBackupHeaderFromRaw($rawHeader);
288 }
289
290 /**
291 * @param string $rawHeader
292 *
293 * @throws InvalidArgumentException
294 * @return BackupHeader
295 */
296 public function setupBackupHeaderFromRaw(string $rawHeader): BackupHeader
297 {
298 $this->magic = rtrim(substr($rawHeader, 0, self::MAGIC_SIZE));
299 $this->copyrightText = substr($rawHeader, self::HEADER_SIZE - self::COPYRIGHT_TEXT_SIZE, self::COPYRIGHT_TEXT_SIZE); // Don't trim this, because it's fixed length with null characters
300
301 // Dynamic part of header currently in use
302 $dynamicHeader = substr($rawHeader, self::MAGIC_SIZE, $this->getHeaderInUseSize());
303 $headerIntData = $this->encoder->hexToIntArray(self::HEADER_IN_USE_HEX_FORMAT, $dynamicHeader);
304 // Change the below code into [$a, $b, $c, $d, $e] = $array format when min php is 7.1
305 $this->backupVersion = $headerIntData[0];
306 $this->filesIndexStartOffset = $headerIntData[1];
307 $this->filesIndexEndOffset = $headerIntData[2];
308 $this->metadataStartOffset = $headerIntData[3];
309 $this->metadataEndOffset = $headerIntData[4];
310
311 return $this;
312 }
313
314 public function isValidBackupHeader(): bool
315 {
316 if ($this->magic !== self::MAGIC) {
317 return false;
318 }
319
320 if ($this->copyrightText !== self::COPYRIGHT_TEXT) {
321 return false;
322 }
323
324 return version_compare($this->getFormattedBackupVersion(), self::MIN_BACKUP_VERSION, '>=');
325 }
326
327 public function getHeader(): string
328 {
329 try {
330 $encodedData = $this->encoder->intArrayToHex(
331 self::HEADER_IN_USE_HEX_FORMAT, // 36-bytes of hex data
332 [
333 $this->backupVersion,
334 $this->filesIndexStartOffset,
335 $this->filesIndexEndOffset,
336 $this->metadataStartOffset,
337 $this->metadataEndOffset,
338 ]
339 );
340 } catch (\InvalidArgumentException $e) {
341 // Log the error with context
342 $this->logBackupHeaderEncodingError($e->getMessage());
343
344 // Apply fallback values
345 $this->applyBackupHeaderFallbackValues();
346
347 // Retry with fallback values
348 $encodedData = $this->encoder->intArrayToHex(
349 self::HEADER_IN_USE_HEX_FORMAT,
350 [
351 $this->backupVersion,
352 $this->filesIndexStartOffset,
353 $this->filesIndexEndOffset,
354 $this->metadataStartOffset,
355 $this->metadataEndOffset,
356 ]
357 );
358 }
359
360 return sprintf(
361 '%s%s%s%s',
362 str_pad(self::MAGIC, self::MAGIC_SIZE, "\0", STR_PAD_RIGHT), // let write magic as it is without converting to hex
363 $encodedData,
364 bin2hex(str_pad("", $this->getUnusedBytesSize(), "\0", STR_PAD_RIGHT)),
365 self::COPYRIGHT_TEXT // 64-bytes of fixed hex data
366 );
367 }
368
369 /**
370 * @param string $backupFilePath
371 * @return void
372 */
373 public function updateHeader(string $backupFilePath)
374 {
375 $header = $this->getHeader();
376 $file = new FileObject($backupFilePath, 'r+');
377 $file->seek(0);
378 $file->fwrite($header);
379 $file = null;
380 }
381
382 /**
383 * Validate Old Backup Header
384 * @param string $content
385 * @return bool
386 */
387 public function verifyV1FormatHeader(string $content): bool
388 {
389 if (empty($content)) {
390 return false;
391 }
392
393 $wpstgBackupHeaderFileContent = self::WPSTG_SQL_BACKUP_DUMP_HEADER;
394 $headerToVerifyLength = strlen($wpstgBackupHeaderFileContent);
395 if (substr($wpstgBackupHeaderFileContent, 0, $headerToVerifyLength) === substr($content, 0, $headerToVerifyLength)) {
396 return true;
397 }
398
399 $wpstgBackupHeaderFile = WPSTG_RESOURCES_DIR . 'wpstgBackupHeader.txt';
400 if (!file_exists($wpstgBackupHeaderFile)) {
401 return true;
402 }
403
404 $wpstgBackupHeaderFileContent = file_get_contents($wpstgBackupHeaderFile);
405 $headerToVerifyLength = self::HEADER_SIZE;
406 if (!empty($wpstgBackupHeaderFileContent) && substr($wpstgBackupHeaderFileContent, 0, $headerToVerifyLength) === substr($content, 0, $headerToVerifyLength)) {
407 return true;
408 }
409
410 return false;
411 }
412
413 public function getV1FormatHeader(): string
414 {
415 $wpstgBackupHeaderFile = WPSTG_RESOURCES_DIR . 'wpstgBackupHeader.txt';
416 // Should not happen
417 if (!file_exists($wpstgBackupHeaderFile)) {
418 return "";
419 }
420
421 return file_get_contents($wpstgBackupHeaderFile);
422 }
423
424 private function getHeaderInUseSize(): int
425 {
426 $size = 0;
427 for ($i = 0; $i < strlen(self::HEADER_IN_USE_HEX_FORMAT); $i++) {
428 $size += intval(substr(self::HEADER_IN_USE_HEX_FORMAT, $i, 1));
429 }
430
431 return $size * 2;
432 }
433
434 private function getUnusedBytesSize(): int
435 {
436 return (self::HEADER_SIZE - $this->getHeaderInUseSize() - self::MAGIC_SIZE - self::COPYRIGHT_TEXT_SIZE) / 2;
437 }
438 }
439