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