*/ private const CREDENTIAL_OPTIONS = [ StudioSettings::OPT_PUBLISHABLE, StudioSettings::OPT_SECRET, StudioSettings::OPT_WEBHOOK_SECRET, StudioSettings::OPT_MODE, ]; /** * Every remaining option the plugin writes. Removed only on a full purge. * * @var list */ private const SETTING_OPTIONS = [ 'us_schedular_version', 'us_questions_offering_nullable', 'us_questions_child_required_backfilled', StudioSettings::OPT_CURRENCY, StudioSettings::OPT_ETRANSFER_EMAIL, StudioSettings::OPT_HST_RATE, StudioSettings::OPT_DEFAULT_PAYMENT_METHOD, StudioSettings::OPT_CANCELLATION_CUTOFF_HOURS, StudioSettings::OPT_REGISTRATION_MODE, RegistrationController::OPTION_PAGE, AccessSettings::OPT_GRANT_STUDIO, AccessSettings::OPT_GRANT_INSTRUCTOR, self::OPT_DELETE_DATA, ]; /** * Every user meta key the plugin writes. Removed for all users on a full * purge, so no student is left carrying a billing override or a half-finished * signup for a plugin that is gone. * * @var list */ private const USER_META = [ GuardianService::META_CHILD, GuardianService::META_BIRTH_YEAR, GuardianService::META_DOB, GuardianService::META_GUARDIAN_ONLY, BillingMethodResolver::META_METHOD, RegistrationStatus::META_AWAITING_APPROVAL, RegistrationStatus::META_EMAIL_CONFIRMED, RegistrationStatus::META_CONFIRM_TOKEN, RegistrationStatus::META_CONFIRM_EXPIRES, RegistrationStatus::META_AUTO_APPROVE, ]; /** * Whether a full purge has been asked for. */ public static function deletesDataOnUninstall(): bool { return '1' === Val::string( get_option( self::OPT_DELETE_DATA, '0' ) ); } /** * Record the site owner's answer. */ public static function setDeletesDataOnUninstall( bool $delete ): void { update_option( self::OPT_DELETE_DATA, $delete ? '1' : '0' ); } /** * Run the uninstall. Called from `uninstall.php`, which WordPress loads in * isolation once the plugin has been deleted. */ public function run(): void { $this->forgetCredentials(); $this->restoreCoreRegistrationSettings(); // The event is cleared on deactivation too, which always precedes a // delete — repeated here because a site can be left with a stale schedule // if the plugin files went away without deactivating cleanly. wp_clear_scheduled_hook( ScheduledBillingRunner::HOOK ); if ( ! self::deletesDataOnUninstall() ) { return; } $this->dropTables(); $this->deleteSettings(); $this->deleteUserMeta(); $this->removeRoles(); } /** * Forget the Stripe credentials, always. {@see StudioSettings::clearStripeConfig()} * is the same act from the settings page; it is not reused here because that * object belongs to a plugin that, by this point, is no longer loaded as one. */ private function forgetCredentials(): void { foreach ( self::CREDENTIAL_OPTIONS as $option ) { delete_option( $option ); } delete_transient( UpdateChecker::TRANSIENT ); } /** * Put back the two core options open registration borrowed, from the snapshot * taken when it was switched on. Without this, deleting the plugin while open * registration is enabled leaves the site accepting public signups into a role * that is about to stop existing. * * Only acts when a snapshot exists, so a site that never enabled open * registration keeps its own settings untouched. */ private function restoreCoreRegistrationSettings(): void { $snapshot = get_option( StudioSettings::OPT_PREV_USERS_CAN_REGISTER, null ); if ( null === $snapshot ) { return; } $prevRole = Val::string( get_option( StudioSettings::OPT_PREV_DEFAULT_ROLE, 'subscriber' ) ); update_option( 'users_can_register', '1' === Val::string( $snapshot ) ? '1' : '0' ); update_option( 'default_role', '' !== $prevRole ? $prevRole : 'subscriber' ); delete_option( StudioSettings::OPT_PREV_USERS_CAN_REGISTER ); delete_option( StudioSettings::OPT_PREV_DEFAULT_ROLE ); } private function dropTables(): void { global $wpdb; if ( ! $wpdb instanceof \wpdb ) { return; } foreach ( Schema::TABLES as $table ) { $sql = $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $wpdb->prefix . $table ); if ( null !== $sql ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange -- uninstall drops the plugin's own tables; the names come from Schema::TABLES, not from input. $wpdb->query( $sql ); } } } private function deleteSettings(): void { foreach ( self::SETTING_OPTIONS as $option ) { delete_option( $option ); } } private function deleteUserMeta(): void { foreach ( self::USER_META as $key ) { delete_metadata( 'user', 0, $key, '', true ); } } /** * Drop the three roles the plugin adds. Only on a full purge: a site keeping * its data is keeping its students too, and a student whose role has been * deleted out from under them is a user with no capabilities at all until the * plugin is reinstalled. */ private function removeRoles(): void { foreach ( [ RoleManager::STUDIO_ADMIN, RoleManager::INSTRUCTOR, RoleManager::STUDENT ] as $role ) { remove_role( $role ); } } }