PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 4.9.2
WP STAGING – WordPress Backup, Restore, Migration & Clone v4.9.2
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 / Ajax / Upload.php
wp-staging / Backup / Ajax Last commit date
Backup 2 months ago FileList 4 months ago Restore 2 months ago Backup.php 2 years ago BackupDownloader.php 1 day ago BackupSizeCalculator.php 1 day ago BackupSpeedIndex.php 9 months ago BaseFileList.php 1 year ago BaseListing.php 9 months ago Delete.php 4 months ago Edit.php 4 months ago Explore.php 3 months ago ExploreCache.php 3 months ago FileInfo.php 9 months ago FileList.php 1 year ago Listing.php 7 months ago Parts.php 1 day ago Restore.php 2 years ago ScheduleList.php 1 year ago Upload.php 1 month ago
Upload.php
393 lines
1 <?php
2
3 namespace WPStaging\Backup\Ajax;
4
5 use Exception;
6 use WPStaging\Backup\BackupRepairer;
7 use WPStaging\Backup\Entity\BackupMetadata;
8 use WPStaging\Backup\Service\BackupsFinder;
9 use WPStaging\Backup\Task\Tasks\JobRestore\RestoreRequirementsCheckTask;
10 use WPStaging\Backup\WithBackupIdentifier;
11 use WPStaging\Core\WPStaging;
12 use WPStaging\Framework\Component\AbstractTemplateComponent;
13 use WPStaging\Framework\Job\Exception\DiskNotWritableException;
14 use WPStaging\Framework\Filesystem\DiskWriteCheck;
15 use WPStaging\Framework\Filesystem\Filesystem;
16 use WPStaging\Framework\Security\Otp\Otp;
17 use WPStaging\Framework\Security\Otp\OtpDisabledException;
18 use WPStaging\Framework\Security\Otp\OtpException;
19 use WPStaging\Framework\TemplateEngine\TemplateEngine;
20 use WPStaging\Framework\Utils\Sanitize;
21
22 use function WPStaging\functions\debug_log;
23
24 class Upload extends AbstractTemplateComponent
25 {
26 use WithBackupIdentifier;
27
28 /**
29 * @var string
30 */
31 const OPTION_UPLOAD_PREPARED = 'wpstg.backups.upload_prepared';
32
33 /** @var BackupsFinder */
34 private $backupsFinder;
35
36 /** @var BackupRepairer */
37 private $backupRepairer;
38
39 /** @var Sanitize */
40 private $sanitize;
41
42 /**
43 * @var Filesystem
44 */
45 private $filesystem;
46
47 /**
48 * @var Otp
49 */
50 private $otpService;
51
52 public function __construct(BackupsFinder $backupsFinder, TemplateEngine $templateEngine, BackupRepairer $backupRepairer, Sanitize $sanitize, Filesystem $filesystem, Otp $otpService)
53 {
54 parent::__construct($templateEngine);
55 $this->backupsFinder = $backupsFinder;
56 $this->backupRepairer = $backupRepairer;
57 $this->sanitize = $sanitize;
58 $this->filesystem = $filesystem;
59 $this->otpService = $otpService;
60 }
61
62 public function ajaxPrepareUpload()
63 {
64 if (!$this->canRenderAjax()) {
65 wp_send_json_error([
66 'message' => esc_html__('Invalid Request!', 'wp-staging'),
67 ], 401);
68 }
69
70 try {
71 $this->otpService->validateOtpRequest();
72 } catch (OtpDisabledException $ex) {
73 debug_log($ex->getMessage());
74 } catch (OtpException $ex) {
75 wp_send_json_error([
76 'message' => esc_html($ex->getMessage()),
77 ], $ex->getCode());
78 }
79
80 delete_option(self::OPTION_UPLOAD_PREPARED);
81 if (!update_option(self::OPTION_UPLOAD_PREPARED, 'true')) {
82 wp_send_json_error([
83 'message' => __('Could not prepare backup upload', 'wp-staging'),
84 ], 500);
85 }
86
87 wp_send_json_success();
88 }
89
90 public function render()
91 {
92 if (!$this->canRenderAjax()) {
93 return;
94 }
95
96 /**
97 * Example:
98 *
99 * name = "8xx.290.myftpupload.com_20210521-193355_967950a65d39 (2).wpstg"
100 * type = "application/octet-stream"
101 * tmp_name = "/tmp/phpYFrjBk"
102 * error = {int} 0
103 * size = {int} 1048576
104 */
105 $file = isset($_FILES['file']) ? $this->sanitize->sanitizeFileUpload($_FILES['file']) : null;
106
107 try {
108 $this->validateRequestData($file);
109 } catch (\Exception $e) {
110 wp_send_json_error([
111 'message' => !empty($e->getMessage()) ? $e->getMessage() : 'Invalid request data',
112 ], 400);
113 }
114
115 $resumableChunkNumber = isset($_GET['resumableChunkNumber']) ? $this->sanitize->sanitizeInt($_GET['resumableChunkNumber'], true) : 0;
116 $resumableChunkSize = isset($_GET['resumableChunkSize']) ? $this->sanitize->sanitizeInt($_GET['resumableChunkSize'], true) : 0;
117 $resumableCurrentChunkSize = isset($_GET['resumableCurrentChunkSize']) ? $this->sanitize->sanitizeInt($_GET['resumableCurrentChunkSize'], true) : 0;
118 $resumableTotalSize = isset($_GET['resumableTotalSize']) ? $this->sanitize->sanitizeInt($_GET['resumableTotalSize'], true) : 0;
119 $resumableTotalChunks = isset($_GET['resumableTotalChunks']) ? $this->sanitize->sanitizeInt($_GET['resumableTotalChunks'], true) : 0;
120 $rawSuffix = isset($_GET['uniqueIdentifierSuffix']) ? $this->sanitize->sanitizeString($_GET['uniqueIdentifierSuffix']) : '';
121 $uniqueIdentifierSuffix = ctype_digit($rawSuffix) ? $rawSuffix : '';
122 $resumableIdentifier = isset($_GET['resumableIdentifier']) ? sanitize_file_name($_GET['resumableIdentifier']) : '';
123 $resumableFilename = isset($_GET['resumableFilename']) ? sanitize_file_name($_GET['resumableFilename']) : '';
124 $resumableRelativePath = isset($_GET['resumableRelativePath']) ? sanitize_file_name($_GET['resumableRelativePath']) : '';
125
126 if (!$this->filesystem->isWpstgBackupFile($resumableFilename) && !$this->isBackupPart($resumableFilename)) {
127 wp_send_json_error([
128 'message' => esc_html__('Invalid backup filename.', 'wp-staging'),
129 ], 400);
130 }
131
132 $backupsDirectory = $this->backupsFinder->getBackupsDirectory();
133
134 $originalPath = $backupsDirectory . $resumableFilename;
135 if ($this->isBackupPart($originalPath) && file_exists($originalPath)) {
136 wp_send_json_error([
137 'message' => __('This backup part exists already', 'wp-staging'),
138 ], 409); // 409 status code for conflict
139 }
140
141 $fullPath = $backupsDirectory . $uniqueIdentifierSuffix . $resumableFilename . '.uploading';
142
143 $resolvedDir = realpath(dirname($fullPath));
144 $resolvedBase = realpath(rtrim($backupsDirectory, '/\\'));
145 if ($resolvedDir === false || $resolvedBase === false || $resolvedDir !== $resolvedBase) {
146 wp_send_json_error([
147 'message' => esc_html__('Invalid upload path.', 'wp-staging'),
148 ], 400);
149 }
150
151 // If neither uploading file or the upload prepared option that mean that the upload is not prepared and didn't pass through the OTP process!
152 if (!file_exists($fullPath) && get_option(self::OPTION_UPLOAD_PREPARED) !== 'true') {
153 wp_send_json_error([
154 'message' => __('The backup file is missing or the upload process is not ready. Try again to upload the backup file or open a support request.', 'wp-staging'),
155 ], 500);
156 }
157
158 delete_option(self::OPTION_UPLOAD_PREPARED);
159 $resumableInternalIdentifier = md5($fullPath);
160
161 // Check free disk space on the first request
162 if ($resumableChunkNumber <= 1) {
163 try {
164 WPStaging::make(DiskWriteCheck::class)->checkPathCanStoreEnoughBytes($this->backupsFinder->getBackupsDirectory(), $resumableTotalSize);
165 } catch (DiskNotWritableException $e) {
166 wp_send_json_error([
167 'message' => $e->getMessage(),
168 'isDiskFull' => true,
169 ], 507);
170 } catch (\RuntimeException $e) {
171 // no-op
172 }
173 }
174
175 // Assert chunks are in sequential order
176 if ($resumableChunkNumber > 1 && $resumableTotalChunks > 1) {
177 $nextExpectedChunk = (int)get_transient("wpstg.upload.nextExpectedChunk.$resumableInternalIdentifier");
178
179 if ($nextExpectedChunk !== $resumableChunkNumber) {
180 // 409 would make more sense, but let's throw a 418 in tribute to the only person in the world capable to laugh at this joke.
181 wp_send_json_error('', 418);
182 }
183 }
184
185 update_option('wpstg.backups.doing_upload', true);
186
187 try {
188 $result = file_put_contents($fullPath, file_get_contents($file['tmp_name']), FILE_APPEND);
189
190 if (!$result) {
191 // Do a disk_free_space() check
192 try {
193 WPStaging::make(DiskWriteCheck::class)->checkPathCanStoreEnoughBytes($this->backupsFinder->getBackupsDirectory());
194 } catch (\RuntimeException $e) {
195 // no-op
196 }
197
198 // If that succeeds or could not be determined, also do a real write check.
199 WPStaging::make(DiskWriteCheck::class)->testDiskIsWriteable();
200 }
201 } catch (DiskNotWritableException $e) {
202 delete_option('wpstg.backups.doing_upload');
203
204 wp_send_json_error([
205 'message' => $e->getMessage(),
206 'isDiskFull' => true,
207 ], 507);
208 } catch (\Exception $e) {
209 delete_option('wpstg.backups.doing_upload');
210
211 wp_send_json_error([
212 'message' => $e->getMessage(),
213 ], 500);
214 }
215
216 // Last chunk?
217 if ($resumableChunkNumber === $resumableTotalChunks) {
218 try {
219 delete_transient("wpstg.upload.nextExpectedChunk.$resumableInternalIdentifier");
220 if ($this->isBackupPart($originalPath)) {
221 rename($fullPath, $originalPath);
222 return;
223 }
224
225 $this->validateBackupFile($fullPath);
226 rename($fullPath, $this->backupsFinder->getBackupsDirectory() . $uniqueIdentifierSuffix . $resumableFilename);
227 } catch (\Exception $e) {
228 if (file_exists($fullPath) && is_file($fullPath)) {
229 unlink($fullPath);
230 }
231
232 wp_send_json_error([
233 'message' => $e->getMessage(),
234 'backupFailedValidation' => true,
235 ], 500);
236 }
237 } else {
238 // Set the next expected chunk, to avoid scenarios where an erratic network connection could skip chunks or send them in unexpected order, eg:
239 // chunk.part.1
240 // chunk.part.2
241 // chunk.part.4 <-- not what we want!
242 // chunk.part.3
243 set_transient("wpstg.upload.nextExpectedChunk.$resumableInternalIdentifier", $resumableChunkNumber + 1, 1 * DAY_IN_SECONDS);
244 }
245
246 delete_option('wpstg.backups.doing_upload');
247
248 wp_send_json_success();
249 }
250
251 public function ajaxDeleteIncompleteUploads()
252 {
253 if (!$this->canRenderAjax()) {
254 wp_send_json_error(esc_html__('You do not have sufficient permissions to access this page.', 'wp-staging'));
255 }
256
257 try {
258 /** @var \SplFileInfo $splFileInfo */
259 foreach (new \DirectoryIterator($this->backupsFinder->getBackupsDirectory()) as $splFileInfo) {
260 if ($splFileInfo->isFile() && !$splFileInfo->isLink() && $splFileInfo->getExtension() === 'uploading') {
261 unlink($splFileInfo->getPathname());
262 }
263 }
264
265 wp_send_json_success();
266 } catch (\Exception $e) {
267 wp_send_json_error();
268 }
269 }
270
271 protected function validateBackupFile($fullPath)
272 {
273 clearstatcache();
274 $backupMetadata = new BackupMetadata();
275 $metadata = $backupMetadata->hydrateByFilePath($fullPath);
276
277 $isCreatedOnPro = $metadata->getCreatedOnPro();
278 $version = $metadata->getWpstgVersion();
279
280 if ($isCreatedOnPro && version_compare($version, RestoreRequirementsCheckTask::BETA_VERSION_LIMIT_PRO, '<')) {
281 throw new Exception(__('This backup was generated on a beta version of WP STAGING and can not be used with this version. Please create a new Backup or get in touch with our support if you need assistance.', 'wp-staging'));
282 }
283
284 $estimatedSize = $metadata->getBackupSize();
285 $isSplitBackup = $metadata->getIsMultipartBackup();
286
287 // Repairing the backup size in metadata
288 if ($estimatedSize === 0 && !$isSplitBackup) {
289 $this->backupRepairer->repairMetadataSize($fullPath);
290 return;
291 }
292
293 $realSize = filesize($fullPath);
294 $allowedDifferece = 1 * KB_IN_BYTES;
295
296 $smallerThanExpected = $realSize + $allowedDifferece - $estimatedSize < 0;
297 $biggerThanExpected = $realSize - $allowedDifferece > $estimatedSize;
298
299 if ($smallerThanExpected || $biggerThanExpected) {
300 throw new Exception(sprintf(__('The backup size (%s) is different than expected (%s). If this issue persists, upload the file directly to this folder using FTP: <strong>wp-content/uploads/wp-staging/backups</strong>', 'wp-staging'), size_format($realSize, 2), size_format($estimatedSize)));
301 }
302 }
303
304 protected function validateRequestData($file)
305 {
306 if (empty($_FILES) || !isset($_FILES['file']) || !is_array($file)) {
307 throw new Exception();
308 }
309
310 switch ((int)$file['error']) {
311 case UPLOAD_ERR_OK:
312 // Ok, no-op
313 break;
314 case UPLOAD_ERR_INI_SIZE:
315 case UPLOAD_ERR_FORM_SIZE:
316 case UPLOAD_ERR_PARTIAL:
317 case UPLOAD_ERR_NO_FILE:
318 case UPLOAD_ERR_NO_TMP_DIR:
319 case UPLOAD_ERR_CANT_WRITE:
320 case UPLOAD_ERR_EXTENSION:
321 throw new Exception();
322 }
323
324 if (empty($file['tmp_name']) || !file_exists($file['tmp_name'])) {
325 throw new Exception();
326 }
327
328 if (!empty($file['name']) && !$this->filesystem->isWpstgBackupFile($file['name'])) {
329 throw new Exception(sprintf(__('Invalid backup file extension: %s', 'wp-staging'), $file['name']));
330 }
331
332 /**
333 * Example:
334 *
335 * resumableChunkNumber = "1"
336 * resumableChunkSize = "1048576"
337 * resumableCurrentChunkSize = "1048576"
338 * resumableTotalSize = "14209912"
339 * resumableType = ""
340 * resumableIdentifier = "14209912-multitestswp-staginglocal_fcd2fae486dcwpstg"
341 * resumableFilename = "multi.tests.wp-staging.local_fcd2fae486dc.wpstg"
342 * resumableRelativePath = "multi.tests.wp-staging.local_fcd2fae486dc.wpstg"
343 * resumableTotalChunks = "13"
344 */
345 $requiredValues = [
346 'resumableChunkNumber',
347 'resumableChunkSize',
348 'resumableCurrentChunkSize',
349 'resumableTotalSize',
350 'resumableIdentifier',
351 'resumableFilename',
352 'resumableRelativePath',
353 'resumableTotalChunks',
354 ];
355
356 foreach ($requiredValues as $requiredValue) {
357 if (!isset($_GET[$requiredValue])) {
358 throw new Exception();
359 }
360 }
361
362 $numericValues = [
363 'resumableChunkNumber',
364 'resumableChunkSize',
365 'resumableCurrentChunkSize',
366 'resumableTotalSize',
367 'resumableTotalChunks',
368 ];
369
370 foreach ($numericValues as $numericValue) {
371 if (!isset($_GET[$numericValue])) {
372 throw new Exception();
373 }
374
375 if (!filter_var($_GET[$numericValue], FILTER_VALIDATE_INT)) {
376 throw new Exception();
377 }
378 }
379
380 $nonEmptyValues = [
381 'resumableIdentifier',
382 'resumableFilename',
383 'resumableRelativePath',
384 ];
385
386 foreach ($nonEmptyValues as $nonEmptyValue) {
387 if (empty($_GET[$nonEmptyValue])) {
388 throw new Exception();
389 }
390 }
391 }
392 }
393