query( $wpdb->prepare( 'DELETE FROM %i WHERE option_name LIKE %s', $wpdb->options, $wpdb->esc_like( $kind . $prefix ) . '%' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- uninstall only. } } foreach ( self::option_prefixes() as $prefix ) { $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE option_name LIKE %s', $wpdb->options, $wpdb->esc_like( $prefix ) . '%' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- uninstall only. } wp_cache_delete( 'alloptions', 'options' ); } public static function forget_caches(): int { global $wpdb; $table = Schema::table( 'orgs' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- uninstall only. if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table ) ) ) !== $table ) { return 0; } $ids = array_map( 'intval', (array) $wpdb->get_col( $wpdb->prepare( 'SELECT id FROM %i ORDER BY id', $table ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- uninstall only. \RoosterLMS\Orgs\OrgUnitRepository::forget_salts( $ids ); return count( $ids ); } public static function options(): array { return array( Settings::OPTION, Migrator::VERSION_OPTION, Migrator::FLUSH_OPTION, Migrator::RULES_OPTION, Migrator::AQ_COUNTS_OPTION, 'rooster_lms_migration_lock', 'rooster_lms_h5p_state', 'rooster_lms_h5p_widgets', 'rooster_lms_demo', 'rooster_lms_h5p_verify', \RoosterLMS\H5P\QuestionTexts::OPTION, \RoosterLMS\Admin\Questions\QuestionImport::UNDO_OPTION, \RoosterLMS\Assessment\Realign::OPTION, Transaction::RECOUNT_OPTION, \RoosterLMS\Content\CourseCatalogue::COUNTS_OPTION, \RoosterLMS\Privacy\UserCleanup::LAST_OPTION, \RoosterLMS\Import\MasterStudy\QuizUuid::SECRET_OPTION, \RoosterLMS\Blocks\LiveData::UNITS_OPTION, \RoosterLMS\Live\Providers\ZoomCredentials::OPTION, \RoosterLMS\Live\LiveModule::BACKFILL_OPTION, \RoosterLMS\Live\EndedSweep::MARK_OPTION, \RoosterLMS\Certificates\AttendanceCertificates::FLAG_OPTION, \RoosterLMS\Account\RoosterAccount::OPTION, \RoosterLMS\UI\PageCaches::OPTION, \RoosterLMS\Account\Signup::OPTION, \RoosterLMS\I18n\LanguagePacks::OPTION, \RoosterLMS\Account\PortalClient::SECRET_OPTION, \RoosterLMS\Account\PortalClient::OUTBOX_OPTION, \RoosterLMS\Account\PortalClient::ENROL_ERROR_OPTION, \RoosterLMS\S1b\Catalogue::OPTION, \RoosterLMS\S1b\PortalPlan::ALERTS, \RoosterLMS\Registration\Front\RateLimiter::OPTION, \RoosterLMS\Messages\TemplateRepository::INDEX_OPTION, \RoosterLMS\Messages\Seeder::OPTION, \RoosterLMS\Messages\Events::DAILY_OPTION, \RoosterLMS\Invitations\RsvpRate::OPTION, \RoosterLMS\Invitations\InvitationService::USED_OPTION, \RoosterLMS\Events\History::OPTION, \RoosterLMS\Events\Facets::OPTION, \RoosterLMS\Events\FeedRate::OPTION, \RoosterLMS\Commerce\WooCommerce\PlanPrice::CACHE_OPTION, \RoosterLMS\Orgs\OfferingService::USED_OPTION, \RoosterLMS\Orgs\OrgService::MULTI_ORG_OPTION, \RoosterLMS\Orgs\MemberService::MULTI_OPTION, \RoosterLMS\Content\CourseStaffService::STAFF_USERS_OPTION, \RoosterLMS\I18n\Strings::MIGRATED_OPTION, \RoosterLMS\Import\ERoom\Tags::OPTION, \RoosterLMS\Persons\LinkedTargets::OPTION, \RoosterLMS\Persons\LegacyRedirects::IMPORTED_OPTION, \RoosterLMS\Community\CommunitySettings::OPTION, \RoosterLMS\Media\MediaSettings::OPTION, \RoosterLMS\Learn\LessonButtons::OPTION, \RoosterLMS\Enrolment\LaunchGrants::MARK_OPTION, \RoosterLMS\Import\Lms\RunEvents::FROM_OPTION, \RoosterLMS\Orgs\Registration\FieldValues::ORGS_OPTION, Migrator::REGF_COUNTS_OPTION, \RoosterLMS\Orgs\MemberService::UNIQUE_OPTION, ); } public static function delete_protected_files(): bool { $uploads = wp_upload_dir( null, false ); $base = untrailingslashit( wp_normalize_path( (string) $uploads['basedir'] ) ); $root = wp_normalize_path( dirname( \RoosterLMS\Live\RecordingStore::dir() ) ); if ( '' === $base || $root !== $base . '/rooster-lms-protected' || is_link( $root ) ) { return false; } if ( ! is_dir( $root ) ) { return true; } $items = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $root, \FilesystemIterator::SKIP_DOTS ), \RecursiveIteratorIterator::CHILD_FIRST ); foreach ( $items as $item ) { $path = $item->getPathname(); if ( $item->isDir() && ! $item->isLink() ) { Files::remove_dir( $path ); } else { wp_delete_file( $path ); } } Files::remove_dir( $root ); return ! is_dir( $root ); } public static function restore_adopted(): int { global $wpdb; $table = Schema::table( 'import_map' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- uninstall only. if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table ) ) ) !== $table ) { return 0; } $types = array( \RoosterLMS\Import\MasterStudy\Schema::COURSE => array( PostTypes::COURSE, \RoosterLMS\Import\MasterStudy\Steps\CoursesStep::taxonomies() ), \RoosterLMS\Import\MasterStudy\Schema::LESSON => array( PostTypes::LESSON, array() ), \RoosterLMS\Import\MasterStudy\Schema::QUIZ => array( PostTypes::QUIZ, array() ), ); $done = 0; $after = array( '', '', '' ); do { // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- uninstall only; keyset paging on the primary key. $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT source_lms, source_type, source_key, object_type, object_id, prev_json FROM %i WHERE mode = %d AND object_type IN (%d, %d) AND (source_lms, source_type, source_key) > (%s, %s, %s) ORDER BY source_lms, source_type, source_key LIMIT 200', $table, \RoosterLMS\Import\LedgerMode::ADOPTED->value, \RoosterLMS\Import\ImportObject::POST->value, \RoosterLMS\Import\ImportObject::TERM->value, $after[0], $after[1], $after[2] ), ARRAY_A ); foreach ( (array) $rows as $row ) { $after = array( (string) $row['source_lms'], (string) $row['source_type'], (string) $row['source_key'] ); $prev = json_decode( (string) $row['prev_json'], true ); $id = (int) $row['object_id']; if ( ! is_array( $prev ) || $id <= 0 ) { continue; } if ( \RoosterLMS\Import\ImportObject::TERM->value === (int) $row['object_type'] ) { \RoosterLMS\Import\MasterStudy\Adopter::restore_term( $id, $prev, Taxonomies::COURSE_CATEGORY ); ++$done; continue; } $type = $types[ (string) ( $prev['post_type'] ?? '' ) ] ?? null; $now = get_post_type( $id ); if ( null !== $type && $now === $type[0] ) { \RoosterLMS\Import\MasterStudy\Adopter::restore_post( $id, $prev, $type[0], $type[1] ); ++$done; } } $full = is_array( $rows ) && 200 === count( $rows ); } while ( $full ); return $done; } public static function release_roles(): int { $ours = \RoosterLMS\Orgs\Roles::CREATED; $fallback = (string) get_option( 'default_role', 'subscriber' ); if ( '' === $fallback || in_array( $fallback, $ours, true ) || null === get_role( $fallback ) ) { $fallback = 'subscriber'; } $ids = get_users( array( 'role__in' => $ours, 'fields' => 'ID', 'number' => -1, 'orderby' => 'ID', ) ); $done = 0; foreach ( array_chunk( array_map( 'intval', (array) $ids ), 500 ) as $chunk ) { cache_users( $chunk ); foreach ( $chunk as $user_id ) { $user = get_userdata( $user_id ); if ( ! $user instanceof \WP_User ) { continue; } foreach ( $ours as $role ) { if ( in_array( $role, (array) $user->roles, true ) ) { $user->remove_role( $role ); } } if ( array() === array_values( (array) $user->roles ) ) { $user->add_role( $fallback ); } ++$done; } } foreach ( $ours as $role ) { remove_role( $role ); } return $done; } public static function delete_scheduled_actions(): void { global $wpdb; $groups = $wpdb->prefix . 'actionscheduler_groups'; $actions = $wpdb->prefix . 'actionscheduler_actions'; $logs = $wpdb->prefix . 'actionscheduler_logs'; foreach ( array( $groups, $actions, $logs ) as $table ) { if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table ) ) ) !== $table ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- uninstall only. if ( function_exists( 'as_unschedule_all_actions' ) ) { as_unschedule_all_actions( '', array(), 'rooster-lms' ); } return; } } $group_id = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT group_id FROM %i WHERE slug = %s', $groups, 'rooster-lms' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- uninstall only. if ( $group_id <= 0 ) { return; } $wpdb->query( $wpdb->prepare( 'DELETE l FROM %i l JOIN %i a ON a.action_id = l.action_id WHERE a.group_id = %d', $logs, $actions, $group_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- uninstall only. $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE group_id = %d', $actions, $group_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- uninstall only. $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE group_id = %d', $groups, $group_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- uninstall only. } public static function delete_posts(): void { $statuses = array_merge( array( 'any' ), array_values( get_post_stati( array( 'exclude_from_search' => true ) ) ) ); foreach ( array_merge( PostTypes::all(), array( PostTypes::CERTIFICATE_TPL, \RoosterLMS\Plans\PlansModule::POST_TYPE, \RoosterLMS\Persons\PersonsModule::POST_TYPE ) ) as $post_type ) { $refused = 0; do { $ids = get_posts( array( 'post_type' => $post_type, 'post_status' => $statuses, 'fields' => 'ids', 'numberposts' => self::BATCH, 'offset' => $refused, 'orderby' => 'ID', 'order' => 'ASC', ) ); foreach ( $ids as $id ) { if ( ! wp_delete_post( (int) $id, true ) ) { ++$refused; } } } while ( array() !== $ids ); } } public static function delete_terms(): void { global $wpdb; ( new Taxonomies() )->register_taxonomies(); $provider = Providers::current(); foreach ( Taxonomies::all() as $taxonomy ) { $terms = $wpdb->get_col( $wpdb->prepare( 'SELECT term_id FROM %i WHERE taxonomy = %s ORDER BY term_id', $wpdb->term_taxonomy, $taxonomy ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- uninstall only; a raw read so no translation plugin filters it. foreach ( (array) $terms as $term_id ) { $term_id = (int) $term_id; Switcher::with( $provider->term_language( $term_id ), static fn() => wp_delete_term( $term_id, $taxonomy ) ); } self::purge_terms( $taxonomy ); } } private static function purge_terms( string $taxonomy ): void { global $wpdb; // phpcs:disable WordPress.DB.DirectDatabaseQuery -- uninstall only. $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT term_id, term_taxonomy_id FROM %i WHERE taxonomy = %s', $wpdb->term_taxonomy, $taxonomy ), ARRAY_A ); if ( ! is_array( $rows ) || array() === $rows ) { return; } foreach ( array_chunk( $rows, 500 ) as $chunk ) { $tt_ids = array_map( static fn( array $row ): int => (int) $row['term_taxonomy_id'], $chunk ); $term_ids = array_map( static fn( array $row ): int => (int) $row['term_id'], $chunk ); foreach ( array( $wpdb->term_relationships, $wpdb->term_taxonomy ) as $table ) { Db::query( SqlPart::of( 'DELETE FROM %i WHERE term_taxonomy_id IN (', $table )->add( SqlPart::in_list( $tt_ids ) )->add( ')' ) ); } foreach ( array( $wpdb->termmeta, $wpdb->terms ) as $table ) { Db::query( SqlPart::of( 'DELETE FROM %i WHERE term_id IN (', $table )->add( SqlPart::in_list( $term_ids ) )->add( ') AND term_id NOT IN ( SELECT term_id FROM %i )', $wpdb->term_taxonomy ) ); } } // phpcs:enable clean_taxonomy_cache( $taxonomy ); } }