PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backups, Restore, Migration & Clone / 4.11.0
WP STAGING – WordPress Backups, Restore, Migration & Clone v4.11.0
4.11.0 4.10.0 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 / FileHeader.php
wp-staging / Backup Last commit date
Ajax 1 day ago BackgroundProcessing 1 day ago Dto 1 day ago Entity 1 day ago Exceptions 1 day ago FileHeader 1 day ago Interfaces 1 day ago Job 1 day ago Request 1 day ago Service 1 day ago Storage 1 day ago Task 1 day ago Traits 1 day ago Utils 1 day ago AfterRestore.php 1 day ago BackupDeleter.php 1 day ago BackupDownload.php 1 day ago BackupFileIndex.php 1 day ago BackupGlitchReason.php 1 day ago BackupHeader.php 1 day ago BackupNextOffer.php 1 day ago BackupRepairer.php 1 day ago BackupRetentionHandler.php 1 day ago BackupScheduler.php 1 day ago BackupServiceProvider.php 1 day ago BackupValidator.php 1 day ago BeforeUpdateRowStatus.php 1 day ago FileHeader.php 1 day ago FileHeaderAttribute.php 1 day ago UpdateProtectionPausedNotice.php 1 day ago WithBackupIdentifier.php 1 day ago
FileHeader.php
823 lines
1 <?php
2
3 namespace WPStaging\Backup;
4
5 use WPStaging\Backup\FileHeader\ExtraFieldCodec;
6 use WPStaging\Backup\FileHeader\ExtraFieldType;
7 use WPStaging\Backup\Interfaces\IndexLineInterface;
8 use WPStaging\Backup\Traits\EncodingErrorHandler;
9 use WPStaging\Framework\Filesystem\PathIdentifier;
10 use WPStaging\Framework\Job\Exception\FileValidationException;
11 use WPStaging\Framework\Traits\EndOfLinePlaceholderTrait;
12 use WPStaging\Framework\Traits\FormatTrait;
13 use WPStaging\Framework\Utils\DataEncoder;
14
15 class FileHeader implements IndexLineInterface
16 {
17 use EndOfLinePlaceholderTrait;
18 use FormatTrait;
19 use EncodingErrorHandler;
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40 const START_SIGNATURE = '47f6600b0200';
41
42
43 const FILE_HEADER_FIXED_SIZE = 72;
44
45
46 const INDEX_HEADER_FIXED_SIZE = 72;
47
48
49
50
51
52
53
54
55
56
57
58 const FILE_HEADER_FORMAT = '44552424';
59
60
61 const INDEX_HEADER_FORMAT = '644552424';
62
63
64 const CRC32_CHECKSUM_ALGO = 'crc32b';
65
66
67 private $startSignature;
68
69
70 private $modifiedTime;
71
72
73 private $crc32Checksum;
74
75
76 private $crc32;
77
78
79 private $compressedSize;
80
81
82 private $uncompressedSize;
83
84
85 private $attributes;
86
87
88 private $extraFieldLength;
89
90
91 private $fileNameLength;
92
93
94 private $filePathLength;
95
96
97 private $startOffset;
98
99
100 private $filePath;
101
102
103 private $fileName;
104
105
106 private $extraField;
107
108
109 private $encoder;
110
111 private $pathIdentifier;
112
113 public function __construct(DataEncoder $encoder, PathIdentifier $pathIdentifier)
114 {
115 $this->encoder = $encoder;
116 $this->pathIdentifier = $pathIdentifier;
117 $this->resetHeader();
118 }
119
120
121
122
123
124
125
126
127 private function logEncodingError(string $method, string $errorMessage)
128 {
129 $fileName = $this->getIdentifiablePath();
130 $context = [
131 'file' => $fileName ?: 'unknown',
132 'method' => $method,
133 'modifiedTime' => $this->modifiedTime,
134 'crc32' => $this->crc32,
135 'compressedSize' => $this->compressedSize,
136 'uncompressedSize' => $this->uncompressedSize,
137 'attributes' => $this->attributes,
138 ];
139
140 $logMessageTemplate = 'DataEncoder error in %s for file "' . ($fileName ?: 'unknown') .
141 '": %s. Using fallback values to continue backup.';
142
143 $this->logEncodingErrorWithContext($errorMessage, $context, $logMessageTemplate);
144 }
145
146
147
148
149
150
151 private function applyFallbackValues()
152 {
153
154 if ($this->modifiedTime === null) {
155 $this->modifiedTime = time();
156 }
157
158 if ($this->crc32 === null) {
159 $this->crc32 = 0;
160 }
161
162 if ($this->compressedSize === null) {
163 $this->compressedSize = 0;
164 }
165
166 if ($this->uncompressedSize === null) {
167 $this->uncompressedSize = 0;
168 }
169
170 if ($this->attributes === null) {
171 $this->attributes = 0;
172 }
173
174 if ($this->startOffset === null) {
175 $this->startOffset = 0;
176 }
177
178
179 if ($this->filePathLength === null) {
180 $this->filePathLength = strlen($this->filePath ?: '');
181 }
182
183 if ($this->fileNameLength === null) {
184 $this->fileNameLength = strlen($this->fileName ?: '');
185 }
186
187 if ($this->extraFieldLength === null) {
188 $this->extraFieldLength = strlen($this->extraField ?: '');
189 }
190 }
191
192
193
194
195
196
197
198
199
200 private function encodeIntArrayToHex(string $format, array $intArray, string $method): string
201 {
202 try {
203 return $this->encoder->intArrayToHex($format, $intArray);
204 } catch (\InvalidArgumentException $e) {
205
206 $this->logEncodingError($method, $e->getMessage());
207
208
209 $this->applyFallbackValues();
210
211
212 if ($method === 'getFileHeader' || $method === 'getUncompressedFileHeader') {
213 $fallbackArray = [
214 $this->modifiedTime,
215 $this->crc32,
216 $this->compressedSize,
217 $this->uncompressedSize,
218 $this->attributes,
219 $this->filePathLength,
220 $this->fileNameLength,
221 $this->extraFieldLength,
222 ];
223 } elseif ($method === 'getIndexHeader') {
224 $fallbackArray = [
225 $this->startOffset,
226 $this->modifiedTime,
227 $this->crc32,
228 $this->compressedSize,
229 $this->uncompressedSize,
230 $this->attributes,
231 $this->filePathLength,
232 $this->fileNameLength,
233 $this->extraFieldLength,
234 ];
235 } else {
236
237 $fallbackArray = $intArray;
238 foreach ($fallbackArray as $index => $value) {
239 if ($value === null) {
240 $fallbackArray[$index] = 0;
241 }
242 }
243 }
244
245
246 return $this->encoder->intArrayToHex($format, $fallbackArray);
247 }
248 }
249
250
251
252
253
254
255
256
257
258
259 public function readFile(string $filePath, string $identifiablePath, bool $skipChecksum = false)
260 {
261 $fileInfo = new \SplFileInfo($filePath);
262 $this->setFileName($fileInfo->getFilename());
263
264 $convertedPath = $this->pathIdentifier->transformIdentifiableToPath($identifiablePath);
265 $convertedPathName = basename($convertedPath);
266
267 $path = substr($identifiablePath, 0, -strlen($convertedPathName));
268 $this->setFilePath($path);
269 $this->setExtraField("");
270 $this->setUncompressedSize($fileInfo->getSize());
271 $this->setCompressedSize($fileInfo->getSize());
272 $this->setModifiedTime($fileInfo->getMTime());
273 $this->setAttributes(0);
274
275 if ($skipChecksum) {
276
277
278 return;
279 }
280
281 $this->setCrc32Checksum(hash_file(self::CRC32_CHECKSUM_ALGO, $filePath));
282 }
283
284
285
286
287
288
289 public function decodeFileHeader(string $index)
290 {
291 $index = $this->trimTrailingLineBreak($index);
292 $fixedHeader = substr($index, 0, self::FILE_HEADER_FIXED_SIZE);
293 $dynamicHeader = substr($index, self::FILE_HEADER_FIXED_SIZE);
294 if (strpos($fixedHeader, self::START_SIGNATURE) !== 0) {
295 throw new \UnexpectedValueException('Invalid file header');
296 }
297
298 $header = $this->encoder->hexToIntArray(self::FILE_HEADER_FORMAT, substr($fixedHeader, 12, self::FILE_HEADER_FIXED_SIZE - 12));
299 $this->setModifiedTime($header[0]);
300 $this->setCrc32($header[1]);
301 $this->setCompressedSize($header[2]);
302 $this->setUncompressedSize($header[3]);
303 $this->setAttributes($header[4]);
304 $this->filePathLength = $header[5];
305 $this->fileNameLength = $header[6];
306 $this->extraFieldLength = $header[7];
307 $this->setFilePath(substr($dynamicHeader, 0, $this->filePathLength));
308 $this->setFileName(substr($dynamicHeader, $this->filePathLength, $this->fileNameLength));
309 $this->setExtraField($this->replacePlaceholdersWithEOLs(substr($dynamicHeader, $this->filePathLength + $this->fileNameLength, $this->extraFieldLength)));
310 }
311
312
313
314
315
316 public function decodeIndexHeader(string $index)
317 {
318 $index = $this->trimTrailingLineBreak($index);
319 $fixedHeader = substr($index, 0, self::INDEX_HEADER_FIXED_SIZE);
320 $dynamicHeader = substr($index, self::INDEX_HEADER_FIXED_SIZE);
321 $header = $this->encoder->hexToIntArray(self::INDEX_HEADER_FORMAT, $fixedHeader);
322
323 $this->setStartOffset($header[0]);
324 $this->setModifiedTime($header[1]);
325 $this->setCrc32($header[2]);
326 $this->setCompressedSize($header[3]);
327 $this->setUncompressedSize($header[4]);
328 $this->setAttributes($header[5]);
329 $this->filePathLength = $header[6];
330 $this->fileNameLength = $header[7];
331 $this->extraFieldLength = $header[8];
332 $this->setFilePath(substr($dynamicHeader, 0, $this->filePathLength));
333 $this->setFileName(substr($dynamicHeader, $this->filePathLength, $this->fileNameLength));
334 $this->setExtraField($this->replacePlaceholdersWithEOLs(substr($dynamicHeader, $this->filePathLength + $this->fileNameLength, $this->extraFieldLength)));
335 }
336
337
338
339
340
341
342
343 private function trimTrailingLineBreak(string $line): string
344 {
345 if (substr($line, -2) === "\r\n") {
346 return substr($line, 0, -2);
347 }
348
349 if (substr($line, -1) === "\n") {
350 return substr($line, 0, -1);
351 }
352
353 return $line;
354 }
355
356
357
358
359
360
361 public function readIndexLine(string $indexLine): IndexLineInterface
362 {
363 $this->decodeIndexHeader($indexLine);
364
365 return $this;
366 }
367
368
369
370
371
372
373 public function isIndexLine(string $indexLine): bool
374 {
375 if (strlen($indexLine) <= self::INDEX_HEADER_FIXED_SIZE) {
376 return false;
377 }
378
379 return true;
380 }
381
382 public function getFileHeader(): string
383 {
384 $fixedHeader = $this->encodeIntArrayToHex(self::FILE_HEADER_FORMAT, [
385 $this->modifiedTime,
386 $this->crc32,
387 $this->compressedSize,
388 $this->uncompressedSize,
389 $this->attributes,
390 $this->filePathLength,
391 $this->fileNameLength,
392 $this->extraFieldLength,
393 ], 'getFileHeader');
394
395 $fileHeader = self::START_SIGNATURE . $fixedHeader . $this->filePath . $this->fileName . $this->extraField;
396 $fileHeader = $this->replaceEOLsWithPlaceholders($fileHeader);
397
398 return $fileHeader;
399 }
400
401
402
403
404
405 public function getUncompressedFileHeader(): string
406 {
407
408 $oldAttributes = $this->attributes;
409 $this->setIsCompressed(false);
410
411 $fixedHeader = $this->encodeIntArrayToHex(self::FILE_HEADER_FORMAT, [
412 $this->modifiedTime,
413 $this->crc32,
414
415
416 $this->uncompressedSize,
417 $this->uncompressedSize,
418 $this->attributes,
419 $this->filePathLength,
420 $this->fileNameLength,
421 $this->extraFieldLength,
422 ], 'getUncompressedFileHeader');
423
424 $fileHeader = self::START_SIGNATURE . $fixedHeader . $this->filePath . $this->fileName . $this->extraField;
425 $fileHeader = $this->replaceEOLsWithPlaceholders($fileHeader);
426
427 $this->setAttributes($oldAttributes);
428
429 return $fileHeader;
430 }
431
432 public function getIndexHeader(): string
433 {
434 $fixedHeader = $this->encodeIntArrayToHex(self::INDEX_HEADER_FORMAT, [
435 $this->startOffset,
436 $this->modifiedTime,
437 $this->crc32,
438 $this->compressedSize,
439 $this->uncompressedSize,
440 $this->attributes,
441 $this->filePathLength,
442 $this->fileNameLength,
443 $this->extraFieldLength,
444 ], 'getIndexHeader');
445
446 $fixedHeader = $fixedHeader . $this->filePath . $this->fileName . $this->extraField;
447 $fixedHeader = $this->replaceEOLsWithPlaceholders($fixedHeader);
448
449 return $fixedHeader;
450 }
451
452
453
454
455 public function resetHeader()
456 {
457 $this->startSignature = '';
458 $this->modifiedTime = 0;
459 $this->crc32 = 0;
460 $this->crc32Checksum = '';
461 $this->compressedSize = 0;
462 $this->uncompressedSize = 0;
463 $this->setAttributes(0);
464 $this->extraFieldLength = 0;
465 $this->fileNameLength = 0;
466 $this->filePathLength = 0;
467 $this->startOffset = 0;
468 $this->filePath = '';
469 $this->fileName = '';
470 $this->extraField = '';
471 }
472
473 public function getStartSignature(): string
474 {
475 return $this->startSignature;
476 }
477
478
479
480
481 public function setStartSignature(string $startSignature)
482 {
483 $this->startSignature = $startSignature;
484 }
485
486 public function getModifiedTime(): int
487 {
488 return $this->modifiedTime;
489 }
490
491
492
493
494 public function setModifiedTime(int $modifiedTime)
495 {
496 $this->modifiedTime = $modifiedTime;
497 }
498
499 public function getCrc32(): int
500 {
501 return $this->crc32;
502 }
503
504
505
506
507 public function setCrc32(int $crc32)
508 {
509 $this->crc32 = $crc32;
510 $this->crc32Checksum = bin2hex(pack('N', $crc32));
511 }
512
513 public function getCrc32Checksum(): string
514 {
515 return $this->crc32Checksum;
516 }
517
518
519
520
521 public function setCrc32Checksum(string $crc32Checksum)
522 {
523 $this->crc32Checksum = $crc32Checksum;
524 $this->crc32 = unpack('N', pack('H*', $this->crc32Checksum))[1];
525 }
526
527 public function getCompressedSize(): int
528 {
529 return $this->compressedSize;
530 }
531
532
533
534
535 public function setCompressedSize(int $compressedSize)
536 {
537 $this->compressedSize = $compressedSize;
538 }
539
540 public function getUncompressedSize(): int
541 {
542 return $this->uncompressedSize;
543 }
544
545
546
547
548 public function setUncompressedSize(int $uncompressedSize)
549 {
550 $this->uncompressedSize = $uncompressedSize;
551 }
552
553 public function getAttributes(): int
554 {
555 return $this->attributes;
556 }
557
558
559
560
561 public function setAttributes(int $attributes)
562 {
563 $this->attributes = $attributes;
564 }
565
566 public function getIsCompressed(): bool
567 {
568 if ($this->attributes & FileHeaderAttribute::COMPRESSED) {
569 return true;
570 }
571
572 return false;
573 }
574
575
576
577
578 public function setIsCompressed(bool $isCompressed)
579 {
580 $isCompressed ?
581 $this->attributes |= FileHeaderAttribute::COMPRESSED :
582 $this->attributes &= ~FileHeaderAttribute::COMPRESSED;
583 }
584
585 public function getIsPreviousPartRequired(): bool
586 {
587 if ($this->attributes & FileHeaderAttribute::REQUIRE_PREVIOUS_PART) {
588 return true;
589 }
590
591 return false;
592 }
593
594
595
596
597 public function setIsPreviousPartRequired(bool $isPreviousPartRequired)
598 {
599 $isPreviousPartRequired ?
600 $this->attributes |= FileHeaderAttribute::REQUIRE_PREVIOUS_PART :
601 $this->attributes &= ~FileHeaderAttribute::REQUIRE_PREVIOUS_PART;
602 }
603
604 public function getIsNextPartRequired(): bool
605 {
606 if ($this->attributes & FileHeaderAttribute::REQUIRE_NEXT_PART) {
607 return true;
608 }
609
610 return false;
611 }
612
613
614
615
616 public function setIsNextPartRequired(bool $isNextPartRequired)
617 {
618 $isNextPartRequired ?
619 $this->attributes |= FileHeaderAttribute::REQUIRE_NEXT_PART :
620 $this->attributes &= ~FileHeaderAttribute::REQUIRE_NEXT_PART;
621 }
622
623 public function getStartOffset(): int
624 {
625 return $this->startOffset;
626 }
627
628
629
630
631
632
633
634
635
636 public function setMultipartTailMetadata(int $wholeFileSize, string $wholeFileCrc)
637 {
638 $this->setExtraFieldEntry(ExtraFieldType::TAIL, sprintf('%d:%s', $wholeFileSize, $wholeFileCrc));
639 }
640
641
642
643
644
645
646
647
648 public function getMultipartTailMetadata()
649 {
650 $payload = $this->getExtraFieldEntry(ExtraFieldType::TAIL);
651 if ($payload === null) {
652 return null;
653 }
654
655 $parts = explode(':', $payload, 2);
656 if (count($parts) !== 2 || $parts[0] === '' || $parts[1] === '') {
657 return null;
658 }
659
660 return [
661 'wholeFileSize' => (int) $parts[0],
662 'wholeFileCRC' => $parts[1],
663 ];
664 }
665
666
667
668
669 public function setStartOffset(int $startOffset)
670 {
671 $this->startOffset = $startOffset;
672 }
673
674 public function getFilePath(): string
675 {
676 return $this->filePath;
677 }
678
679
680
681
682 public function setFilePath(string $filePath)
683 {
684 $this->filePath = $filePath;
685 $filePathRenamed = $this->replaceEOLsWithPlaceholders($filePath);
686 $this->filePathLength = strlen($filePathRenamed);
687 }
688
689 public function getFileName(): string
690 {
691 return $this->fileName;
692 }
693
694
695
696
697 public function setFileName(string $fileName)
698 {
699 $this->fileName = $fileName;
700 $renamedFile = $this->replaceEOLsWithPlaceholders($fileName);
701 $this->fileNameLength = strlen($renamedFile);
702 }
703
704 public function getExtraField(): string
705 {
706 return $this->extraField;
707 }
708
709
710
711
712 public function setExtraField(string $extraField)
713 {
714 $this->extraField = $extraField;
715
716
717
718
719
720 $this->extraFieldLength = strlen($this->replaceEOLsWithPlaceholders($extraField));
721 }
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737 public function setExtraFieldEntry(int $type, string $value)
738 {
739 if ($type === ExtraFieldType::LEGACY_RAW) {
740 throw new \UnexpectedValueException(sprintf('FileHeader::setExtraFieldEntry: type 0x%02X (LEGACY_RAW) is a parser-only sentinel and cannot be written.', ExtraFieldType::LEGACY_RAW));
741 }
742
743 $codec = new ExtraFieldCodec();
744 $entries = $codec->decode($this->extraField);
745 if (isset($entries[ExtraFieldType::LEGACY_RAW])) {
746 throw new \UnexpectedValueException('FileHeader::setExtraFieldEntry: refusing to add a TLV entry to a non-TLV extraField; opaque legacy bytes would be destroyed. Reset the field with setExtraField("") first if this is intentional.');
747 }
748
749 $entries[$type] = $value;
750 $this->setExtraField($codec->encode($entries));
751 }
752
753
754
755
756
757
758
759 public function getExtraFieldEntry(int $type)
760 {
761 $entries = (new ExtraFieldCodec())->decode($this->extraField);
762 return isset($entries[$type]) ? $entries[$type] : null;
763 }
764
765 public function getIdentifiablePath(): string
766 {
767 return $this->filePath . $this->fileName;
768 }
769
770 public function getDynamicHeaderLength(): int
771 {
772 return $this->filePathLength + $this->fileNameLength + $this->extraFieldLength;
773 }
774
775 public function getContentStartOffset(): int
776 {
777 return $this->startOffset + self::FILE_HEADER_FIXED_SIZE + $this->getDynamicHeaderLength() + 1;
778 }
779
780
781
782
783
784
785
786 public function validateFile(string $filePath, string $pathForErrorLogging = '')
787 {
788 if (empty($pathForErrorLogging)) {
789 $pathForErrorLogging = $filePath;
790 }
791
792 if (!file_exists($filePath)) {
793 throw new FileValidationException(sprintf('File doesn\'t exist: %s.', $pathForErrorLogging));
794 }
795
796 $fileSize = filesize($filePath);
797 if ($this->getUncompressedSize() !== $fileSize) {
798 throw new FileValidationException(sprintf('Filesize validation failed for file %s. Expected: %s. Actual: %s', $pathForErrorLogging, $this->formatSize($this->getUncompressedSize(), 2), $this->formatSize($fileSize, 2)));
799 }
800
801 $crc32Checksum = hash_file(self::CRC32_CHECKSUM_ALGO, $filePath);
802 if ($this->crc32Checksum !== $crc32Checksum) {
803 throw new FileValidationException(sprintf('CRC32 Checksum validation failed for file %s. Expected: %s. Actual: %s', $pathForErrorLogging, $this->getCrc32Checksum(), $crc32Checksum));
804 }
805 }
806
807 public function toArray(): array
808 {
809 return [
810 'startOffset' => $this->getStartOffset(),
811 'modifiedTime' => $this->getModifiedTime(),
812 'crc32' => $this->getCrc32(),
813 'crc32Checksum' => $this->getCrc32Checksum(),
814 'compressedSize' => $this->getCompressedSize(),
815 'uncompressedSize' => $this->getUncompressedSize(),
816 'filePath' => $this->getFilePath(),
817 'fileName' => $this->getFileName(),
818 'extraField' => $this->getExtraField(),
819 'isCompressed' => $this->getIsCompressed(),
820 ];
821 }
822 }
823