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 / Backend / Modules / Jobs / Delete.php
wp-staging / Backend / Modules / Jobs Last commit date
Cleaners 5 years ago Exceptions 5 years ago Cancel.php 2 years ago CancelUpdate.php 2 years ago Cloning.php 2 years ago CloningProcess.php 2 years ago Data.php 2 years ago Database.php 2 years ago Delete.php 2 years ago Directories.php 2 years ago Files.php 2 years ago Finish.php 2 years ago Job.php 2 years ago JobExecutable.php 2 years ago Logs.php 3 years ago PreserveDataFirstStep.php 3 years ago PreserveDataSecondStep.php 3 years ago ProcessLock.php 2 years ago Scan.php 2 years ago SearchReplace.php 2 years ago TotalStepsAreNumberOfTables.php 5 years ago Updating.php 2 years ago
Delete.php
594 lines
1 <?php
2
3 namespace WPStaging\Backend\Modules\Jobs;
4
5 use Exception;
6 use FilesystemIterator;
7 use mysqli;
8 use stdClass;
9 use wpdb;
10 use WPStaging\Backend\Modules\Jobs\Exceptions\CloneNotFoundException;
11 use WPStaging\Core\Utils\Logger;
12 use WPStaging\Core\WPStaging;
13 use WPStaging\Framework\Filesystem\Filesystem;
14 use WPStaging\Framework\Filesystem\FilesystemExceptions;
15 use WPStaging\Framework\Staging\Sites;
16 use WPStaging\Framework\Utils\Sanitize;
17 use WPStaging\Framework\Utils\Strings;
18
19 /**
20 * Class Delete
21 * @package WPStaging\Backend\Modules\Jobs
22 */
23 class Delete extends Job
24 {
25 /**
26 * @var string
27 */
28 const DELETE_STATUS_FINISHED = 'finished';
29
30 /**
31 * @var string
32 */
33 const DELETE_STATUS_UNFINISHED = 'unfinished';
34
35 /**
36 * @var stdClass|false
37 */
38 private $clone = false;
39
40 /**
41 * The path to delete
42 * @var string
43 */
44 private $deleteDir;
45
46 /**
47 * @var null|object|array
48 */
49 private $tables = null;
50
51 /**
52 * @var object|null
53 */
54 private $job = null;
55
56 /**
57 * @var wpdb
58 */
59 public $wpdb;
60
61 /**
62 * @var bool
63 */
64 private $isExternalDb;
65
66 /** @var Strings */
67 private $strings;
68
69 /** @var Sanitize */
70 private $sanitize;
71
72 public function __construct()
73 {
74 parent::__construct();
75
76 /** @var Sanitize */
77 $this->sanitize = WPStaging::make(Sanitize::class);
78 $this->deleteDir = !empty($_POST['deleteDir']) ? $this->sanitize->sanitizePath($_POST['deleteDir']) : '';
79 $this->strings = new Strings();
80 }
81
82 /**
83 * @param bool $isExternal
84 * @return void
85 */
86 public function setIsExternalDb(bool $isExternal = false)
87 {
88 $this->isExternalDb = $isExternal;
89 }
90
91 /**
92 * Sets Clone and Table Records
93 * @param null|array $clone
94 * @return bool
95 */
96 public function setData($clone = null): bool
97 {
98 if (!is_array($clone)) {
99 $this->getCloneRecords();
100 } else {
101 $this->clone = (object)$clone;
102 }
103
104 // Set cache file name for the delete cloning job
105 $this->cache->setFilename($this->getJobCacheFileName());
106
107 if (!$this->isExternalDatabase()) {
108 $this->wpdb = WPStaging::getInstance()->get("wpdb");
109 $this->getTableRecords();
110 return true;
111 }
112
113 if ($this->isExternalDatabaseError()) {
114 return false;
115 }
116
117 $this->wpdb = $this->getExternalStagingDb();
118 $this->getTableRecords();
119 return true;
120 }
121
122 /**
123 * Get database object to interact with
124 * @return wpdb
125 */
126 private function getExternalStagingDb(): wpdb
127 {
128 if ($this->clone->databaseSsl && !defined('MYSQL_CLIENT_FLAGS')) {
129 // phpcs:disable PHPCompatibility.Constants.NewConstants.mysqli_client_ssl_dont_verify_server_certFound
130 define('MYSQL_CLIENT_FLAGS', MYSQLI_CLIENT_SSL | MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT);
131 }
132
133 return new wpdb($this->clone->databaseUser, $this->clone->databasePassword, $this->clone->databaseDatabase, $this->clone->databaseServer);
134 }
135
136 /**
137 * Date database name
138 * @return string
139 */
140 public function getDbName(): string
141 {
142 return $this->wpdb->dbname;
143 }
144
145 /**
146 * Check if external database is used
147 * @return bool
148 */
149 protected function isExternalDatabase(): bool
150 {
151 if (isset($this->isExternalDb)) {
152 return $this->isExternalDb;
153 }
154
155 if (!empty($this->clone->databaseUser) && !empty($this->clone->databasePassword) && !empty($this->clone->databaseDatabase) && !empty($this->clone->databaseServer)) {
156 return true;
157 }
158
159 return false;
160 }
161
162 /**
163 * Get clone
164 * @param null|string $name
165 * @return void
166 */
167 private function getCloneRecords($name = null)
168 {
169 if ($name === null && !isset($_POST["clone"])) {
170 $this->log("Clone name is not set", Logger::TYPE_FATAL);
171 $this->returnException("Clone name is not set");
172 }
173
174 if ($name === null) {
175 $name = $this->sanitize->sanitizeString($_POST["clone"]);
176 }
177
178 $clones = get_option(Sites::STAGING_SITES_OPTION, []);
179
180 if (empty($clones) || !isset($clones[$name])) {
181 $this->log("Couldn't find clone name $name or no existing clone", Logger::TYPE_FATAL);
182 $this->returnException("Couldn't find clone name $name or no existing clone");
183 }
184
185 $this->clone = $clones[$name];
186 $this->clone["name"] = $name;
187
188 $this->clone = (object)$this->clone;
189
190 unset($clones);
191 }
192
193 /**
194 * Get Tables
195 * @return void
196 */
197 private function getTableRecords()
198 {
199 $stagingPrefix = $this->getStagingPrefix();
200
201 // Escape "_" to allow searching for that character
202 $prefix = $this->strings->replaceLastMatch('_', '\_', $stagingPrefix);
203
204 if ($this->isExternalDatabase()) { // Show all tables if its an external database
205 $tables = $this->wpdb->get_results("SHOW TABLE STATUS");
206 } else {
207 $tables = $this->wpdb->get_results("SHOW TABLE STATUS LIKE '$prefix%'");
208 }
209
210 $this->tables = [];
211
212 // no results
213 if ($tables !== null) {
214 foreach ($tables as $table) {
215 $this->tables[] = [
216 "name" => $table->Name,
217 "size" => $this->utilsMath->formatSize($table->Data_length + $table->Index_length)
218 ];
219 }
220 }
221
222 $this->tables = json_decode(json_encode($this->tables));
223 }
224
225 /**
226 * Check and return prefix of the staging site
227 * @return string
228 */
229 private function getStagingPrefix(): string
230 {
231 if ($this->isExternalDatabase() && !empty($this->clone->databasePrefix)) {
232 $this->clone->prefix = $this->clone->databasePrefix;
233 return $this->clone->databasePrefix;
234 }
235
236 // Prefix not defined! Happens if staging site has been generated with older version of wpstg
237 // Try to get staging prefix from wp-config.php of staging site
238 if (empty($this->clone->prefix)) {
239 $path = ABSPATH . $this->clone->directoryName . "/wp-config.php";
240 if (($content = @file_get_contents($path)) === false) {
241 $this->log("Can not open $path. Can't read contents", Logger::TYPE_ERROR);
242 }
243
244 preg_match("/table_prefix\s*=\s*'(\w*)';/", $content, $matches);
245
246 if (!empty($matches[1])) {
247 $this->clone->prefix = $matches[1];
248 } else {
249 $this->returnException("Fatal Error: Can not delete staging site. Can not find Prefix. '$matches[1]'. Stopping for security reasons. Creating a new staging site will likely resolve this the next time. Contact support@wp-staging.com");
250 }
251 }
252
253 if (empty($this->clone->prefix)) {
254 $this->returnException("Fatal Error: Can not delete staging site. Can not find table prefix. Contact support@wp-staging.com");
255 }
256
257 // Check if staging prefix is the same as the live prefix
258 if (empty($this->options->databaseUser) && $this->wpdb->prefix === $this->clone->prefix) {
259 $this->log("Fatal Error: Can not delete staging site. Prefix. '{$this->clone->prefix}' is used for the production site. Stopping for security reasons. Go to Sites > Actions > Edit Data and correct the table prefix or contact us.");
260 $this->returnException("Fatal Error: Can not delete staging site. Prefix. '{$this->clone->prefix}' is used for the production site. Stopping for security reasons. Go to Sites > Actions > Edit Data and correct the table prefix or contact us");
261 }
262
263 return $this->clone->prefix;
264 }
265
266 /**
267 * @return stdClass|false
268 */
269 public function getClone()
270 {
271 return $this->clone;
272 }
273
274 /**
275 * @return null|object
276 */
277 public function getTables()
278 {
279 return $this->tables;
280 }
281
282 /**
283 * Start Module
284 * @param null|array $clone
285 * @return void
286 * @throws CloneNotFoundException
287 * @throws Exception
288 */
289 public function start($clone = null)
290 {
291 // Set data
292 $this->setData($clone);
293
294 // Get the job first
295 $this->getJob();
296
297 $method = "delete" . ucwords($this->job->current);
298
299 if (method_exists($this, $method)) {
300 $this->{$method}();
301 return;
302 }
303
304 // If method doesn't exist probably the cache file was corrupted
305 // Just delete that corrupted cache file and restart itself.
306 $this->cache->delete();
307 $this->start($clone);
308 }
309
310 /**
311 * Get job data
312 * @return void
313 * @throws Exception
314 */
315 public function getJob()
316 {
317 $this->job = $this->cache->get();
318 $this->job = json_decode(json_encode($this->job)); // Convert to object
319
320 if ($this->job !== null && isset($this->job->current)) {
321 return;
322 }
323
324 // Generate JOB
325 $this->job = (object)[
326 "current" => "tables",
327 "nextDirectoryToDelete" => $this->clone->path,
328 "name" => $this->clone->name
329 ];
330
331 $this->cache->save($this->job);
332 }
333
334 /**
335 * @return bool
336 * @throws Exception
337 */
338 private function updateJob(): bool
339 {
340 $this->job->nextDirectoryToDelete = trim($this->job->nextDirectoryToDelete);
341 $result = $this->cache->save($this->job);
342
343 return $result !== false;
344 }
345
346 /**
347 * @return array
348 */
349 private function getTablesToRemove(): array
350 {
351 $tables = $this->getTableNames();
352
353 if (!isset($_POST["excludedTables"]) || !is_array($_POST["excludedTables"]) || empty($_POST["excludedTables"])) {
354 return $tables;
355 }
356
357 return array_diff($tables, $this->sanitize->sanitizeString($_POST["excludedTables"]));
358 }
359
360 /**
361 * @return array
362 */
363 private function getTableNames(): array
364 {
365 return (!is_array($this->tables)) ? [] : array_map(function ($value) {
366 return ($value->name);
367 }, $this->tables);
368 }
369
370 /**
371 * Delete Tables
372 * @return void
373 * @throws Exception
374 * @todo DRY the code by implementing through WPStaging\Framework\Database\TableService::deleteTablesStartWith
375 */
376 public function deleteTables()
377 {
378
379 if ($this->isOverThreshold()) {
380 $this->log("Deleting: Is over threshold");
381 return;
382 }
383
384 $tables = $this->getTablesToRemove();
385
386 foreach ($tables as $table) {
387 // PROTECTION: Never delete any table that begins with wp prefix of live site
388 if (!$this->isExternalDatabase() && $this->strings->startsWith($table, $this->wpdb->prefix)) {
389 $this->log("Fatal Error: Trying to delete table $table of main WP installation!", Logger::TYPE_CRITICAL);
390 }
391
392 $this->wpdb->query("DROP TABLE $table");
393 }
394
395 // Move on to the next
396 $this->job->current = "directory";
397 $this->updateJob();
398 }
399
400 /**
401 * Delete complete directory including all files and sub folders
402 * @return void
403 * @throws Exception
404 */
405 public function deleteDirectory()
406 {
407 if ($this->isFatalError()) {
408 $this->returnException('Can not delete directory: ' . $this->deleteDir . '. This seems to be the root directory. Exclude this directory from deleting and try again.');
409 throw new Exception('Can not delete directory: ' . $this->deleteDir . ' This seems to be the root directory. Exclude this directory from deleting and try again.');
410 }
411
412 // Finished or path does not exist
413 if (
414 empty($this->deleteDir) ||
415 $this->deleteDir === get_home_path() ||
416 !is_dir($this->deleteDir)
417 ) {
418 $this->job->current = "finish";
419 $this->updateJob();
420 $this->deleteFinish();
421 return;
422 }
423
424 $this->log("Delete staging site: " . $this->clone->path);
425
426 // Make sure the root dir is never deleted!
427 if ($this->deleteDir === get_home_path()) {
428 $this->log("Fatal Error 8: Trying to delete root of WP installation!", Logger::TYPE_CRITICAL);
429 $this->returnException('Fatal Error 8: Trying to delete root of WP installation!');
430 }
431
432 // Check if threshold is reached
433 if ($this->isOverThreshold()) {
434 return;
435 }
436
437 $clone = (string)$this->clone->path;
438 $errorMessage = sprintf(__('We could not delete the staging site completely. There are still files in the folder %s that could not be deleted. This could be a write permission issue. Try to delete the folder manually by using FTP or a file manager plugin.<br/> If this happens again please contact us at support@wp-staging.com', 'wp-staging'), $clone);
439 $deleteStatus = self::DELETE_STATUS_FINISHED;
440 $isDeleted = false;
441
442 try {
443 $isDeleted = $this->cleanStagingDirectory($this->deleteDir);
444 } catch (FilesystemExceptions $ex) {
445 $errorMessage = $ex->getMessage();
446 $deleteStatus = self::DELETE_STATUS_UNFINISHED;
447 }
448
449 // If the folder has still not been deleted and there was no exception, we will try again deleting it.
450 if (!$isDeleted && $deleteStatus !== self::DELETE_STATUS_UNFINISHED) {
451 return;
452 }
453
454 // Throw fatal error if the folder has still not been deleted and there are files in it
455 if (!$this->isEmptyDir($this->deleteDir)) {
456 $response = [
457 'job' => 'delete',
458 'status' => true,
459 'delete' => $deleteStatus,
460 'message' => $errorMessage,
461 'error' => true,
462 ];
463 wp_die(json_encode($response));
464 }
465
466 // Successful finish deleting job
467 $this->deleteFinish();
468 }
469
470 /**
471 * @param string $deleteDir
472 * @return bool true if the directory is empty or deleted successfully otherwise false
473 * @throws FilesystemExceptions
474 */
475 protected function cleanStagingDirectory(string $deleteDir): bool
476 {
477 if ($this->isEmptyDir($deleteDir)) {
478 return true;
479 }
480
481 /** @var Filesystem */
482 $fs = (new Filesystem())
483 ->setShouldStop([$this, 'isOverThreshold'])
484 ->shouldPermissionExceptionsBypass(true)
485 ->setRecursive();
486
487 try {
488 if (!$fs->delete($this->deleteDir)) {
489 return false;
490 }
491 } catch (FilesystemExceptions $ex) {
492 throw $ex;
493 }
494
495 return true;
496 }
497
498 /**
499 * Check if directory exists and is not empty
500 * @param string $dir
501 * @return bool
502 */
503 private function isEmptyDir($dir): bool
504 {
505 if (!is_dir($dir)) {
506 return true;
507 }
508
509 $iterator = new FilesystemIterator($dir);
510
511 return !$iterator->valid();
512 }
513
514 /**
515 * @return bool
516 */
517 public function isFatalError(): bool
518 {
519 $homePath = rtrim(get_home_path(), "/");
520 return $homePath === rtrim($this->deleteDir, "/");
521 }
522
523 /**
524 * Finish / Update Existing Clones
525 * @return void
526 * @throws Exception
527 */
528 public function deleteFinish()
529 {
530 $response = [
531 'delete' => self::DELETE_STATUS_FINISHED,
532 ];
533
534 $existingClones = get_option(Sites::STAGING_SITES_OPTION, []);
535
536 // Check if clone exist and then remove it from options
537 $this->log("Verifying existing clones...");
538 foreach ($existingClones as $name => $clone) {
539 if ($clone["path"] === $this->clone->path) {
540 unset($existingClones[$name]);
541 }
542 }
543
544 if (update_option(Sites::STAGING_SITES_OPTION, $existingClones) === false) {
545 $this->log("Delete: Nothing to save.'");
546 }
547
548 // Delete cached file
549 $this->cache->delete();
550 $this->cloneOptionCache->delete();
551
552 wp_die(json_encode($response));
553 }
554
555 /**
556 * Check if there is error in external database connection
557 * can happen if the external database does not exist or stored credentials are wrong
558 * @return bool
559 *
560 * @todo replace it logic with DbInfo once collation check PR is merged.
561 */
562 private function isExternalDatabaseError(): bool
563 {
564 if ($this->clone->databaseSsl) {
565 // wpdb requires this constant for SSL use
566 if (!defined('MYSQL_CLIENT_FLAGS')) {
567 // phpcs:disable PHPCompatibility.Constants.NewConstants.mysqli_client_ssl_dont_verify_server_certFound
568 define('MYSQL_CLIENT_FLAGS', MYSQLI_CLIENT_SSL | MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT);
569 }
570
571 $db = mysqli_init();
572 $db->real_connect($this->clone->databaseServer, $this->clone->databaseUser, $this->clone->databasePassword, $this->clone->databaseDatabase, null, null, MYSQL_CLIENT_FLAGS);
573 } else {
574 $db = new mysqli($this->clone->databaseServer, $this->clone->databaseUser, $this->clone->databasePassword, $this->clone->databaseDatabase);
575 }
576
577 if ($db->connect_error) {
578 return true;
579 }
580
581 return false;
582 }
583
584 /**
585 * Return the cache file which contains the info about current job
586 *
587 * @return string
588 */
589 private function getJobCacheFileName(): string
590 {
591 return "delete_job_{$this->clone->name}";
592 }
593 }
594