PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 4.7.3
WP STAGING – WordPress Backup, Restore, Migration & Clone v4.7.3
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 / uninstall.php
wp-staging Last commit date
Backend 2 months ago Backup 2 months ago Basic 3 months ago Component 6 months ago Core 3 months ago Framework 2 months ago Frontend 5 months ago Notifications 8 months ago Staging 2 months ago assets 2 months ago languages 2 months ago resources 1 year ago vendor_wpstg 2 months ago views 2 months ago CONTRIBUTING.md 1 year ago Deactivate.php 8 months ago README.md 3 months ago SECURITY.md 2 years ago autoloader.php 6 months ago bootstrap.php 9 months ago constantsFree.php 2 months ago freeBootstrap.php 1 year ago install.php 1 year ago opcacheBootstrap.php 2 months ago readme.txt 2 months ago runtimeRequirements.php 3 months ago uninstall.php 2 months ago wp-staging-error-handler.php 6 months ago wp-staging.php 2 months ago
uninstall.php
580 lines
1 <?php
2
3
4 if (!defined('WP_UNINSTALL_PLUGIN')) {
5 exit;
6 }
7
8 /**
9 * Handles plugin uninstallation and cleanup of WP Staging data
10 *
11 * This class manages the complete uninstallation process including:
12 * - Detecting single site vs multisite/network uninstall scenarios
13 * - Distinguishing between Basic and Pro version uninstallation
14 * - Preserving data when both versions are installed
15 * - Cleaning up options, transients, and cron events
16 * - Removing plugin directories (except those containing backups)
17 * - Respecting user's "Remove Data on Uninstall" setting
18 *
19 * The class runs in standalone context without the plugin's autoloader,
20 * so it must be self-contained with no external dependencies.
21 *
22 * Note: Avoids using class constants to prevent loading the whole plugin.
23 * @package WPSTG
24 * @subpackage Uninstall
25 * @copyright Copyright (c) 2015, René Hermenau
26 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
27 * @since 0.9.0
28 */
29 class Uninstall
30 {
31 /**
32 * Options we want to preserve.
33 * These should remain until we have a staging site deletion routine.
34 */
35 private $preserveOptions = [
36 'wpstg_existing_clones',
37 'wpstg_existing_clones_beta',
38 'wpstg_staging_sites',
39 'wpstg_connection',
40 ];
41
42 public function __construct()
43 {
44 if (!is_multisite()) {
45 $this->runForSingleSite(); // Normal single-site uninstall
46 return;
47 }
48
49 if ($this->isNetworkUninstall()) {
50 $this->runForNetwork(); // Full cleanup across all sites + network data
51 } else {
52 $this->runForSingleSite(); // Only clean current subsite
53 }
54 }
55
56 /**
57 * @return void
58 */
59 private function runForNetwork()
60 {
61 $siteIds = get_sites(['fields' => 'ids']);
62 foreach ($siteIds as $siteId) {
63 switch_to_blog($siteId);
64 $this->runForSingleSite();
65 restore_current_blog();
66 }
67
68 $this->deleteNetworkOptions();
69 }
70
71 /**
72 * @return void
73 */
74 private function runForSingleSite()
75 {
76 $settings = $this->getSettings();
77
78 if (empty($settings['unInstallOnDelete']) || $settings['unInstallOnDelete'] !== '1') {
79 return;
80 }
81
82 // If Pro is installed, no matter if active or not, and we're uninstalling Basic, do nothing to preserve all data.
83 // This is to make sure pro version still works once user installs free version again in case he only temporary uninstalled it.
84 if ($this->isProInstalled() && $this->isUninstallingBasic()) {
85 return;
86 }
87
88 // If Basic is installed, and we're uninstalling Pro, remove only Pro data
89 if ($this->isBasicInstalled() && $this->isUninstallingPro()) {
90 $this->deleteOptions($this->getProOptions());
91 return;
92 }
93
94 // If Basic not installed, and we're uninstalling Pro, remove all data
95 if (!$this->isBasicInstalled() && $this->isUninstallingPro()) {
96 $this->performCompleteCleanup(true);
97 return;
98 }
99
100 // If Pro not installed, and we're uninstalling Basic, remove all data
101 if (!$this->isProInstalled() && $this->isUninstallingBasic()) {
102 $this->performCompleteCleanup(false);
103 }
104 }
105
106 /**
107 * @param bool $isPro
108 * @return void
109 */
110 private function performCompleteCleanup(bool $isPro)
111 {
112 $this->deleteOptions($this->getBasicOptions());
113 if ($isPro) {
114 $this->deleteOptions($this->getProOptions());
115 }
116
117 $this->dropWpStagingSettingsTable();
118 $this->deleteTransients();
119 $this->cleanupEmptyPreserveOptions();
120 $this->clearCronEvents();
121 $this->cleanupWpStagingDirectories();
122 }
123
124 /**
125 * @return void
126 */
127 private function dropWpStagingSettingsTable()
128 {
129 global $wpdb;
130
131 if (!($wpdb instanceof \wpdb)) {
132 return;
133 }
134
135 $tableName = str_replace('`', '', $wpdb->prefix . 'wpstg_settings');
136 $wpdb->query("DROP TABLE IF EXISTS `{$tableName}`");
137 }
138
139 /**
140 * @return bool
141 */
142 private function isNetworkUninstall(): bool
143 {
144 return (is_multisite() && is_network_admin());
145 }
146
147 /**
148 * @return bool
149 */
150 private function isUninstallingBasic(): bool
151 {
152 $pluginDirs = ['wp-staging', 'wp-staging-1'];
153 return $this->isUninstallingPlugin($pluginDirs);
154 }
155
156 /**
157 * @return bool
158 */
159 private function isUninstallingPro(): bool
160 {
161 $pluginDirs = ['wp-staging-pro', 'wp-staging-pro-1'];
162 return $this->isUninstallingPlugin($pluginDirs);
163 }
164
165 /**
166 * @param array $pluginDirs
167 * @return bool
168 */
169 private function isUninstallingPlugin(array $pluginDirs): bool
170 {
171 return in_array(basename(__DIR__), $pluginDirs);
172 }
173
174 /**
175 * @return bool
176 */
177 private function isProInstalled(): bool
178 {
179 // First try header-based detection (more robust)
180 if ($this->isProInstalledByHeaders()) {
181 return true;
182 }
183
184 // Fallback to file-based detection for backward compatibility
185 $plugins = [
186 'wp-staging-pro-1/wp-staging-pro.php',
187 'wp-staging-pro/wp-staging-pro.php',
188 ];
189 foreach ($plugins as $plugin) {
190 if ($this->isPluginInstalled($plugin)) {
191 return true;
192 }
193 }
194
195 return false;
196 }
197
198 /**
199 * @return bool
200 */
201 private function isBasicInstalled(): bool
202 {
203 // First try header-based detection (more robust)
204 if ($this->isBasicInstalledByHeaders()) {
205 return true;
206 }
207
208 // Fallback to file-based detection for backward compatibility
209 $plugins = [
210 'wp-staging-1/wp-staging.php',
211 'wp-staging/wp-staging.php',
212 ];
213 foreach ($plugins as $plugin) {
214 if ($this->isPluginInstalled($plugin)) {
215 return true;
216 }
217 }
218
219 return false;
220 }
221
222 /**
223 * @param $pluginName
224 * @return bool
225 */
226 private function isPluginInstalled($pluginName): bool
227 {
228 return file_exists( WP_PLUGIN_DIR . '/' . $pluginName );
229 }
230
231 /**
232 * @param array $identifiers
233 * @return bool
234 */
235 private function findPluginByIdentifiers(array $identifiers): bool
236 {
237 if (!function_exists('get_plugins')) {
238 require_once ABSPATH . 'wp-admin/includes/plugin.php';
239 }
240
241 $plugins = get_plugins();
242 $searchCriteria = array_change_key_case($identifiers, CASE_LOWER);
243
244 foreach ($plugins as $file => $data) {
245 $name = strtolower($data['Name'] ?? '');
246 $slug = strtolower(dirname($file));
247 $mainFile = strtolower(basename($file, '.php'));
248
249 if (isset($searchCriteria['file']) && strtolower($searchCriteria['file']) === strtolower($file)) {
250 return true;
251 }
252
253 if (isset($searchCriteria['slug']) && ($slug === $searchCriteria['slug'] || $mainFile === $searchCriteria['slug'])) {
254 return true;
255 }
256
257 if (isset($searchCriteria['name']) && $name === strtolower($searchCriteria['name'])) {
258 return true;
259 }
260 }
261
262 return false;
263 }
264
265 /**
266 * @return bool
267 */
268 private function isBasicInstalledByHeaders(): bool
269 {
270 return $this->findPluginByIdentifiers([
271 'slug' => 'wp-staging',
272 'name' => 'WP Staging',
273 'file' => 'wp-staging/wp-staging.php',
274 ]);
275 }
276
277 /**
278 * @return bool
279 */
280 private function isProInstalledByHeaders(): bool
281 {
282 return $this->findPluginByIdentifiers([
283 'slug' => 'wp-staging-pro',
284 'name' => 'WP Staging Pro',
285 'file' => 'wp-staging-pro/wp-staging-pro.php',
286 ]);
287 }
288
289 /**
290 * @return array
291 */
292 private function getSettings(): array
293 {
294 return json_decode(json_encode(get_option('wpstg_settings', [])), true) ?? [];
295 }
296
297 /**
298 * @return string[]
299 */
300 private function getBasicOptions(): array
301 {
302 return [
303 'wpstg_settings',
304 'wpstg_clone_settings',
305 'wpstg_free_install_date',
306 'wpstg_installDate',
307 'wpstg_version',
308 'wpstg_version_upgraded_from',
309 'wpstg_free_upgrade_date',
310 'wpstg_unique_identifier',
311 'wpstg_is_staging_site',
312 'wpstg_resave_permalinks_executed',
313 'wpstg_rmpermalinks_executed',
314 'wpstg_connection',
315 'wpstg_staging_sites',
316 'wpstg_existing_clones',
317 'wpstg_existing_clones_beta',
318 'wpstg_execute',
319 'wpstg_emails_disabled',
320 'wpstg_woo_scheduler_disabled',
321 'wpstg_clone_excluded_files_list',
322 'wpstg_clone_excluded_gd_files_list',
323 'wpstg_freemius_notice',
324 'wpstg_queue_table_structure_version',
325 'wpstg_settings_table_version',
326 'wpstg_q_feature_detection_ajax_available',
327 'wpstg_analytics_has_consent',
328 'wpstg_analytics_modal_dismissed',
329 'wpstg_analytics_notice_dismissed',
330 'wpstg_analytics_consent_remind_me',
331 'wpstg_default_color_mode',
332 'wpstg_default_os_color_mode',
333 'wpstg_last_backup_info',
334 'wpstg_backups_retention',
335 'wpstg_otps',
336 'wpstg_access_token',
337 'wpstg_disabled_notice',
338 'wpstg_send_email_as_html',
339 'wpstg_cli_notice_hidden_forever',
340 'wpstg_cli_dock_cta_shown',
341 ];
342 }
343
344 /**
345 * @return string[]
346 */
347 private function getProOptions(): array
348 {
349 return [
350 'wpstgpro_version',
351 'wpstgpro_version_upgraded_from',
352 'wpstgpro_install_date',
353 'wpstgpro_upgrade_date',
354 'wpstg_license_key',
355 'wpstg_license_status',
356 'wpstg_pro_latest_version',
357 'wpstg_googledrive',
358 'wpstg_dropbox',
359 'wpstg_one-drive',
360 'wpstg_pcloud',
361 'wpstg_amazons3',
362 'wpstg_sftp',
363 'wpstg_digitalocean-spaces',
364 'wpstg_wasabi',
365 'wpstg_generic-s3',
366 'wpstg_backup_schedules',
367 'wpstg_backup_schedules_send_error_report',
368 'wpstg_backup_schedules_report_email',
369 'wpstg_backup_schedules_send_slack_error_report',
370 'wpstg_backup_schedules_report_slack_webhook',
371 'wpstg_current_site_login_links',
372 'wpstg_remote_sync_api_token',
373 'wpstg_remote_sync_password',
374 ];
375 }
376
377 /**
378 * @return string[]
379 */
380 private function getAllTransients(): array
381 {
382 return [
383 'wpstg_current_job',
384 'wpstg_rest_url',
385 'wpstg.run_daily',
386 'wpstg_show_login_notice',
387 'wpstg_user_logged_in_status',
388 'wpstg_auto_login_failed',
389 'wpstg_auto_login_failed_reason',
390 'wpstg_failed_auto_login_attempts',
391 'wpstg_otp_sent',
392 'wpstg_otp_consecutive_failures',
393 'wpstg_otp_locked',
394 'wpstg_redirect_url',
395 'wpstg_remote_sync_session',
396 'wpstg_remote_sync_session_data',
397 'wpstg_remote_sync_session_events_offset',
398 'wpstg.queue.request.get_method',
399 'is_invalid_backup_file_index',
400 'wpstg_permalinks_do_purge',
401 'wpstg_purge_litespeed_cache',
402 'wpstg_activation_redirect',
403 'wpstg_pro_activation_redirect',
404 'wpstg_weekly_version_update',
405 'wpstg_rate_limit_update_check',
406 'wpstg_issue_report_submitted',
407 'wpstg.backup.schedules.slack_report_sent',
408 'wpstg_email_notification_access_token',
409 'wpstg.directory_listing.last_checked',
410 ];
411 }
412
413 /**
414 * @param array $optionNames
415 * @return void
416 */
417 private function deleteOptions(array $optionNames)
418 {
419 foreach ($optionNames as $optionName) {
420 // Skip if this option should be preserved
421 if (in_array($optionName, $this->preserveOptions, true)) {
422 continue;
423 }
424
425 delete_option($optionName);
426 }
427 }
428
429 /**
430 * @return void
431 */
432 private function deleteTransients()
433 {
434 $transients = $this->getAllTransients();
435 foreach ($transients as $transientName) {
436 delete_transient($transientName);
437 }
438 }
439
440 /**
441 * @return void
442 */
443 private function cleanupEmptyPreserveOptions()
444 {
445 $this->cleanupEmptyOptions($this->preserveOptions);
446 }
447
448 /**
449 * @param array $options
450 * @param bool $isSiteOptions
451 * @return void
452 */
453 private function cleanupEmptyOptions(array $options, bool $isSiteOptions = false)
454 {
455 foreach ($options as $option) {
456 $value = $isSiteOptions ? get_site_option($option): get_option($option);
457 if (empty($value)) {
458 $isSiteOptions ? delete_site_option($option): delete_option($option);
459 }
460 }
461 }
462
463 /**
464 * @return void
465 */
466 private function clearCronEvents()
467 {
468 // @see WPStaging\Core\Cron\Cron::ACTION_WEEKLY_EVENT
469 wp_clear_scheduled_hook('wpstg_weekly_event');
470 }
471
472 /**
473 * @return void
474 */
475 private function cleanupWpStagingDirectories()
476 {
477 $uploadsBase = $this->getUploadsDirectory() . 'wp-staging/';
478 $directoriesToClean = [
479 $this->getWpContentDirectory() . 'wp-staging',
480 ];
481
482 // Delete wp-staging uploads dir if it does not contain .wpstg files
483 if (!$this->isDirectoryContainsWpstgFiles($uploadsBase . 'backups')) {
484 $directoriesToClean[] = $uploadsBase;
485 } else {
486 $directoriesToClean[] = $uploadsBase . 'cache';
487 $directoriesToClean[] = $uploadsBase . 'logs';
488 $directoriesToClean[] = $uploadsBase . 'tmp';
489 }
490
491 foreach ($directoriesToClean as $directory) {
492 $this->deleteDirectoryRecursively($directory);
493 }
494 }
495
496 /**
497 * @param string $directory
498 * @return void
499 */
500 private function deleteDirectoryRecursively(string $directory)
501 {
502 if (!is_dir($directory)) {
503 return;
504 }
505
506 $absPath = trailingslashit(ABSPATH);
507 if ($directory === $absPath || $directory === dirname($absPath)) {
508 return;
509 }
510
511 foreach (new \DirectoryIterator($directory) as $item) {
512 if ($item->isDot()) {
513 continue;
514 }
515
516 $itemPath = $item->getPathname();
517 if ($item->isDir()) {
518 $this->deleteDirectoryRecursively($itemPath);
519 } else {
520 @unlink($itemPath);
521 }
522 }
523
524 @rmdir($directory);
525 }
526
527 /**
528 * @return void
529 */
530 private function deleteNetworkOptions()
531 {
532 delete_site_option('wpstg_license_key');
533 delete_site_option('wpstg_license_status');
534 delete_site_option('wpstgDisableLicenseNotice');
535 $this->cleanupEmptyOptions($this->preserveOptions, true);
536 }
537
538 /**
539 * @return string
540 */
541 private function getUploadsDirectory(): string
542 {
543 $uploadDir = wp_upload_dir();
544 return trailingslashit($uploadDir['basedir']);
545 }
546
547 /**
548 * @return string
549 */
550 private function getWpContentDirectory(): string
551 {
552 return trailingslashit(WP_CONTENT_DIR);
553 }
554
555 /**
556 * @param string $backupsDir
557 * @return bool
558 */
559 private function isDirectoryContainsWpstgFiles(string $backupsDir): bool
560 {
561 if (!is_dir($backupsDir)) {
562 return false;
563 }
564
565 $iterator = new \RecursiveIteratorIterator(
566 new \RecursiveDirectoryIterator($backupsDir, \FilesystemIterator::SKIP_DOTS)
567 );
568
569 foreach ($iterator as $item) {
570 if ($item->isFile() && strcasecmp($item->getExtension(), 'wpstg') === 0) {
571 return true;
572 }
573 }
574
575 return false;
576 }
577 }
578
579 new Uninstall();
580