PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 4.3.2
WP STAGING – WordPress Backup, Restore, Migration & Clone v4.3.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 / FileHeader.php
wp-staging / Backup Last commit date
Ajax 8 months ago BackgroundProcessing 1 year ago Dto 8 months ago Entity 10 months ago Exceptions 1 year ago Interfaces 1 year ago Job 8 months ago Request 1 year ago Service 8 months ago Storage 8 months ago Task 8 months ago Traits 10 months ago AfterRestore.php 1 year ago BackupDeleter.php 1 year ago BackupDownload.php 8 months ago BackupFileIndex.php 1 year ago BackupGlitchReason.php 1 year ago BackupHeader.php 8 months ago BackupRepairer.php 1 year ago BackupRetentionHandler.php 1 year ago BackupScheduler.php 11 months ago BackupServiceProvider.php 10 months ago BackupValidator.php 8 months ago FileHeader.php 8 months ago FileHeaderAttribute.php 2 years ago WithBackupIdentifier.php 1 year ago
FileHeader.php
706 lines
1 <?php
2
3 namespace WPStaging\Backup;
4
5 use WPStaging\Backup\Interfaces\IndexLineInterface;
6 use WPStaging\Backup\Traits\EncodingErrorHandler;
7 use WPStaging\Framework\Filesystem\PathIdentifier;
8 use WPStaging\Framework\Job\Exception\FileValidationException;
9 use WPStaging\Framework\Traits\EndOfLinePlaceholderTrait;
10 use WPStaging\Framework\Traits\FormatTrait;
11 use WPStaging\Framework\Utils\DataEncoder;
12
13 class FileHeader implements IndexLineInterface
14 {
15 use EndOfLinePlaceholderTrait;
16 use FormatTrait;
17 use EncodingErrorHandler;
18
19 /**
20 * Packed Hex Code of `WPSTG`
21 * This constant represents an 48bit unsigned integer packed as a hex string.
22 * It is appended as it is to the start of the file header.
23 *
24 * Example:
25 * $hex = '47f6600b0200';
26 * to make it 8 bytes
27 * $hex = $hex . '0000';
28 * $bin = hex2bin($hex);
29 * $int = unpack('P', $bin)[1];
30 * echo $int; //8780838471 the original string
31 * 87 -> W
32 * 80 -> P
33 * 83 -> S
34 * 84 -> T
35 * 71 -> G
36 * @var string
37 */
38 const START_SIGNATURE = '47f6600b0200';
39
40 /** @var int */
41 const FILE_HEADER_FIXED_SIZE = 72;
42
43 /** @var int */
44 const INDEX_HEADER_FIXED_SIZE = 72;
45
46 /**
47 * @var string
48 * The File Header format without the start signature to make it compatible with 32bit PHP.
49 * This is a format string used with the DataEncoder class to encode/decode binary data. The format represents how integer values are packed into a binary header structure for backup files. Each character pair in the
50 * string represents the bit size of each field in the header (4=32bit, 5=40bit, 2=16bit, etc.), used when encoding arrays of integers into hexadecimal format for the file header.
51 *
52 * Example:
53 * $format = '44552424';
54 * $intArray = [123456789, 123456789, 123456789, 123456789];
55 * $hex = $encoder->intArrayToHex($format, $intArray); */
56 const FILE_HEADER_FORMAT = '44552424';
57
58 /** @var string */
59 const INDEX_HEADER_FORMAT = '644552424';
60
61 /** @var string */
62 const CRC32_CHECKSUM_ALGO = 'crc32b';
63
64 /** @var string */
65 private $startSignature;
66
67 /** @var int */
68 private $modifiedTime;
69
70 /** @var string */
71 private $crc32Checksum;
72
73 /** @var int */
74 private $crc32;
75
76 /** @var int */
77 private $compressedSize;
78
79 /** @var int */
80 private $uncompressedSize;
81
82 /** @var int */
83 private $attributes;
84
85 /** @var int */
86 private $extraFieldLength;
87
88 /** @var int */
89 private $fileNameLength;
90
91 /** @var int */
92 private $filePathLength;
93
94 /** @var int */
95 private $startOffset;
96
97 /** @var string */
98 private $filePath;
99
100 /** @var string */
101 private $fileName;
102
103 /** @var string */
104 private $extraField;
105
106 /** @var DataEncoder */
107 private $encoder;
108
109 private $pathIdentifier;
110
111 public function __construct(DataEncoder $encoder, PathIdentifier $pathIdentifier)
112 {
113 $this->encoder = $encoder;
114 $this->pathIdentifier = $pathIdentifier;
115 $this->resetHeader();
116 }
117
118 /**
119 * Log encoding errors with context about the file being processed
120 *
121 * @param string $method The method where the error occurred
122 * @param string $errorMessage The error message from DataEncoder
123 * @return void
124 */
125 private function logEncodingError(string $method, string $errorMessage)
126 {
127 $fileName = $this->getIdentifiablePath();
128 $context = [
129 'file' => $fileName ?: 'unknown',
130 'method' => $method,
131 'modifiedTime' => $this->modifiedTime,
132 'crc32' => $this->crc32,
133 'compressedSize' => $this->compressedSize,
134 'uncompressedSize' => $this->uncompressedSize,
135 'attributes' => $this->attributes,
136 ];
137
138 $logMessageTemplate = 'DataEncoder error in %s for file "' . ($fileName ?: 'unknown') .
139 '": %s. Using fallback values to continue backup.';
140
141 $this->logEncodingErrorWithContext($errorMessage, $context, $logMessageTemplate);
142 }
143
144 /**
145 * Apply fallback values for null properties to allow backup to continue
146 *
147 * @return void
148 */
149 private function applyFallbackValues()
150 {
151 // Apply fallback values for properties that might be null
152 if ($this->modifiedTime === null) {
153 $this->modifiedTime = time(); // Use current time as fallback
154 }
155
156 if ($this->crc32 === null) {
157 $this->crc32 = 0; // Use 0 as fallback for CRC32
158 }
159
160 if ($this->compressedSize === null) {
161 $this->compressedSize = 0; // Use 0 as fallback
162 }
163
164 if ($this->uncompressedSize === null) {
165 $this->uncompressedSize = 0; // Use 0 as fallback
166 }
167
168 if ($this->attributes === null) {
169 $this->attributes = 0; // Use 0 as fallback
170 }
171
172 if ($this->startOffset === null) {
173 $this->startOffset = 0; // Use 0 as fallback
174 }
175
176 // Ensure string lengths are not null
177 if ($this->filePathLength === null) {
178 $this->filePathLength = strlen($this->filePath ?: '');
179 }
180
181 if ($this->fileNameLength === null) {
182 $this->fileNameLength = strlen($this->fileName ?: '');
183 }
184
185 if ($this->extraFieldLength === null) {
186 $this->extraFieldLength = strlen($this->extraField ?: '');
187 }
188 }
189
190 /**
191 * Helper method to safely encode integer array with error handling and fallback values
192 *
193 * @param string $format The format string for encoding
194 * @param array $intArray The array of integers to encode
195 * @param string $method The calling method name for logging
196 * @return string The encoded hex string
197 */
198 private function encodeIntArrayToHex(string $format, array $intArray, string $method): string
199 {
200 try {
201 return $this->encoder->intArrayToHex($format, $intArray);
202 } catch (\InvalidArgumentException $e) {
203 // Log the error with context about which file is causing the issue
204 $this->logEncodingError($method, $e->getMessage());
205
206 // Use fallback values to allow backup to continue
207 $this->applyFallbackValues();
208
209 // Rebuild the array with current property values after fallback application
210 if ($method === 'getFileHeader' || $method === 'getUncompressedFileHeader') {
211 $fallbackArray = [
212 $this->modifiedTime,
213 $this->crc32,
214 $this->compressedSize,
215 $this->uncompressedSize,
216 $this->attributes,
217 $this->filePathLength,
218 $this->fileNameLength,
219 $this->extraFieldLength,
220 ];
221 } elseif ($method === 'getIndexHeader') {
222 $fallbackArray = [
223 $this->startOffset,
224 $this->modifiedTime,
225 $this->crc32,
226 $this->compressedSize,
227 $this->uncompressedSize,
228 $this->attributes,
229 $this->filePathLength,
230 $this->fileNameLength,
231 $this->extraFieldLength,
232 ];
233 } else {
234 // Default fallback - use the original array but replace nulls
235 $fallbackArray = $intArray;
236 foreach ($fallbackArray as $index => $value) {
237 if ($value === null) {
238 $fallbackArray[$index] = 0;
239 }
240 }
241 }
242
243 // Retry with fallback values
244 return $this->encoder->intArrayToHex($format, $fallbackArray);
245 }
246 }
247
248 /**
249 * @param string $filePath
250 * @param string $identifiablePath
251 * @return void
252 */
253 public function readFile(string $filePath, string $identifiablePath)
254 {
255 $fileInfo = new \SplFileInfo($filePath);
256 $this->setFileName($fileInfo->getFilename());
257
258 $convertedPath = $this->pathIdentifier->transformIdentifiableToPath($identifiablePath);
259 $convertedPathName = basename($convertedPath);
260
261 $path = substr($identifiablePath, 0, -strlen($convertedPathName));
262 $this->setFilePath($path);
263 $this->setExtraField("");
264 $this->setUncompressedSize($fileInfo->getSize());
265 $this->setCompressedSize($fileInfo->getSize());
266 $this->setModifiedTime($fileInfo->getMTime());
267 $this->setAttributes(0);
268 $this->setCrc32Checksum(hash_file(self::CRC32_CHECKSUM_ALGO, $filePath));
269 }
270
271 /**
272 * @param string $index
273 * @return void
274 * @throws \UnexpectedValueException
275 */
276 public function decodeFileHeader(string $index)
277 {
278 $index = rtrim($index);
279 $fixedHeader = substr($index, 0, self::FILE_HEADER_FIXED_SIZE);
280 $dynamicHeader = substr($index, self::FILE_HEADER_FIXED_SIZE);
281 if (strpos($fixedHeader, self::START_SIGNATURE) !== 0) {
282 throw new \UnexpectedValueException('Invalid file header');
283 }
284
285 $header = $this->encoder->hexToIntArray(self::FILE_HEADER_FORMAT, substr($fixedHeader, 12, self::FILE_HEADER_FIXED_SIZE - 12));
286 $this->setModifiedTime($header[0]);
287 $this->setCrc32($header[1]);
288 $this->setCompressedSize($header[2]);
289 $this->setUncompressedSize($header[3]);
290 $this->setAttributes($header[4]);
291 $this->filePathLength = $header[5];
292 $this->fileNameLength = $header[6];
293 $this->extraFieldLength = $header[7];
294 $this->setFilePath(substr($dynamicHeader, 0, $this->filePathLength));
295 $this->setFileName(substr($dynamicHeader, $this->filePathLength, $this->fileNameLength));
296 $this->setExtraField(substr($dynamicHeader, $this->filePathLength + $this->fileNameLength, $this->extraFieldLength));
297 }
298
299 /**
300 * @param string $index
301 * @return void
302 */
303 public function decodeIndexHeader(string $index)
304 {
305 $index = rtrim($index);
306 $fixedHeader = substr($index, 0, self::INDEX_HEADER_FIXED_SIZE);
307 $dynamicHeader = substr($index, self::INDEX_HEADER_FIXED_SIZE);
308 $header = $this->encoder->hexToIntArray(self::INDEX_HEADER_FORMAT, $fixedHeader);
309
310 $this->setStartOffset($header[0]);
311 $this->setModifiedTime($header[1]);
312 $this->setCrc32($header[2]);
313 $this->setCompressedSize($header[3]);
314 $this->setUncompressedSize($header[4]);
315 $this->setAttributes($header[5]);
316 $this->filePathLength = $header[6];
317 $this->fileNameLength = $header[7];
318 $this->extraFieldLength = $header[8];
319 $this->setFilePath(substr($dynamicHeader, 0, $this->filePathLength));
320 $this->setFileName(substr($dynamicHeader, $this->filePathLength, $this->fileNameLength));
321 $this->setExtraField(substr($dynamicHeader, $this->filePathLength + $this->fileNameLength, $this->extraFieldLength));
322 }
323
324 /**
325 * For compatibility with IndexLineInterface
326 * @param string $indexLine
327 * @return IndexLineInterface
328 */
329 public function readIndexLine(string $indexLine): IndexLineInterface
330 {
331 $this->decodeIndexHeader($indexLine);
332
333 return $this;
334 }
335
336 /**
337 * For compatibility with IndexLineInterface
338 * @param string $indexLine
339 * @return bool
340 */
341 public function isIndexLine(string $indexLine): bool
342 {
343 if (strlen($indexLine) <= self::INDEX_HEADER_FIXED_SIZE) {
344 return false;
345 }
346
347 return true;
348 }
349
350 public function getFileHeader(): string
351 {
352 $fixedHeader = $this->encodeIntArrayToHex(self::FILE_HEADER_FORMAT, [
353 $this->modifiedTime,
354 $this->crc32,
355 $this->compressedSize,
356 $this->uncompressedSize,
357 $this->attributes,
358 $this->filePathLength,
359 $this->fileNameLength,
360 $this->extraFieldLength,
361 ], 'getFileHeader');
362
363 $fileHeader = self::START_SIGNATURE . $fixedHeader . $this->filePath . $this->fileName . $this->extraField;
364 $fileHeader = $this->replaceEOLsWithPlaceholders($fileHeader);
365
366 return $fileHeader;
367 }
368
369 /**
370 * Used for repairing the file content in compressed file i.e. removing header inside the content. Issue: https://github.com/wp-staging/wp-staging-pro/issues/4241
371 * @return string
372 */
373 public function getUncompressedFileHeader(): string
374 {
375 // Force current attribute to be without compression, preserving them first to restore them later
376 $oldAttributes = $this->attributes;
377 $this->setIsCompressed(false);
378
379 $fixedHeader = $this->encodeIntArrayToHex(self::FILE_HEADER_FORMAT, [
380 $this->modifiedTime,
381 $this->crc32,
382 // Usually, it refers to the compressed size, but we need to set it to the uncompressed size because we initially add the file without compression and perform compression later.
383 // This is done to remove this file header within file content through search replace
384 $this->uncompressedSize,
385 $this->uncompressedSize,
386 $this->attributes,
387 $this->filePathLength,
388 $this->fileNameLength,
389 $this->extraFieldLength,
390 ], 'getUncompressedFileHeader');
391
392 $fileHeader = self::START_SIGNATURE . $fixedHeader . $this->filePath . $this->fileName . $this->extraField;
393 $fileHeader = $this->replaceEOLsWithPlaceholders($fileHeader);
394
395 $this->setAttributes($oldAttributes);
396
397 return $fileHeader;
398 }
399
400 public function getIndexHeader(): string
401 {
402 $fixedHeader = $this->encodeIntArrayToHex(self::INDEX_HEADER_FORMAT, [
403 $this->startOffset,
404 $this->modifiedTime,
405 $this->crc32,
406 $this->compressedSize,
407 $this->uncompressedSize,
408 $this->attributes,
409 $this->filePathLength,
410 $this->fileNameLength,
411 $this->extraFieldLength,
412 ], 'getIndexHeader');
413
414 $fixedHeader = $fixedHeader . $this->filePath . $this->fileName . $this->extraField;
415 $fixedHeader = $this->replaceEOLsWithPlaceholders($fixedHeader);
416
417 return $fixedHeader;
418 }
419
420 /**
421 * @return void
422 */
423 public function resetHeader()
424 {
425 $this->startSignature = '';
426 $this->modifiedTime = 0;
427 $this->crc32 = 0;
428 $this->crc32Checksum = '';
429 $this->compressedSize = 0;
430 $this->uncompressedSize = 0;
431 $this->setAttributes(0);
432 $this->extraFieldLength = 0;
433 $this->fileNameLength = 0;
434 $this->filePathLength = 0;
435 $this->startOffset = 0;
436 $this->filePath = '';
437 $this->fileName = '';
438 $this->extraField = '';
439 }
440
441 public function getStartSignature(): string
442 {
443 return $this->startSignature;
444 }
445
446 /**
447 * @return void
448 */
449 public function setStartSignature(string $startSignature)
450 {
451 $this->startSignature = $startSignature;
452 }
453
454 public function getModifiedTime(): int
455 {
456 return $this->modifiedTime;
457 }
458
459 /**
460 * @return void
461 */
462 public function setModifiedTime(int $modifiedTime)
463 {
464 $this->modifiedTime = $modifiedTime;
465 }
466
467 public function getCrc32(): int
468 {
469 return $this->crc32;
470 }
471
472 /**
473 * @return void
474 */
475 public function setCrc32(int $crc32)
476 {
477 $this->crc32 = $crc32;
478 $this->crc32Checksum = bin2hex(pack('N', $crc32));
479 }
480
481 public function getCrc32Checksum(): string
482 {
483 return $this->crc32Checksum;
484 }
485
486 /**
487 * @return void
488 */
489 public function setCrc32Checksum(string $crc32Checksum)
490 {
491 $this->crc32Checksum = $crc32Checksum;
492 $this->crc32 = unpack('N', pack('H*', $this->crc32Checksum))[1];
493 }
494
495 public function getCompressedSize(): int
496 {
497 return $this->compressedSize;
498 }
499
500 /**
501 * @return void
502 */
503 public function setCompressedSize(int $compressedSize)
504 {
505 $this->compressedSize = $compressedSize;
506 }
507
508 public function getUncompressedSize(): int
509 {
510 return $this->uncompressedSize;
511 }
512
513 /**
514 * @return void
515 */
516 public function setUncompressedSize(int $uncompressedSize)
517 {
518 $this->uncompressedSize = $uncompressedSize;
519 }
520
521 public function getAttributes(): int
522 {
523 return $this->attributes;
524 }
525
526 /**
527 * @return void
528 */
529 public function setAttributes(int $attributes)
530 {
531 $this->attributes = $attributes;
532 }
533
534 public function getIsCompressed(): bool
535 {
536 if ($this->attributes & FileHeaderAttribute::COMPRESSED) {
537 return true;
538 }
539
540 return false;
541 }
542
543 /**
544 * @return void
545 */
546 public function setIsCompressed(bool $isCompressed)
547 {
548 $isCompressed ?
549 $this->attributes |= FileHeaderAttribute::COMPRESSED :
550 $this->attributes &= ~FileHeaderAttribute::COMPRESSED;
551 }
552
553 public function getIsPreviousPartRequired(): bool
554 {
555 if ($this->attributes & FileHeaderAttribute::REQUIRE_PREVIOUS_PART) {
556 return true;
557 }
558
559 return false;
560 }
561
562 /**
563 * @return void
564 */
565 public function setIsPreviousPartRequired(bool $isPreviousPartRequired)
566 {
567 $isPreviousPartRequired ?
568 $this->attributes |= FileHeaderAttribute::REQUIRE_PREVIOUS_PART :
569 $this->attributes &= ~FileHeaderAttribute::REQUIRE_PREVIOUS_PART;
570 }
571
572 public function getIsNextPartRequired(): bool
573 {
574 if ($this->attributes & FileHeaderAttribute::REQUIRE_NEXT_PART) {
575 return true;
576 }
577
578 return false;
579 }
580
581 /**
582 * @return void
583 */
584 public function setIsNextPartRequired(bool $isNextPartRequired)
585 {
586 $isNextPartRequired ?
587 $this->attributes |= FileHeaderAttribute::REQUIRE_NEXT_PART :
588 $this->attributes &= ~FileHeaderAttribute::REQUIRE_NEXT_PART;
589 }
590
591 public function getStartOffset(): int
592 {
593 return $this->startOffset;
594 }
595
596 /**
597 * @return void
598 */
599 public function setStartOffset(int $startOffset)
600 {
601 $this->startOffset = $startOffset;
602 }
603
604 public function getFilePath(): string
605 {
606 return $this->filePath;
607 }
608
609 /**
610 * @return void
611 */
612 public function setFilePath(string $filePath)
613 {
614 $this->filePath = $filePath;
615 $filePathRenamed = $this->replaceEOLsWithPlaceholders($filePath);
616 $this->filePathLength = strlen($filePathRenamed);
617 }
618
619 public function getFileName(): string
620 {
621 return $this->fileName;
622 }
623
624 /**
625 * @return void
626 */
627 public function setFileName(string $fileName)
628 {
629 $this->fileName = $fileName;
630 $renamedFile = $this->replaceEOLsWithPlaceholders($fileName);
631 $this->fileNameLength = strlen($renamedFile);
632 }
633
634 public function getExtraField(): string
635 {
636 return $this->extraField;
637 }
638
639 /**
640 * @return void
641 */
642 public function setExtraField(string $extraField)
643 {
644 $this->extraField = $extraField;
645 $this->extraFieldLength = strlen($extraField);
646 }
647
648 public function getIdentifiablePath(): string
649 {
650 return $this->filePath . $this->fileName;
651 }
652
653 public function getDynamicHeaderLength(): int
654 {
655 return $this->filePathLength + $this->fileNameLength + $this->extraFieldLength;
656 }
657
658 public function getContentStartOffset(): int
659 {
660 return $this->startOffset + self::FILE_HEADER_FIXED_SIZE + $this->getDynamicHeaderLength() + 1;
661 }
662
663 /**
664 * @param string $filePath
665 * @param string $pathForErrorLogging
666 * @return void
667 * @throws FileValidationException
668 */
669 public function validateFile(string $filePath, string $pathForErrorLogging = '')
670 {
671 if (empty($pathForErrorLogging)) {
672 $pathForErrorLogging = $filePath;
673 }
674
675 if (!file_exists($filePath)) {
676 throw new FileValidationException(sprintf('File doesn\'t exist: %s.', $pathForErrorLogging));
677 }
678
679 $fileSize = filesize($filePath);
680 if ($this->getUncompressedSize() !== $fileSize) {
681 throw new FileValidationException(sprintf('Filesize validation failed for file %s. Expected: %s. Actual: %s', $pathForErrorLogging, $this->formatSize($this->getUncompressedSize(), 2), $this->formatSize($fileSize, 2)));
682 }
683
684 $crc32Checksum = hash_file(self::CRC32_CHECKSUM_ALGO, $filePath);
685 if ($this->crc32Checksum !== $crc32Checksum) {
686 throw new FileValidationException(sprintf('CRC32 Checksum validation failed for file %s. Expected: %s. Actual: %s', $pathForErrorLogging, $this->getCrc32Checksum(), $crc32Checksum));
687 }
688 }
689
690 public function toArray(): array
691 {
692 return [
693 'startOffset' => $this->getStartOffset(),
694 'modifiedTime' => $this->getModifiedTime(),
695 'crc32' => $this->getCrc32(),
696 'crc32Checksum' => $this->getCrc32Checksum(),
697 'compressedSize' => $this->getCompressedSize(),
698 'uncompressedSize' => $this->getUncompressedSize(),
699 'filePath' => $this->getFilePath(),
700 'fileName' => $this->getFileName(),
701 'extraField' => $this->getExtraField(),
702 'isCompressed' => $this->getIsCompressed(),
703 ];
704 }
705 }
706