settings = $settings; } /** * Uninstall hook stored in `uninstall_plugins`, so it runs however the plugin is deleted. * * The SDK's own hook needs a logged-in user who can activate plugins and is stored only when an administrator * deactivates the plugin in wp-admin, so WP-CLI (no user, no wp-admin) would never purge. With such a user the SDK * is handed the job, so its account clean-up runs and ends in `after_uninstall`; without one the purge runs here. */ public static function handle_uninstall(): void { if ( current_user_can( 'activate_plugins' ) && class_exists( Freemius::class ) ) { Freemius::_uninstall_plugin_hook(); return; } self::run_for_current_uninstall(); } /** * Entry point for the SDK's `after_uninstall` action (see consentfold.php). * * Does nothing while another package of the plugin is still installed: both share one set of * settings and tables, so purging for one would break the other. */ public static function run_for_current_uninstall(): void { $plugin_dir = self::uninstalling_plugin_dir(); if ( self::another_copy_installed( $plugin_dir ) ) { return; } self::register_autoloader_for( $plugin_dir ); ( new self( new Repository() ) )->run(); } /** * Directory of the package core is uninstalling, with a trailing slash. * * Read from the `uninstall_{plugin}` action the SDK runs inside rather than from * CONSENTFOLD_DIR: when both packages are deleted in one process, the second one's main file * stops at the coexistence guard, so the constant still names the first, already deleted, package. */ public static function uninstalling_plugin_dir(): string { foreach ( array_reverse( (array) $GLOBALS['wp_current_filter'] ) as $hook ) { if ( str_starts_with( (string) $hook, 'uninstall_' ) ) { return trailingslashit( WP_PLUGIN_DIR ) . trailingslashit( dirname( substr( (string) $hook, strlen( 'uninstall_' ) ) ) ); } } return CONSENTFOLD_DIR; } /** * Whether another directory next to $plugin_dir holds a copy of the plugin's main file. * * A bare directory is no proof (WordPress can leave an emptied one behind), so the main file is * looked for. An unreadable plugins directory counts as "installed": a skipped purge leaves data * behind, a wrong one deletes data the other package needs. * * @param string $plugin_dir Directory of the package being uninstalled. */ public static function another_copy_installed( string $plugin_dir ): bool { $own = untrailingslashit( $plugin_dir ); $entries = @scandir( dirname( $own ) ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- An unreadable directory is handled as "cannot tell" below. if ( false === $entries ) { return true; } foreach ( $entries as $entry ) { $directory = dirname( $own ) . '/' . $entry; if ( '.' !== $entry && '..' !== $entry && $directory !== $own && file_exists( $directory . '/consentfold.php' ) ) { return true; } } return false; } /** * Makes the uninstalled package's own classes resolve from files that are still on disk. * * With both packages deleted in one process the autoloader of the first one points at deleted * files, so a class not loaded yet would fatal. A second loader for this package, prepended ahead * of the stale one, avoids that. * * @param string $plugin_dir Directory of the package being uninstalled. */ private static function register_autoloader_for( string $plugin_dir ): void { if ( CONSENTFOLD_DIR === $plugin_dir || ! class_exists( ClassLoader::class ) ) { return; } $loader = new ClassLoader(); $loader->addPsr4( 'Consentfold\\', $plugin_dir . 'src' ); $loader->register( true ); } /** * Uninstall from the current site, or from every site of the network. */ public function run(): void { if ( ! is_multisite() ) { $this->uninstall_site(); return; } $kept = false; foreach ( get_sites( array( 'fields' => 'ids', 'number' => 0, ) ) as $site_id ) { switch_to_blog( (int) $site_id ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.switch_to_blog_switch_to_blog -- Uninstall visits every site. try { $kept = ! $this->uninstall_site() || $kept; } finally { restore_current_blog(); } } if ( ! $kept ) { delete_site_option( Repository::NETWORK_OPTION ); } } /** * Remove the current site's data when its settings ask for it. * * @return bool Whether the data was removed. */ private function uninstall_site(): bool { if ( true !== ( $this->settings->get()['privacy']['delete_on_uninstall'] ?? false ) ) { return false; } Schema::drop(); delete_option( Repository::OPTION ); delete_option( ProxyNotice::OPTION ); delete_option( Health::OPTION ); delete_option( StaticScanner::STATE_OPTION ); delete_option( StaticScanner::RESULTS_OPTION ); delete_option( StaticScanner::CRASH_OPTION ); delete_option( EnqueueDump::OPTION ); delete_option( RuntimeCollector::OPTION ); delete_option( Inventory::OPTION ); delete_option( Inventory::IGNORED_OPTION ); delete_option( ImportService::SNAPSHOT_OPTION ); delete_option( SetupRoute::OPTION ); $this->delete_extension_options(); $this->delete_extension_files(); HostedFonts::remove_all(); delete_transient( Notices::TRANSIENT ); delete_transient( ConfigBuilder::TRANSIENT ); delete_transient( ServiceDetails::TRANSIENT ); wp_unschedule_hook( Activator::PRUNE_HOOK ); wp_unschedule_hook( ScanCron::SCAN_HOOK ); wp_unschedule_hook( ScanCron::CHUNK_HOOK ); Activator::unschedule_extension_events(); delete_metadata( 'user', 0, UserConsentIds::meta_key(), '', true ); delete_metadata( 'user', 0, Notices::meta_key(), '', true ); delete_metadata( 'user', 0, Preferences::META_KEY, '', true ); return true; } /** * Remove the options add-on code stored with the `consentfold_ext_` prefix. They are found by name, so the * purge needs no add-on code to be loaded. */ private function delete_extension_options(): void { global $wpdb; $names = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Uninstall, the names are what is being looked up. $wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like( 'consentfold_ext_' ) . '%' ) ); foreach ( $names as $name ) { delete_option( (string) $name ); } } /** * Remove the folders add-on code keeps in the current site's uploads under `consentfold/ext-*`. They are * found by name, like the options. */ private function delete_extension_files(): void { global $wp_filesystem; require_once ABSPATH . 'wp-admin/includes/file.php'; if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) { WP_Filesystem(); } if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) { return; } $root = untrailingslashit( wp_upload_dir()['basedir'] ) . '/consentfold'; foreach ( (array) $wp_filesystem->dirlist( $root ) as $name => $entry ) { if ( str_starts_with( (string) $name, 'ext-' ) ) { $wp_filesystem->delete( $root . '/' . $name, true ); } } } }