PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 4.2.0
WP STAGING – WordPress Backup, Restore, Migration & Clone v4.2.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 / FileHeader.php
wp-staging / Backup Last commit date
Ajax 1 year ago BackgroundProcessing 1 year ago Dto 1 year ago Entity 1 year ago Exceptions 1 year ago Interfaces 1 year ago Job 1 year ago Request 1 year ago Service 1 year ago Storage 1 year ago Task 1 year ago AfterRestore.php 1 year ago BackupDeleter.php 1 year ago BackupDownload.php 1 year ago BackupFileIndex.php 1 year ago BackupGlitchReason.php 1 year ago BackupHeader.php 1 year ago BackupRepairer.php 1 year ago BackupRetentionHandler.php 1 year ago BackupScheduler.php 1 year ago BackupServiceProvider.php 1 year ago BackupValidator.php 1 year ago FileHeader.php 1 year ago FileHeaderAttribute.php 2 years ago WithBackupIdentifier.php 1 year ago
FileHeader.php
550 lines
1 <?php
2
3 namespace WPStaging\Backup;
4
5 use WPStaging\Backup\Interfaces\IndexLineInterface;
6 use WPStaging\Framework\Filesystem\PathIdentifier;
7 use WPStaging\Framework\Job\Exception\FileValidationException;
8 use WPStaging\Framework\Traits\EndOfLinePlaceholderTrait;
9 use WPStaging\Framework\Traits\FormatTrait;
10 use WPStaging\Framework\Utils\DataEncoder;
11
12 class FileHeader implements IndexLineInterface
13 {
14 use EndOfLinePlaceholderTrait;
15 use FormatTrait;
16
17 /**
18 * Packed Hex Code of `WPSTG`
19 * This constant represents an 48bit unsigned integer packed as a hex string.
20 * It is appended as it is to the start of the file header.
21 *
22 * Example:
23 * $hex = '47f6600b0200';
24 * to make it 8 bytes
25 * $hex = $hex . '0000';
26 * $bin = hex2bin($hex);
27 * $int = unpack('P', $bin)[1];
28 * echo $int; //8780838471 the original string
29 * 87 -> W
30 * 80 -> P
31 * 83 -> S
32 * 84 -> T
33 * 71 -> G
34 * @var string
35 */
36 const START_SIGNATURE = '47f6600b0200';
37
38 /** @var int */
39 const FILE_HEADER_FIXED_SIZE = 72;
40
41 /** @var int */
42 const INDEX_HEADER_FIXED_SIZE = 72;
43
44 /**
45 * @var string
46 * The File Header format without the start signature to make it compatible with 32bit PHP
47 */
48 const FILE_HEADER_FORMAT = '44552424';
49
50 /** @var string */
51 const INDEX_HEADER_FORMAT = '644552424';
52
53 /** @var string */
54 const CRC32_CHECKSUM_ALGO = 'crc32b';
55
56 /** @var string */
57 private $startSignature;
58
59 /** @var int */
60 private $modifiedTime;
61
62 /** @var string */
63 private $crc32Checksum;
64
65 /** @var int */
66 private $crc32;
67
68 /** @var int */
69 private $compressedSize;
70
71 /** @var int */
72 private $uncompressedSize;
73
74 /** @var int */
75 private $attributes;
76
77 /** @var int */
78 private $extraFieldLength;
79
80 /** @var int */
81 private $fileNameLength;
82
83 /** @var int */
84 private $filePathLength;
85
86 /** @var int */
87 private $startOffset;
88
89 /** @var string */
90 private $filePath;
91
92 /** @var string */
93 private $fileName;
94
95 /** @var string */
96 private $extraField;
97
98 /** @var DataEncoder */
99 private $encoder;
100
101 private $pathIdentifier;
102
103 public function __construct(DataEncoder $encoder, PathIdentifier $pathIdentifier)
104 {
105 $this->encoder = $encoder;
106 $this->pathIdentifier = $pathIdentifier;
107 $this->resetHeader();
108 }
109
110 /**
111 * @param string $filePath
112 * @param string $identifiablePath
113 * @return void
114 */
115 public function readFile(string $filePath, string $identifiablePath)
116 {
117 $fileInfo = new \SplFileInfo($filePath);
118 $this->setFileName($fileInfo->getFilename());
119
120 $convertedPath = $this->pathIdentifier->transformIdentifiableToPath($identifiablePath);
121 $convertedPathName = basename($convertedPath);
122
123 $path = substr($identifiablePath, 0, -strlen($convertedPathName));
124 $this->setFilePath($path);
125 $this->setExtraField("");
126 $this->setUncompressedSize($fileInfo->getSize());
127 $this->setCompressedSize($fileInfo->getSize());
128 $this->setModifiedTime($fileInfo->getMTime());
129 $this->setAttributes(0);
130 $this->setCrc32Checksum(hash_file(self::CRC32_CHECKSUM_ALGO, $filePath));
131 }
132
133 /**
134 * @param string $index
135 * @return void
136 * @throws \UnexpectedValueException
137 */
138 public function decodeFileHeader(string $index)
139 {
140 $index = rtrim($index);
141 $fixedHeader = substr($index, 0, self::FILE_HEADER_FIXED_SIZE);
142 $dynamicHeader = substr($index, self::FILE_HEADER_FIXED_SIZE);
143 if (strpos($fixedHeader, self::START_SIGNATURE) !== 0) {
144 throw new \UnexpectedValueException('Invalid file header');
145 }
146
147 $header = $this->encoder->hexToIntArray(self::FILE_HEADER_FORMAT, substr($fixedHeader, 12, self::FILE_HEADER_FIXED_SIZE - 12));
148 $this->setModifiedTime($header[0]);
149 $this->setCrc32($header[1]);
150 $this->setCompressedSize($header[2]);
151 $this->setUncompressedSize($header[3]);
152 $this->setAttributes($header[4]);
153 $this->filePathLength = $header[5];
154 $this->fileNameLength = $header[6];
155 $this->extraFieldLength = $header[7];
156 $this->setFilePath(substr($dynamicHeader, 0, $this->filePathLength));
157 $this->setFileName(substr($dynamicHeader, $this->filePathLength, $this->fileNameLength));
158 $this->setExtraField(substr($dynamicHeader, $this->filePathLength + $this->fileNameLength, $this->extraFieldLength));
159 }
160
161 /**
162 * @param string $index
163 * @return void
164 */
165 public function decodeIndexHeader(string $index)
166 {
167 $index = rtrim($index);
168 $fixedHeader = substr($index, 0, self::INDEX_HEADER_FIXED_SIZE);
169 $dynamicHeader = substr($index, self::INDEX_HEADER_FIXED_SIZE);
170 $header = $this->encoder->hexToIntArray(self::INDEX_HEADER_FORMAT, $fixedHeader);
171
172 $this->setStartOffset($header[0]);
173 $this->setModifiedTime($header[1]);
174 $this->setCrc32($header[2]);
175 $this->setCompressedSize($header[3]);
176 $this->setUncompressedSize($header[4]);
177 $this->setAttributes($header[5]);
178 $this->filePathLength = $header[6];
179 $this->fileNameLength = $header[7];
180 $this->extraFieldLength = $header[8];
181 $this->setFilePath(substr($dynamicHeader, 0, $this->filePathLength));
182 $this->setFileName(substr($dynamicHeader, $this->filePathLength, $this->fileNameLength));
183 $this->setExtraField(substr($dynamicHeader, $this->filePathLength + $this->fileNameLength, $this->extraFieldLength));
184 }
185
186 /**
187 * For compatibility with IndexLineInterface
188 * @param string $indexLine
189 * @return IndexLineInterface
190 */
191 public function readIndexLine(string $indexLine): IndexLineInterface
192 {
193 $this->decodeIndexHeader($indexLine);
194
195 return $this;
196 }
197
198 /**
199 * For compatibility with IndexLineInterface
200 * @param string $indexLine
201 * @return bool
202 */
203 public function isIndexLine(string $indexLine): bool
204 {
205 if (strlen($indexLine) <= self::INDEX_HEADER_FIXED_SIZE) {
206 return false;
207 }
208
209 return true;
210 }
211
212 public function getFileHeader(): string
213 {
214 $fixedHeader = $this->encoder->intArrayToHex(self::FILE_HEADER_FORMAT, [
215 $this->modifiedTime,
216 $this->crc32,
217 $this->compressedSize,
218 $this->uncompressedSize,
219 $this->attributes,
220 $this->filePathLength,
221 $this->fileNameLength,
222 $this->extraFieldLength
223 ]);
224 $fileHeader = self::START_SIGNATURE . $fixedHeader . $this->filePath . $this->fileName . $this->extraField;
225 $fileHeader = $this->replaceEOLsWithPlaceholders($fileHeader);
226
227 return $fileHeader;
228 }
229
230 /**
231 * 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
232 * @return string
233 */
234 public function getUncompressedFileHeader(): string
235 {
236 // Force current attribute to be without compression, preserving them first to restore them later
237 $oldAttributes = $this->attributes;
238 $this->setIsCompressed(false);
239
240 $fixedHeader = $this->encoder->intArrayToHex(self::FILE_HEADER_FORMAT, [
241 $this->modifiedTime,
242 $this->crc32,
243 // 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.
244 // This is done to remove this file header within file content through search replace
245 $this->uncompressedSize,
246 $this->uncompressedSize,
247 $this->attributes,
248 $this->filePathLength,
249 $this->fileNameLength,
250 $this->extraFieldLength
251 ]);
252 $fileHeader = self::START_SIGNATURE . $fixedHeader . $this->filePath . $this->fileName . $this->extraField;
253 $fileHeader = $this->replaceEOLsWithPlaceholders($fileHeader);
254
255 $this->setAttributes($oldAttributes);
256
257 return $fileHeader;
258 }
259
260 public function getIndexHeader(): string
261 {
262 $fixedHeader = $this->encoder->intArrayToHex(self::INDEX_HEADER_FORMAT, [
263 $this->startOffset,
264 $this->modifiedTime,
265 $this->crc32,
266 $this->compressedSize,
267 $this->uncompressedSize,
268 $this->attributes,
269 $this->filePathLength,
270 $this->fileNameLength,
271 $this->extraFieldLength
272 ]);
273
274 $fixedHeader = $fixedHeader . $this->filePath . $this->fileName . $this->extraField;
275 $fixedHeader = $this->replaceEOLsWithPlaceholders($fixedHeader);
276
277 return $fixedHeader;
278 }
279
280 /**
281 * @return void
282 */
283 public function resetHeader()
284 {
285 $this->startSignature = '';
286 $this->modifiedTime = 0;
287 $this->crc32 = 0;
288 $this->crc32Checksum = '';
289 $this->compressedSize = 0;
290 $this->uncompressedSize = 0;
291 $this->setAttributes(0);
292 $this->extraFieldLength = 0;
293 $this->fileNameLength = 0;
294 $this->filePathLength = 0;
295 $this->startOffset = 0;
296 $this->filePath = '';
297 $this->fileName = '';
298 $this->extraField = '';
299 }
300
301 public function getStartSignature(): string
302 {
303 return $this->startSignature;
304 }
305
306 /**
307 * @return void
308 */
309 public function setStartSignature(string $startSignature)
310 {
311 $this->startSignature = $startSignature;
312 }
313
314 public function getModifiedTime(): int
315 {
316 return $this->modifiedTime;
317 }
318
319 /**
320 * @return void
321 */
322 public function setModifiedTime(int $modifiedTime)
323 {
324 $this->modifiedTime = $modifiedTime;
325 }
326
327 public function getCrc32(): int
328 {
329 return $this->crc32;
330 }
331
332 /**
333 * @return void
334 */
335 public function setCrc32(int $crc32)
336 {
337 $this->crc32 = $crc32;
338 $this->crc32Checksum = bin2hex(pack('N', $crc32));
339 }
340
341 public function getCrc32Checksum(): string
342 {
343 return $this->crc32Checksum;
344 }
345
346 /**
347 * @return void
348 */
349 public function setCrc32Checksum(string $crc32Checksum)
350 {
351 $this->crc32Checksum = $crc32Checksum;
352 $this->crc32 = unpack('N', pack('H*', $this->crc32Checksum))[1];
353 }
354
355 public function getCompressedSize(): int
356 {
357 return $this->compressedSize;
358 }
359
360 /**
361 * @return void
362 */
363 public function setCompressedSize(int $compressedSize)
364 {
365 $this->compressedSize = $compressedSize;
366 }
367
368 public function getUncompressedSize(): int
369 {
370 return $this->uncompressedSize;
371 }
372
373 /**
374 * @return void
375 */
376 public function setUncompressedSize(int $uncompressedSize)
377 {
378 $this->uncompressedSize = $uncompressedSize;
379 }
380
381 public function getAttributes(): int
382 {
383 return $this->attributes;
384 }
385
386 /**
387 * @return void
388 */
389 public function setAttributes(int $attributes)
390 {
391 $this->attributes = $attributes;
392 }
393
394 public function getIsCompressed(): bool
395 {
396 if ($this->attributes & FileHeaderAttribute::COMPRESSED) {
397 return true;
398 }
399
400 return false;
401 }
402
403 /**
404 * @return void
405 */
406 public function setIsCompressed(bool $isCompressed)
407 {
408 $isCompressed ?
409 $this->attributes |= FileHeaderAttribute::COMPRESSED :
410 $this->attributes &= ~FileHeaderAttribute::COMPRESSED;
411 }
412
413 public function getIsPreviousPartRequired(): bool
414 {
415 if ($this->attributes & FileHeaderAttribute::REQUIRE_PREVIOUS_PART) {
416 return true;
417 }
418
419 return false;
420 }
421
422 /**
423 * @return void
424 */
425 public function setIsPreviousPartRequired(bool $isPreviousPartRequired)
426 {
427 $isPreviousPartRequired ?
428 $this->attributes |= FileHeaderAttribute::REQUIRE_PREVIOUS_PART :
429 $this->attributes &= ~FileHeaderAttribute::REQUIRE_PREVIOUS_PART;
430 }
431
432 public function getIsNextPartRequired(): bool
433 {
434 if ($this->attributes & FileHeaderAttribute::REQUIRE_NEXT_PART) {
435 return true;
436 }
437
438 return false;
439 }
440
441 /**
442 * @return void
443 */
444 public function setIsNextPartRequired(bool $isNextPartRequired)
445 {
446 $isNextPartRequired ?
447 $this->attributes |= FileHeaderAttribute::REQUIRE_NEXT_PART :
448 $this->attributes &= ~FileHeaderAttribute::REQUIRE_NEXT_PART;
449 }
450
451 public function getStartOffset(): int
452 {
453 return $this->startOffset;
454 }
455
456 /**
457 * @return void
458 */
459 public function setStartOffset(int $startOffset)
460 {
461 $this->startOffset = $startOffset;
462 }
463
464 public function getFilePath(): string
465 {
466 return $this->filePath;
467 }
468
469 /**
470 * @return void
471 */
472 public function setFilePath(string $filePath)
473 {
474 $this->filePath = $filePath;
475 $filePathRenamed = $this->replaceEOLsWithPlaceholders($filePath);
476 $this->filePathLength = strlen($filePathRenamed);
477 }
478
479 public function getFileName(): string
480 {
481 return $this->fileName;
482 }
483
484 /**
485 * @return void
486 */
487 public function setFileName(string $fileName)
488 {
489 $this->fileName = $fileName;
490 $renamedFile = $this->replaceEOLsWithPlaceholders($fileName);
491 $this->fileNameLength = strlen($renamedFile);
492 }
493
494 public function getExtraField(): string
495 {
496 return $this->extraField;
497 }
498
499 /**
500 * @return void
501 */
502 public function setExtraField(string $extraField)
503 {
504 $this->extraField = $extraField;
505 $this->extraFieldLength = strlen($extraField);
506 }
507
508 public function getIdentifiablePath(): string
509 {
510 return $this->filePath . $this->fileName;
511 }
512
513 public function getDynamicHeaderLength(): int
514 {
515 return $this->filePathLength + $this->fileNameLength + $this->extraFieldLength;
516 }
517
518 public function getContentStartOffset(): int
519 {
520 return $this->startOffset + self::FILE_HEADER_FIXED_SIZE + $this->getDynamicHeaderLength() + 1;
521 }
522
523 /**
524 * @param string $filePath
525 * @param string $pathForErrorLogging
526 * @return void
527 * @throws FileValidationException
528 */
529 public function validateFile(string $filePath, string $pathForErrorLogging = '')
530 {
531 if (empty($pathForErrorLogging)) {
532 $pathForErrorLogging = $filePath;
533 }
534
535 if (!file_exists($filePath)) {
536 throw new FileValidationException(sprintf('File doesn\'t exist: %s.', $pathForErrorLogging));
537 }
538
539 $fileSize = filesize($filePath);
540 if ($this->getUncompressedSize() !== $fileSize) {
541 throw new FileValidationException(sprintf('Filesize validation failed for file %s. Expected: %s. Actual: %s', $pathForErrorLogging, $this->formatSize($this->getUncompressedSize(), 2), $this->formatSize($fileSize, 2)));
542 }
543
544 $crc32Checksum = hash_file(self::CRC32_CHECKSUM_ALGO, $filePath);
545 if ($this->crc32Checksum !== $crc32Checksum) {
546 throw new FileValidationException(sprintf('CRC32 Checksum validation failed for file %s. Expected: %s. Actual: %s', $pathForErrorLogging, $this->getCrc32Checksum(), $crc32Checksum));
547 }
548 }
549 }
550