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