PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 4.1.1
WP STAGING – WordPress Backup, Restore, Migration & Clone v4.1.1
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 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
520 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 public function getIndexHeader(): string
231 {
232 $fixedHeader = $this->encoder->intArrayToHex(self::INDEX_HEADER_FORMAT, [
233 $this->startOffset,
234 $this->modifiedTime,
235 $this->crc32,
236 $this->compressedSize,
237 $this->uncompressedSize,
238 $this->attributes,
239 $this->filePathLength,
240 $this->fileNameLength,
241 $this->extraFieldLength
242 ]);
243
244 $fixedHeader = $fixedHeader . $this->filePath . $this->fileName . $this->extraField;
245 $fixedHeader = $this->replaceEOLsWithPlaceholders($fixedHeader);
246
247 return $fixedHeader;
248 }
249
250 /**
251 * @return void
252 */
253 public function resetHeader()
254 {
255 $this->startSignature = '';
256 $this->modifiedTime = 0;
257 $this->crc32 = 0;
258 $this->crc32Checksum = '';
259 $this->compressedSize = 0;
260 $this->uncompressedSize = 0;
261 $this->setAttributes(0);
262 $this->extraFieldLength = 0;
263 $this->fileNameLength = 0;
264 $this->filePathLength = 0;
265 $this->startOffset = 0;
266 $this->filePath = '';
267 $this->fileName = '';
268 $this->extraField = '';
269 }
270
271 public function getStartSignature(): string
272 {
273 return $this->startSignature;
274 }
275
276 /**
277 * @return void
278 */
279 public function setStartSignature(string $startSignature)
280 {
281 $this->startSignature = $startSignature;
282 }
283
284 public function getModifiedTime(): int
285 {
286 return $this->modifiedTime;
287 }
288
289 /**
290 * @return void
291 */
292 public function setModifiedTime(int $modifiedTime)
293 {
294 $this->modifiedTime = $modifiedTime;
295 }
296
297 public function getCrc32(): int
298 {
299 return $this->crc32;
300 }
301
302 /**
303 * @return void
304 */
305 public function setCrc32(int $crc32)
306 {
307 $this->crc32 = $crc32;
308 $this->crc32Checksum = bin2hex(pack('N', $crc32));
309 }
310
311 public function getCrc32Checksum(): string
312 {
313 return $this->crc32Checksum;
314 }
315
316 /**
317 * @return void
318 */
319 public function setCrc32Checksum(string $crc32Checksum)
320 {
321 $this->crc32Checksum = $crc32Checksum;
322 $this->crc32 = unpack('N', pack('H*', $this->crc32Checksum))[1];
323 }
324
325 public function getCompressedSize(): int
326 {
327 return $this->compressedSize;
328 }
329
330 /**
331 * @return void
332 */
333 public function setCompressedSize(int $compressedSize)
334 {
335 $this->compressedSize = $compressedSize;
336 }
337
338 public function getUncompressedSize(): int
339 {
340 return $this->uncompressedSize;
341 }
342
343 /**
344 * @return void
345 */
346 public function setUncompressedSize(int $uncompressedSize)
347 {
348 $this->uncompressedSize = $uncompressedSize;
349 }
350
351 public function getAttributes(): int
352 {
353 return $this->attributes;
354 }
355
356 /**
357 * @return void
358 */
359 public function setAttributes(int $attributes)
360 {
361 $this->attributes = $attributes;
362 }
363
364 public function getIsCompressed(): bool
365 {
366 if ($this->attributes & FileHeaderAttribute::COMPRESSED) {
367 return true;
368 }
369
370 return false;
371 }
372
373 /**
374 * @return void
375 */
376 public function setIsCompressed(bool $isCompressed)
377 {
378 $isCompressed ?
379 $this->attributes |= FileHeaderAttribute::COMPRESSED :
380 $this->attributes &= ~FileHeaderAttribute::COMPRESSED;
381 }
382
383 public function getIsPreviousPartRequired(): bool
384 {
385 if ($this->attributes & FileHeaderAttribute::REQUIRE_PREVIOUS_PART) {
386 return true;
387 }
388
389 return false;
390 }
391
392 /**
393 * @return void
394 */
395 public function setIsPreviousPartRequired(bool $isPreviousPartRequired)
396 {
397 $isPreviousPartRequired ?
398 $this->attributes |= FileHeaderAttribute::REQUIRE_PREVIOUS_PART :
399 $this->attributes &= ~FileHeaderAttribute::REQUIRE_PREVIOUS_PART;
400 }
401
402 public function getIsNextPartRequired(): bool
403 {
404 if ($this->attributes & FileHeaderAttribute::REQUIRE_NEXT_PART) {
405 return true;
406 }
407
408 return false;
409 }
410
411 /**
412 * @return void
413 */
414 public function setIsNextPartRequired(bool $isNextPartRequired)
415 {
416 $isNextPartRequired ?
417 $this->attributes |= FileHeaderAttribute::REQUIRE_NEXT_PART :
418 $this->attributes &= ~FileHeaderAttribute::REQUIRE_NEXT_PART;
419 }
420
421 public function getStartOffset(): int
422 {
423 return $this->startOffset;
424 }
425
426 /**
427 * @return void
428 */
429 public function setStartOffset(int $startOffset)
430 {
431 $this->startOffset = $startOffset;
432 }
433
434 public function getFilePath(): string
435 {
436 return $this->filePath;
437 }
438
439 /**
440 * @return void
441 */
442 public function setFilePath(string $filePath)
443 {
444 $this->filePath = $filePath;
445 $filePathRenamed = $this->replaceEOLsWithPlaceholders($filePath);
446 $this->filePathLength = strlen($filePathRenamed);
447 }
448
449 public function getFileName(): string
450 {
451 return $this->fileName;
452 }
453
454 /**
455 * @return void
456 */
457 public function setFileName(string $fileName)
458 {
459 $this->fileName = $fileName;
460 $renamedFile = $this->replaceEOLsWithPlaceholders($fileName);
461 $this->fileNameLength = strlen($renamedFile);
462 }
463
464 public function getExtraField(): string
465 {
466 return $this->extraField;
467 }
468
469 /**
470 * @return void
471 */
472 public function setExtraField(string $extraField)
473 {
474 $this->extraField = $extraField;
475 $this->extraFieldLength = strlen($extraField);
476 }
477
478 public function getIdentifiablePath(): string
479 {
480 return $this->filePath . $this->fileName;
481 }
482
483 public function getDynamicHeaderLength(): int
484 {
485 return $this->filePathLength + $this->fileNameLength + $this->extraFieldLength;
486 }
487
488 public function getContentStartOffset(): int
489 {
490 return $this->startOffset + self::FILE_HEADER_FIXED_SIZE + $this->getDynamicHeaderLength() + 1;
491 }
492
493 /**
494 * @param string $filePath
495 * @param string $pathForErrorLogging
496 * @return void
497 * @throws FileValidationException
498 */
499 public function validateFile(string $filePath, string $pathForErrorLogging = '')
500 {
501 if (empty($pathForErrorLogging)) {
502 $pathForErrorLogging = $filePath;
503 }
504
505 if (!file_exists($filePath)) {
506 throw new FileValidationException(sprintf('File doesn\'t exist: %s.', $pathForErrorLogging));
507 }
508
509 $fileSize = filesize($filePath);
510 if ($this->getUncompressedSize() !== $fileSize) {
511 throw new FileValidationException(sprintf('Filesize validation failed for file %s. Expected: %s. Actual: %s', $pathForErrorLogging, $this->formatSize($this->getUncompressedSize(), 2), $this->formatSize($fileSize, 2)));
512 }
513
514 $crc32Checksum = hash_file(self::CRC32_CHECKSUM_ALGO, $filePath);
515 if ($this->crc32Checksum !== $crc32Checksum) {
516 throw new FileValidationException(sprintf('CRC32 Checksum validation failed for file %s. Expected: %s. Actual: %s', $pathForErrorLogging, $this->getCrc32Checksum(), $crc32Checksum));
517 }
518 }
519 }
520