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