PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 3.1.3
WP STAGING – WordPress Backup, Restore, Migration & Clone v3.1.3
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 / Framework / Filesystem / FileObject.php
wp-staging / Framework / Filesystem Last commit date
Filters 2 years ago Scanning 5 years ago DebugLogReader.php 2 years ago DirectoryListing.php 3 years ago DiskWriteCheck.php 3 years ago FileObject.php 2 years ago Filesystem.php 2 years ago FilesystemExceptions.php 5 years ago FilterableDirectoryIterator.php 3 years ago LogCleanup.php 2 years ago LogFiles.php 2 years ago MissingFileException.php 3 years ago PathChecker.php 2 years ago PathIdentifier.php 2 years ago Permissions.php 5 years ago WpUploadsFolderSymlinker.php 4 years ago
FileObject.php
446 lines
1 <?php
2
3 // TODO PHP7.x; declare(strict_type=1);
4 // TODO PHP7.x; type hints & return types
5
6 namespace WPStaging\Framework\Filesystem;
7
8 use Exception;
9 use LimitIterator;
10 use RuntimeException;
11 use SplFileObject;
12 use WPStaging\Core\WPStaging;
13 use WPStaging\functions;
14 use WPStaging\Backup\Exceptions\DiskNotWritableException;
15
16 use function tad\WPBrowser\debug;
17 use function WPStaging\functions\debug_log;
18
19 class FileObject extends SplFileObject
20 {
21 const MODE_READ = 'rb'; // read only, binary
22 const MODE_WRITE = 'wb'; // write only, binary
23 const MODE_APPEND = 'ab'; // append with create, binary
24 const MODE_APPEND_AND_READ = 'ab+'; // append with read and create if not exists, binary
25 const MODE_WRITE_SAFE = 'xb'; // write if exists E_WARNING & return false, binary
26 const MODE_WRITE_UNSAFE = 'cb'; // append, if exists cursor to top, binary
27
28 const AVERAGE_LINE_LENGTH = 4096;
29
30 private $existingMetadataPosition;
31
32 /** @var int */
33 private $totalLines = null;
34
35 /** @var bool */
36 private $fgetsUsedOnKey0 = false;
37
38 /** @var bool */
39 private $fseekUsed = false;
40
41 /**
42 * Lock File Handle for Windows
43 * @var resource|null
44 */
45 private $lockHandle = null;
46
47 /**
48 * @throws DiskNotWritableException
49 * @throws FilesystemExceptions
50 */
51 public function __construct($fullPath, $openMode = self::MODE_READ)
52 {
53 $fullPath = untrailingslashit($fullPath);
54
55 if (empty($fullPath)) {
56 throw new DiskNotWritableException("Empty path given. Please contact support@wp-staging.com");
57 }
58
59 if (!file_exists($fullPath)) {
60 WPStaging::make(Filesystem::class)->mkdir(dirname($fullPath), true);
61 }
62
63 try {
64 parent::__construct($fullPath, $openMode);
65 } catch (Exception $e) {
66 // If this fails, it will throw an exception.
67 WPStaging::make(DiskWriteCheck::class)->testDiskIsWriteable();
68
69 // If it didn't fail due to disk write check, re-throw
70 throw $e;
71 }
72 }
73
74 /**
75 * @param $str
76 * @param $length
77 * @return false|int False on error, number of bytes written on success
78 */
79 public function fwriteSafe($str, $length = null)
80 {
81 // Not sure if we need mbstring_binary_safe_encoding. If not, delete it as we already open file with binary mode.
82 mbstring_binary_safe_encoding();
83
84 $strLen = strlen($str);
85 $writtenBytes = $length !== null ? $this->fwrite($str, $length) : $this->fwrite($str);
86 reset_mbstring_encoding();
87
88 if ($strLen !== $writtenBytes) {
89 return false;
90 }
91
92 return $writtenBytes;
93 }
94
95 /**
96 * @param int $lines
97 * @return array
98 *
99 * @throws Exception
100 * @todo DRY /Framework/Utils/Cache/BufferedCache.php
101 */
102 public function readBottomLines($lines)
103 {
104 $this->seek(PHP_INT_MAX);
105 $lastLine = $this->key();
106 $offset = $lastLine - $lines;
107 if ($offset < 0) {
108 $offset = 0;
109 }
110
111 $allLines = new LimitIterator($this, $offset, $lastLine);
112 return array_reverse(array_values(iterator_to_array($allLines)));
113 }
114
115 /**
116 * @return array The backup metadata array
117 * @throws RuntimeException
118 */
119 public function readBackupMetadata()
120 {
121 // Default max size 128KB for backup metadata
122 $maxBackupMetadataSize = apply_filters('wpstg_max_backup_metadata_size', 128 * KB_IN_BYTES);
123 // Make sure the max size is never above 1MB
124 $negativeOffset = min($maxBackupMetadataSize, 1 * MB_IN_BYTES);
125 // Make sure the max size is never below 32KB
126 $negativeOffset = max($negativeOffset, 32 * KB_IN_BYTES);
127
128 // Set the pointer to the end of the file, minus the negative offset for which to start looking for the backup metadata.
129 $this->fseek(max($this->getSize() - $negativeOffset, 0), SEEK_SET);
130
131 $backupMetadata = null;
132
133 do {
134 $this->existingMetadataPosition = $this->ftell();
135 $line = trim($this->readAndMoveNext());
136 if ($this->isValidMetadata($line)) {
137 $backupMetadata = $this->extractMetadata($line);
138 }
139 } while ($this->valid() && !is_array($backupMetadata));
140
141 if (!is_array($backupMetadata)) {
142 $error = sprintf('Could not find metadata in the backup file %s - This file could be corrupt.', $this->getFilename());
143 throw new RuntimeException($error);
144 }
145
146 return $backupMetadata;
147 }
148
149 public function extractMetadata($line)
150 {
151 if (!$this->isSqlFile()) {
152 return json_decode($line, true);
153 }
154
155 return json_decode(substr($line, 3), true);
156 }
157
158 /**
159 * @param string $line
160 * @return bool
161 *
162 * @todo Move all metadata related function out of FileObject
163 */
164 public function isValidMetadata($line)
165 {
166 if ($this->isSqlFile() && substr($line, 3, 1) !== '{') {
167 return false;
168 } elseif (!$this->isSqlFile() && substr($line, 0, 1) !== '{') {
169 return false;
170 }
171
172 $maybeMetadata = $this->extractMetadata($line);
173
174 if (!is_array($maybeMetadata) || !array_key_exists('networks', $maybeMetadata) || !is_array($maybeMetadata['networks'])) {
175 return false;
176 }
177
178 $network = $maybeMetadata['networks']['1'];
179 if (!is_array($network) || !array_key_exists('blogs', $network) || !is_array($network['blogs'])) {
180 return false;
181 }
182
183 return true;
184 }
185
186 public function getExistingMetadataPosition()
187 {
188 if ($this->existingMetadataPosition === null) {
189 $this->readBackupMetadata();
190 }
191
192 return $this->existingMetadataPosition;
193 }
194
195 /**
196 * @return mixed int|null
197 * @throws Exception
198 */
199 public function totalLines()
200 {
201 if ($this->totalLines !== null) {
202 return $this->totalLines;
203 }
204
205 $currentKey = $this->key();
206 $this->seek(PHP_INT_MAX);
207 $this->totalLines = $this->key();
208 $this->seek($currentKey);
209 return $this->totalLines;
210 }
211
212 /**
213 * Override SplFileObject::seek()
214 * Alternative function for SplFileObject::seek() that behaves identical in all PHP Versions.
215 *
216 * There was a major change in PHP 8.0.1 where after using `SplFileObject::seek($line)`, the first subsequent
217 * call to `SplFileObject::fgets()` does not increase the line pointer anymore as it did in earlier version since PHP 5.x
218 * @see https://bugs.php.net/bug.php?id=81551
219 *
220 * Note: This will remove READ_AHEAD flag while execution to deliver reliable and identical results as READ_AHEAD tells
221 * SplFileObject to read on next() and rewind() too which our custom seek() makes use of.
222 * This would disturb this seek() implementation and would lead to fatal errors if 'cpu load' setting is 'medium' or 'high'
223 *
224 *
225 * @param int $offset The zero-based line number to seek to.
226 * @throws Exception
227 */
228 #[\ReturnTypeWillChange]
229 public function seek($offset)
230 {
231 if ($offset < 0) {
232 throw new Exception("Can't seek file: " . $this->getPathname() . " to negative offset: $offset");
233 }
234
235 $this->fseekUsed = false;
236 $this->fgetsUsedOnKey0 = false;
237 if ($offset === 0 || version_compare(PHP_VERSION, '8.0.1', '<')) {
238 parent::seek($offset);
239 return;
240 }
241
242 $offset -= 1;
243
244 if ($this->totalLines !== null && $offset >= $this->totalLines) {
245 $offset += 1;
246 }
247
248 $originalFlags = $this->getFlags();
249 $newFlags = $originalFlags & ~self::READ_AHEAD;
250 $this->setFlags($newFlags);
251
252 parent::seek($offset);
253
254 if ($this->eof()) {
255 $this->current();
256 $this->totalLines = $this->key();
257 return;
258 }
259
260 $this->current();
261 $this->next();
262 $this->current();
263
264 $this->setFlags($originalFlags);
265 }
266
267 /**
268 * SplFileObject::fgets() is not consistent after SplFileObject::fseek() between php 5.x/7.x and php 8.0.1.
269 * We could either make fgets consistent after SplFileObject::seek() or SplFileObject::fseek()
270 * This implementation makes it consistent after SplFileObject::seek across all PHP versions up to 8.0.1.
271 * Use readAndMoveNext() instead if you want to achieve consistent behavior of SplFileObject::fgets after SplFileObject::fseek.
272 *
273 * @deprecated 4.2.13 Use readAndMoveNext instead as it is hard to make fgets against multiple php version after seek(0)
274 *
275 * @return string
276 */
277 #[\ReturnTypeWillChange]
278 public function fgets()
279 {
280 if ($this->key() === 0 || version_compare(PHP_VERSION, '8.0.1', '<')) {
281 $this->fgetsUsedOnKey0 = true;
282 return parent::fgets();
283 }
284
285 $originalFlags = $this->getFlags();
286 $newFlags = $originalFlags & ~self::READ_AHEAD;
287 $this->setFlags($newFlags);
288
289 $line = $this->current();
290 $this->next();
291
292 if (version_compare(PHP_VERSION, '8.0.19', '<')) {
293 $line = $this->current();
294 }
295
296 if (version_compare(PHP_VERSION, '8.1', '>') && version_compare(PHP_VERSION, '8.1.6', '<')) {
297 $line = $this->current();
298 }
299
300 if (!$this->fseekUsed) {
301 $line = $this->current();
302 }
303
304 $this->setFlags($originalFlags);
305 return $line;
306 }
307
308 /**
309 * Gets the current line number
310 *
311 * @return int
312 */
313 #[\ReturnTypeWillChange]
314 public function key()
315 {
316 if (!$this->fgetsUsedOnKey0 || version_compare(PHP_VERSION, '8.0.19', '<')) {
317 return parent::key();
318 }
319
320 if (version_compare(PHP_VERSION, '8.1', '>') && version_compare(PHP_VERSION, '8.1.6', '<')) {
321 return parent::key();
322 }
323
324 return parent::key() - 1;
325 }
326
327 /**
328 * Seek to a position
329 *
330 * @param int $offset The value to start from added to the $whence
331 * @param int $whence values are:
332 * SEEK_SET - Set position equal to offset bytes.
333 * SEEK_CUR - Set position to current location plus offset.
334 * SEEK_END - Set position to end-of-file plus offset.
335 * @return int
336 */
337 #[\ReturnTypeWillChange]
338 public function fseek($offset, $whence = SEEK_SET)
339 {
340 if (version_compare(PHP_VERSION, '8.0.19', '<')) {
341 return parent::fseek($offset, $whence);
342 }
343
344 if (version_compare(PHP_VERSION, '8.1', '>') && version_compare(PHP_VERSION, '8.1.6', '<')) {
345 return parent::fseek($offset, $whence);
346 }
347
348 // After calling parent::fseek() and $this->>fgets() two or three times it starts to act different on PHP >= 8.0.19, PHP >= 8.1.6 and PHP >= 8.2.
349 // Calling it three times helps to write a consistent fseek() for the above mentioned PHP versions.
350 for ($i = 0; $i < 3; $i++) {
351 parent::fseek(0);
352 $this->fgets();
353 }
354
355 $this->fseekUsed = true;
356 return parent::fseek($offset, $whence);
357 }
358
359
360 /**
361 * SplFileObject::fgets() is not consistent after SplFileObject::fseek() between php 5.x/7.x and php 8.0.1.
362 * Use this method instead if you want to achieve consistent behavior of SplFileObject::fgets after SplFileObject::fseek across all PHP versions up to PHP 8.0.1.
363 * READ_AHEAD flag will not have any affect on this method. It's disabled.
364 *
365 * @var bool $useFgets default false. Setting this to true will use fgets on PHP < 8.0.1
366 *
367 * @return string
368 */
369 public function readAndMoveNext($useFgets = false)
370 {
371 if ($useFgets && version_compare(PHP_VERSION, '8.0.1', '<')) {
372 return parent::fgets();
373 }
374
375 $originalFlags = $this->getFlags();
376 $newFlags = $originalFlags & ~self::READ_AHEAD;
377 $this->setFlags($newFlags);
378
379 $line = $this->current();
380 $this->next();
381
382 $this->setFlags($originalFlags);
383 return $line;
384 }
385
386 /**
387 * @inheritDoc
388 */
389 #[\ReturnTypeWillChange]
390 public function flock($operation, &$wouldBlock = null)
391 {
392 if (!WPStaging::isWindowsOs()) {
393 $parentMethodFlock = 'parent::flock';
394 if (version_compare(PHP_VERSION, '8.2', '>=')) {
395 // phpcs:ignore SlevomatCodingStandard.PHP.ForbiddenClasses.ForbiddenClass
396 $parentMethodFlock = \SplFileObject::class . '::flock';
397 }
398
399 if (!is_callable($parentMethodFlock)) {
400 return false;
401 }
402
403 return parent::flock($operation, $wouldBlock);
404 }
405
406 // create a lock file for Windows
407 $lockFileName = untrailingslashit($this->getPathname()) . '.lock';
408 $this->lockHandle = fopen($lockFileName, 'cb');
409
410 if (!is_resource($this->lockHandle)) {
411 throw new RuntimeException("Could not open lock file {$this->getPathname()}");
412 }
413
414 return flock($this->lockHandle, $operation, $wouldBlock);
415 }
416
417 /**
418 * Release Lock if Windows OS
419 */
420 public function releaseLock()
421 {
422 if (!WPStaging::isWindowsOs() || $this->lockHandle === null) {
423 return;
424 }
425
426 $lockFileName = untrailingslashit($this->getPathname()) . '.lock';
427 if (is_file($lockFileName)) {
428 unlink($lockFileName);
429 }
430
431 flock($this->lockHandle, LOCK_UN);
432 fclose($this->lockHandle);
433 $this->lockHandle = null;
434 }
435
436 public function __destruct()
437 {
438 $this->releaseLock();
439 }
440
441 public function isSqlFile()
442 {
443 return $this->getExtension() === 'sql';
444 }
445 }
446