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