PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backups, Restore, Migration & Clone / 4.10.0
WP STAGING – WordPress Backups, Restore, Migration & Clone v4.10.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 / uninstall.php
wp-staging Last commit date
Backend 4 days ago Backup 4 days ago Basic 1 week ago Component 1 month ago Core 4 days ago Framework 4 days ago Frontend 6 months ago Notifications 10 months ago Staging 4 days ago assets 4 days ago languages 4 days ago resources 1 year ago vendor_wpstg 4 days ago views 4 days ago CONTRIBUTING.md 2 years ago Deactivate.php 10 months ago README.md 5 months ago SECURITY.md 2 weeks ago autoloader.php 3 months ago bootstrap.php 2 weeks ago commonBootstrap.php 2 weeks ago constantsFree.php 4 days ago freeBootstrap.php 1 month ago install.php 4 days ago opcacheBootstrap.php 4 days ago readme.txt 4 days ago runtimeRequirements.php 4 months ago uninstall.php 4 days ago wp-staging-error-handler.php 8 months ago wp-staging.php 4 days ago
uninstall.php
625 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->deleteUserMeta($this->getBasicUserMeta());
118 $this->dropWpStagingSettingsTable();
119 $this->deleteTransients();
120 $this->cleanupEmptyPreserveOptions();
121 $this->clearCronEvents();
122 $this->cleanupWpStagingDirectories();
123 }
124
125 /**
126 * @return void
127 */
128 private function dropWpStagingSettingsTable()
129 {
130 global $wpdb;
131
132 if (!($wpdb instanceof \wpdb)) {
133 return;
134 }
135
136 $tableName = str_replace('`', '', $wpdb->prefix . 'wpstg_settings');
137 $wpdb->query("DROP TABLE IF EXISTS `{$tableName}`");
138 }
139
140 /**
141 * @return bool
142 */
143 private function isNetworkUninstall(): bool
144 {
145 return (is_multisite() && is_network_admin());
146 }
147
148 /**
149 * @return bool
150 */
151 private function isUninstallingBasic(): bool
152 {
153 $pluginDirs = ['wp-staging', 'wp-staging-1'];
154 return $this->isUninstallingPlugin($pluginDirs);
155 }
156
157 /**
158 * @return bool
159 */
160 private function isUninstallingPro(): bool
161 {
162 $pluginDirs = ['wp-staging-pro', 'wp-staging-pro-1'];
163 return $this->isUninstallingPlugin($pluginDirs);
164 }
165
166 /**
167 * @param array $pluginDirs
168 * @return bool
169 */
170 private function isUninstallingPlugin(array $pluginDirs): bool
171 {
172 return in_array(basename(__DIR__), $pluginDirs);
173 }
174
175 /**
176 * @return bool
177 */
178 private function isProInstalled(): bool
179 {
180 // First try header-based detection (more robust)
181 if ($this->isProInstalledByHeaders()) {
182 return true;
183 }
184
185 // Fallback to file-based detection for backward compatibility
186 $plugins = [
187 'wp-staging-pro-1/wp-staging-pro.php',
188 'wp-staging-pro/wp-staging-pro.php',
189 ];
190 foreach ($plugins as $plugin) {
191 if ($this->isPluginInstalled($plugin)) {
192 return true;
193 }
194 }
195
196 return false;
197 }
198
199 /**
200 * @return bool
201 */
202 private function isBasicInstalled(): bool
203 {
204 // First try header-based detection (more robust)
205 if ($this->isBasicInstalledByHeaders()) {
206 return true;
207 }
208
209 // Fallback to file-based detection for backward compatibility
210 $plugins = [
211 'wp-staging-1/wp-staging.php',
212 'wp-staging/wp-staging.php',
213 ];
214 foreach ($plugins as $plugin) {
215 if ($this->isPluginInstalled($plugin)) {
216 return true;
217 }
218 }
219
220 return false;
221 }
222
223 /**
224 * @param $pluginName
225 * @return bool
226 */
227 private function isPluginInstalled($pluginName): bool
228 {
229 return file_exists( WP_PLUGIN_DIR . '/' . $pluginName );
230 }
231
232 /**
233 * @param array $identifiers
234 * @return bool
235 */
236 private function findPluginByIdentifiers(array $identifiers): bool
237 {
238 if (!function_exists('get_plugins')) {
239 require_once ABSPATH . 'wp-admin/includes/plugin.php';
240 }
241
242 $plugins = get_plugins();
243 $searchCriteria = array_change_key_case($identifiers, CASE_LOWER);
244
245 foreach ($plugins as $file => $data) {
246 $name = strtolower($data['Name'] ?? '');
247 $slug = strtolower(dirname($file));
248 $mainFile = strtolower(basename($file, '.php'));
249
250 if (isset($searchCriteria['file']) && strtolower($searchCriteria['file']) === strtolower($file)) {
251 return true;
252 }
253
254 if (isset($searchCriteria['slug']) && ($slug === $searchCriteria['slug'] || $mainFile === $searchCriteria['slug'])) {
255 return true;
256 }
257
258 if (isset($searchCriteria['name']) && $name === strtolower($searchCriteria['name'])) {
259 return true;
260 }
261 }
262
263 return false;
264 }
265
266 /**
267 * @return bool
268 */
269 private function isBasicInstalledByHeaders(): bool
270 {
271 return $this->findPluginByIdentifiers([
272 'slug' => 'wp-staging',
273 'name' => 'WP Staging',
274 'file' => 'wp-staging/wp-staging.php',
275 ]);
276 }
277
278 /**
279 * @return bool
280 */
281 private function isProInstalledByHeaders(): bool
282 {
283 return $this->findPluginByIdentifiers([
284 'slug' => 'wp-staging-pro',
285 'name' => 'WP Staging Pro',
286 'file' => 'wp-staging-pro/wp-staging-pro.php',
287 ]);
288 }
289
290 /**
291 * @return array
292 */
293 private function getSettings(): array
294 {
295 return json_decode(json_encode(get_option('wpstg_settings', [])), true) ?? [];
296 }
297
298 /**
299 * @return string[]
300 */
301 private function getBasicOptions(): array
302 {
303 return [
304 'wpstg_settings',
305 'wpstg_clone_settings',
306 'wpstg_free_install_date',
307 'wpstg_installDate',
308 'wpstg_version',
309 'wpstg_version_upgraded_from',
310 'wpstg_free_upgrade_date',
311 'wpstg_rating',
312 'wpstg_rating_snooze_count',
313 'wpstg_unique_identifier',
314 'wpstg_is_staging_site',
315 'wpstg_resave_permalinks_executed',
316 'wpstg_rmpermalinks_executed',
317 'wpstg_connection',
318 'wpstg_staging_sites',
319 'wpstg_existing_clones',
320 'wpstg_existing_clones_beta',
321 'wpstg_execute',
322 'wpstg_emails_disabled',
323 'wpstg_woo_scheduler_disabled',
324 'wpstg_clone_excluded_files_list',
325 'wpstg_clone_excluded_gd_files_list',
326 'wpstg_freemius_notice',
327 'wpstg_queue_table_structure_version',
328 'wpstg_settings_table_version',
329 'wpstg_q_feature_detection_ajax_available',
330 'wpstg_analytics_has_consent',
331 'wpstg_analytics_modal_dismissed',
332 'wpstg_analytics_notice_dismissed',
333 'wpstg_analytics_consent_remind_me',
334 'wpstg_experiments',
335 'wpstg_first_install',
336 'wpstg_onboarding_completed',
337 'wpstg_onboarding_exposure',
338 'wpstg_onboarding_journey',
339 'wpstg_onboarding_queued_backup',
340 'wpstg_onboarding_restarted',
341 'wpstg_default_color_mode',
342 'wpstg_default_os_color_mode',
343 'wpstg_last_backup_info',
344 'wpstg_backups_retention',
345 'wpstg_otps',
346 'wpstg_access_token',
347 'wpstg_disabled_notice',
348 'wpstg_send_email_as_html',
349 'wpstg_cli_notice_hidden_forever',
350 'wpstg_cli_dock_cta_shown',
351 'wpstg_cli_notice_dismissed_until',
352 'wpstg_completed_upgrades',
353 'wpstg_next_gen_engine_notice',
354 'wpstg_staging_engine_preference',
355 'wpstg_staging_engine_preferences',
356 ];
357 }
358
359 /**
360 * Per-user meta keys written by the Free/shared code, removed for every user.
361 *
362 * @return string[]
363 */
364 private function getBasicUserMeta(): array
365 {
366 return [
367 'wpstg_user_general_pro_card_snoozed_until',
368 ];
369 }
370
371 /**
372 * @return string[]
373 */
374 private function getProOptions(): array
375 {
376 return [
377 'wpstgpro_version',
378 'wpstgpro_version_upgraded_from',
379 'wpstgpro_install_date',
380 'wpstgpro_upgrade_date',
381 'wpstg_license_key',
382 'wpstg_license_status',
383 'wpstg_pro_latest_version',
384 'wpstg_googledrive', //Legacy
385 'wpstg_google-drive',
386 'wpstg_dropbox',
387 'wpstg_one-drive',
388 'wpstg_pcloud',
389 'wpstg_amazons3', //Legacy
390 'wpstg_amazon-s3',
391 'wpstg_sftp',
392 'wpstg_digitalocean', //Legacy
393 'wpstg_digitalocean-spaces',
394 'wpstg_wasabi', //Legacy
395 'wpstg_wasabi-s3',
396 'wpstg_generic-s3',
397 'wpstg_backup_schedules',
398 'wpstg_backup_schedules_send_error_report',
399 'wpstg_backup_schedules_report_email',
400 'wpstg_backup_schedules_send_slack_error_report',
401 'wpstg_backup_schedules_report_slack_webhook',
402 'wpstg_current_site_login_links',
403 'wpstg_remote_sync_api_token',
404 'wpstg_remote_sync_password',
405 ];
406 }
407
408 /**
409 * @return string[]
410 */
411 private function getAllTransients(): array
412 {
413 return [
414 'wpstg_current_job',
415 'wpstg_rest_url',
416 'wpstg.run_daily',
417 'wpstg_show_login_notice',
418 'wpstg_user_logged_in_status',
419 'wpstg_auto_login_failed',
420 'wpstg_auto_login_failed_reason',
421 'wpstg_failed_auto_login_attempts',
422 'wpstg_otp_sent',
423 'wpstg_otp_consecutive_failures',
424 'wpstg_otp_locked',
425 'wpstg_redirect_url',
426 'wpstg_remote_sync_session',
427 'wpstg_remote_sync_session_data',
428 'wpstg_remote_sync_session_events_offset',
429 'wpstg.queue.request.get_method',
430 'is_invalid_backup_file_index',
431 'wpstg_permalinks_do_purge',
432 'wpstg_purge_litespeed_cache',
433 'wpstg_activation_redirect',
434 'wpstg_pro_activation_redirect',
435 'wpstg_weekly_version_update',
436 'wpstg_rate_limit_update_check',
437 'wpstg_issue_report_submitted',
438 'wpstg.backup.schedules.slack_report_sent',
439 'wpstg_email_notification_access_token',
440 'wpstg.directory_listing.last_checked',
441 'wpstg_push_size_cache',
442 ];
443 }
444
445 /**
446 * @param array $optionNames
447 * @return void
448 */
449 private function deleteOptions(array $optionNames)
450 {
451 foreach ($optionNames as $optionName) {
452 // Skip if this option should be preserved
453 if (in_array($optionName, $this->preserveOptions, true)) {
454 continue;
455 }
456
457 delete_option($optionName);
458 }
459 }
460
461 /**
462 * Delete the given user meta keys for every user on the site.
463 *
464 * @param string[] $metaKeys
465 * @return void
466 */
467 private function deleteUserMeta(array $metaKeys)
468 {
469 foreach ($metaKeys as $metaKey) {
470 delete_metadata('user', 0, $metaKey, '', true);
471 }
472 }
473
474 /**
475 * @return void
476 */
477 private function deleteTransients()
478 {
479 $transients = $this->getAllTransients();
480 foreach ($transients as $transientName) {
481 delete_transient($transientName);
482 }
483 }
484
485 /**
486 * @return void
487 */
488 private function cleanupEmptyPreserveOptions()
489 {
490 $this->cleanupEmptyOptions($this->preserveOptions);
491 }
492
493 /**
494 * @param array $options
495 * @param bool $isSiteOptions
496 * @return void
497 */
498 private function cleanupEmptyOptions(array $options, bool $isSiteOptions = false)
499 {
500 foreach ($options as $option) {
501 $value = $isSiteOptions ? get_site_option($option): get_option($option);
502 if (empty($value)) {
503 $isSiteOptions ? delete_site_option($option): delete_option($option);
504 }
505 }
506 }
507
508 /**
509 * @return void
510 */
511 private function clearCronEvents()
512 {
513 // @see WPStaging\Core\Cron\Cron::ACTION_WEEKLY_EVENT
514 wp_clear_scheduled_hook('wpstg_weekly_event');
515 }
516
517 /**
518 * @return void
519 */
520 private function cleanupWpStagingDirectories()
521 {
522 $uploadsBase = $this->getUploadsDirectory() . 'wp-staging/';
523 $directoriesToClean = [
524 $this->getWpContentDirectory() . 'wp-staging',
525 ];
526
527 // Delete wp-staging uploads dir if it does not contain .wpstg files
528 if (!$this->isDirectoryContainsWpstgFiles($uploadsBase . 'backups')) {
529 $directoriesToClean[] = $uploadsBase;
530 } else {
531 $directoriesToClean[] = $uploadsBase . 'cache';
532 $directoriesToClean[] = $uploadsBase . 'logs';
533 $directoriesToClean[] = $uploadsBase . 'tmp';
534 }
535
536 foreach ($directoriesToClean as $directory) {
537 $this->deleteDirectoryRecursively($directory);
538 }
539 }
540
541 /**
542 * @param string $directory
543 * @return void
544 */
545 private function deleteDirectoryRecursively(string $directory)
546 {
547 if (!is_dir($directory)) {
548 return;
549 }
550
551 $absPath = trailingslashit(ABSPATH);
552 if ($directory === $absPath || $directory === dirname($absPath)) {
553 return;
554 }
555
556 foreach (new \DirectoryIterator($directory) as $item) {
557 if ($item->isDot()) {
558 continue;
559 }
560
561 $itemPath = $item->getPathname();
562 if ($item->isDir()) {
563 $this->deleteDirectoryRecursively($itemPath);
564 } else {
565 @unlink($itemPath);
566 }
567 }
568
569 @rmdir($directory);
570 }
571
572 /**
573 * @return void
574 */
575 private function deleteNetworkOptions()
576 {
577 delete_site_option('wpstg_license_key');
578 delete_site_option('wpstg_license_status');
579 delete_site_option('wpstgDisableLicenseNotice');
580 $this->cleanupEmptyOptions($this->preserveOptions, true);
581 }
582
583 /**
584 * @return string
585 */
586 private function getUploadsDirectory(): string
587 {
588 $uploadDir = wp_upload_dir();
589 return trailingslashit($uploadDir['basedir']);
590 }
591
592 /**
593 * @return string
594 */
595 private function getWpContentDirectory(): string
596 {
597 return trailingslashit(WP_CONTENT_DIR);
598 }
599
600 /**
601 * @param string $backupsDir
602 * @return bool
603 */
604 private function isDirectoryContainsWpstgFiles(string $backupsDir): bool
605 {
606 if (!is_dir($backupsDir)) {
607 return false;
608 }
609
610 $iterator = new \RecursiveIteratorIterator(
611 new \RecursiveDirectoryIterator($backupsDir, \FilesystemIterator::SKIP_DOTS)
612 );
613
614 foreach ($iterator as $item) {
615 if ($item->isFile() && strcasecmp($item->getExtension(), 'wpstg') === 0) {
616 return true;
617 }
618 }
619
620 return false;
621 }
622 }
623
624 new Uninstall();
625