PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 3.8.2
WP STAGING – WordPress Backup, Restore, Migration & Clone v3.8.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 years ago FileList 2 years ago Restore 2 years ago Backup.php 2 years ago BackupSpeedIndex.php 2 years ago Cancel.php 3 years ago Delete.php 3 years ago Edit.php 2 years ago FileInfo.php 3 years ago FileList.php 2 years ago Listing.php 2 years ago Parts.php 3 years ago PrepareJob.php 2 years ago Restore.php 2 years ago ScheduleList.php 2 years ago Status.php 3 years ago Upload.php 2 years ago
Upload.php
324 lines
1 <?php
2
3 namespace WPStaging\Backup\Ajax;
4
5 use Exception;
6 use WPStaging\Core\WPStaging;
7 use WPStaging\Framework\Component\AbstractTemplateComponent;
8 use WPStaging\Framework\Filesystem\DiskWriteCheck;
9 use WPStaging\Framework\Filesystem\Filesystem;
10 use WPStaging\Framework\TemplateEngine\TemplateEngine;
11 use WPStaging\Framework\Utils\Sanitize;
12 use WPStaging\Backup\BackupRepairer;
13 use WPStaging\Backup\Entity\BackupMetadata;
14 use WPStaging\Backup\Exceptions\DiskNotWritableException;
15 use WPStaging\Backup\Service\BackupsFinder;
16 use WPStaging\Backup\Task\Tasks\JobRestore\RestoreRequirementsCheckTask;
17 use WPStaging\Backup\WithBackupIdentifier;
18
19 class Upload extends AbstractTemplateComponent
20 {
21 use WithBackupIdentifier;
22
23 /** @var BackupsFinder */
24 private $backupsFinder;
25
26 /** @var BackupRepairer */
27 private $backupRepairer;
28
29 /** @var Sanitize */
30 private $sanitize;
31
32 /**
33 * @var Filesystem
34 */
35 private $filesystem;
36
37 public function __construct(BackupsFinder $backupsFinder, TemplateEngine $templateEngine, BackupRepairer $backupRepairer, Sanitize $sanitize)
38 {
39 parent::__construct($templateEngine);
40 $this->backupsFinder = $backupsFinder;
41 $this->backupRepairer = $backupRepairer;
42 $this->sanitize = $sanitize;
43 $this->filesystem = WPStaging::make(Filesystem::class);
44 }
45
46 public function render()
47 {
48 if (!$this->canRenderAjax()) {
49 return;
50 }
51
52 /**
53 * Example:
54 *
55 * name = "8xx.290.myftpupload.com_20210521-193355_967950a65d39 (2).wpstg"
56 * type = "application/octet-stream"
57 * tmp_name = "/tmp/phpYFrjBk"
58 * error = {int} 0
59 * size = {int} 1048576
60 */
61 $file = isset($_FILES['file']) ? $this->sanitize->sanitizeFileUpload($_FILES['file']) : null;
62
63 try {
64 $this->validateRequestData($file);
65 } catch (\Exception $e) {
66 wp_send_json_error([
67 'message' => !empty($e->getMessage()) ? $e->getMessage() : 'Invalid request data',
68 ], 400);
69 }
70
71 $resumableChunkNumber = isset($_GET['resumableChunkNumber']) ? $this->sanitize->sanitizeInt($_GET['resumableChunkNumber'], true) : 0;
72 $resumableChunkSize = isset($_GET['resumableChunkSize']) ? $this->sanitize->sanitizeInt($_GET['resumableChunkSize'], true) : 0;
73 $resumableCurrentChunkSize = isset($_GET['resumableCurrentChunkSize']) ? $this->sanitize->sanitizeInt($_GET['resumableCurrentChunkSize'], true) : 0;
74 $resumableTotalSize = isset($_GET['resumableTotalSize']) ? $this->sanitize->sanitizeInt($_GET['resumableTotalSize'], true) : 0;
75 $resumableTotalChunks = isset($_GET['resumableTotalChunks']) ? $this->sanitize->sanitizeInt($_GET['resumableTotalChunks'], true) : 0;
76 $uniqueIdentifierSuffix = isset($_GET['uniqueIdentifierSuffix']) ? $this->sanitize->sanitizeString($_GET['uniqueIdentifierSuffix']) : '';
77 $resumableIdentifier = isset($_GET['resumableIdentifier']) ? sanitize_file_name($_GET['resumableIdentifier']) : '';
78 $resumableFilename = isset($_GET['resumableFilename']) ? sanitize_file_name($_GET['resumableFilename']) : '';
79 $resumableRelativePath = isset($_GET['resumableRelativePath']) ? sanitize_file_name($_GET['resumableRelativePath']) : '';
80
81 $originalPath = $this->backupsFinder->getBackupsDirectory() . $resumableFilename;
82 if ($this->isBackupPart($originalPath) && file_exists($originalPath)) {
83 wp_send_json_error([
84 'message' => __('This backup part exists already', 'wp-staging'),
85 ], 409); // 409 status code for conflict
86 }
87
88 $fullPath = $this->backupsFinder->getBackupsDirectory() . $uniqueIdentifierSuffix . $resumableFilename . '.uploading';
89
90 $resumableInternalIdentifier = md5($fullPath);
91
92 // Check free disk space on the first request
93 if ($resumableChunkNumber <= 1) {
94 try {
95 WPStaging::make(DiskWriteCheck::class)->checkPathCanStoreEnoughBytes($this->backupsFinder->getBackupsDirectory(), $resumableTotalSize);
96 } catch (DiskNotWritableException $e) {
97 wp_send_json_error([
98 'message' => $e->getMessage(),
99 'isDiskFull' => true,
100 ], 507);
101 } catch (\RuntimeException $e) {
102 // no-op
103 }
104 }
105
106 // Assert chunks are in sequential order
107 if ($resumableChunkNumber > 1 && $resumableTotalChunks > 1) {
108 $nextExpectedChunk = (int)get_transient("wpstg.upload.nextExpectedChunk.$resumableInternalIdentifier");
109
110 if ($nextExpectedChunk !== $resumableChunkNumber) {
111 // 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.
112 wp_send_json_error('', 418);
113 }
114 }
115
116 update_option('wpstg.backups.doing_upload', true);
117
118 try {
119 $result = file_put_contents($fullPath, file_get_contents($file['tmp_name']), FILE_APPEND);
120
121 if (!$result) {
122 // Do a disk_free_space() check
123 try {
124 WPStaging::make(DiskWriteCheck::class)->checkPathCanStoreEnoughBytes($this->backupsFinder->getBackupsDirectory());
125 } catch (\RuntimeException $e) {
126 // no-op
127 }
128
129 // If that succeeds or could not be determined, also do a real write check.
130 WPStaging::make(DiskWriteCheck::class)->testDiskIsWriteable();
131 }
132 } catch (DiskNotWritableException $e) {
133 delete_option('wpstg.backups.doing_upload');
134
135 wp_send_json_error([
136 'message' => $e->getMessage(),
137 'isDiskFull' => true,
138 ], 507);
139 } catch (\Exception $e) {
140 delete_option('wpstg.backups.doing_upload');
141
142 wp_send_json_error([
143 'message' => $e->getMessage(),
144 ], 500);
145 }
146
147 // Last chunk?
148 if ($resumableChunkNumber === $resumableTotalChunks) {
149 try {
150 delete_transient("wpstg.upload.nextExpectedChunk.$resumableInternalIdentifier");
151 if ($this->isBackupPart($originalPath)) {
152 rename($fullPath, $originalPath);
153 return;
154 }
155
156 $this->validateBackupFile($fullPath);
157 rename($fullPath, $this->backupsFinder->getBackupsDirectory() . $uniqueIdentifierSuffix . $resumableFilename);
158 } catch (\Exception $e) {
159 if (file_exists($fullPath) && is_file($fullPath)) {
160 unlink($fullPath);
161 }
162
163 wp_send_json_error([
164 'message' => $e->getMessage(),
165 'backupFailedValidation' => true,
166 ], 500);
167 }
168 } else {
169 // Set the next expected chunk, to avoid scenarios where an erratic network connection could skip chunks or send them in unexpected order, eg:
170 // chunk.part.1
171 // chunk.part.2
172 // chunk.part.4 <-- not what we want!
173 // chunk.part.3
174 set_transient("wpstg.upload.nextExpectedChunk.$resumableInternalIdentifier", $resumableChunkNumber + 1, 1 * DAY_IN_SECONDS);
175 }
176
177 delete_option('wpstg.backups.doing_upload');
178
179 wp_send_json_success();
180 }
181
182 public function ajaxDeleteIncompleteUploads()
183 {
184 if (!$this->canRenderAjax()) {
185 wp_send_json_error(esc_html__('You do not have sufficient permissions to access this page.', 'wp-staging'));
186 }
187
188 try {
189 /** @var \SplFileInfo $splFileInfo */
190 foreach (new \DirectoryIterator($this->backupsFinder->getBackupsDirectory()) as $splFileInfo) {
191 if ($splFileInfo->isFile() && !$splFileInfo->isLink() && $splFileInfo->getExtension() === 'uploading') {
192 unlink($splFileInfo->getPathname());
193 }
194 }
195
196 wp_send_json_success();
197 } catch (\Exception $e) {
198 wp_send_json_error();
199 }
200 }
201
202 protected function validateBackupFile($fullPath)
203 {
204 clearstatcache();
205 $backupMetadata = new BackupMetadata();
206 $metadata = $backupMetadata->hydrateByFilePath($fullPath);
207
208 $isCreatedOnPro = $metadata->getCreatedOnPro();
209 $version = $metadata->getWpstgVersion();
210
211 if ($isCreatedOnPro && version_compare($version, RestoreRequirementsCheckTask::BETA_VERSION_LIMIT_PRO, '<')) {
212 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'));
213 }
214
215 $estimatedSize = $metadata->getBackupSize();
216 $isSplitBackup = $metadata->getIsMultipartBackup();
217
218 // Repairing the backup size in metadata
219 if ($estimatedSize === 0 && !$isSplitBackup) {
220 $this->backupRepairer->repairMetadataSize($fullPath);
221 return;
222 }
223
224 $realSize = filesize($fullPath);
225 $allowedDifferece = 1 * KB_IN_BYTES;
226
227 $smallerThanExpected = $realSize + $allowedDifferece - $estimatedSize < 0;
228 $biggerThanExpected = $realSize - $allowedDifferece > $estimatedSize;
229
230 if ($smallerThanExpected || $biggerThanExpected) {
231 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)));
232 }
233 }
234
235 protected function validateRequestData($file)
236 {
237 if (empty($_FILES) || !isset($_FILES['file']) || !is_array($file)) {
238 throw new Exception();
239 }
240
241 switch ((int)$file['error']) {
242 case UPLOAD_ERR_OK:
243 // Ok, no-op
244 break;
245 case UPLOAD_ERR_INI_SIZE:
246 case UPLOAD_ERR_FORM_SIZE:
247 case UPLOAD_ERR_PARTIAL:
248 case UPLOAD_ERR_NO_FILE:
249 case UPLOAD_ERR_NO_TMP_DIR:
250 case UPLOAD_ERR_CANT_WRITE:
251 case UPLOAD_ERR_EXTENSION:
252 throw new Exception();
253 }
254
255 if (empty($file['tmp_name']) || !file_exists($file['tmp_name'])) {
256 throw new Exception();
257 }
258
259 if (!empty($file['name']) && !$this->filesystem->isWpstgBackupFile($file['name'])) {
260 throw new Exception(sprintf(__('Invalid backup file extension: %s', 'wp-staging'), $file['name']));
261 }
262
263 /**
264 * Example:
265 *
266 * resumableChunkNumber = "1"
267 * resumableChunkSize = "1048576"
268 * resumableCurrentChunkSize = "1048576"
269 * resumableTotalSize = "14209912"
270 * resumableType = ""
271 * resumableIdentifier = "14209912-multitestswp-staginglocal_fcd2fae486dcwpstg"
272 * resumableFilename = "multi.tests.wp-staging.local_fcd2fae486dc.wpstg"
273 * resumableRelativePath = "multi.tests.wp-staging.local_fcd2fae486dc.wpstg"
274 * resumableTotalChunks = "13"
275 */
276 $requiredValues = [
277 'resumableChunkNumber',
278 'resumableChunkSize',
279 'resumableCurrentChunkSize',
280 'resumableTotalSize',
281 'resumableIdentifier',
282 'resumableFilename',
283 'resumableRelativePath',
284 'resumableTotalChunks',
285 ];
286
287 foreach ($requiredValues as $requiredValue) {
288 if (!isset($_GET[$requiredValue])) {
289 throw new Exception();
290 }
291 }
292
293 $numericValues = [
294 'resumableChunkNumber',
295 'resumableChunkSize',
296 'resumableCurrentChunkSize',
297 'resumableTotalSize',
298 'resumableTotalChunks',
299 ];
300
301 foreach ($numericValues as $numericValue) {
302 if (!isset($_GET[$numericValue])) {
303 throw new Exception();
304 }
305
306 if (!filter_var($_GET[$numericValue], FILTER_VALIDATE_INT)) {
307 throw new Exception();
308 }
309 }
310
311 $nonEmptyValues = [
312 'resumableIdentifier',
313 'resumableFilename',
314 'resumableRelativePath',
315 ];
316
317 foreach ($nonEmptyValues as $nonEmptyValue) {
318 if (empty($_GET[$nonEmptyValue])) {
319 throw new Exception();
320 }
321 }
322 }
323 }
324