prefix . 'accessibility_audio_tts_listening_stats', ); // Remove tables safely. foreach ( $aat_tables_to_remove as $aat_table ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin uninstall maintenance check with prepared query. $aat_table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $aat_table ) ); if ( $aat_table_exists ) { // Validate table name is from our whitelist before dropping. $aat_table_name = esc_sql( $aat_table ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Plugin uninstall cleanup; validated and escaped table name. $wpdb->query( "DROP TABLE IF EXISTS `{$aat_table_name}`" ); } } /** * ======================================== * CLEAN UP UPLOADED FILES * ======================================== */ // Only remove files if user explicitly wants to (could be an option). $aat_remove_files = get_option( 'accessibility_audio_tts_remove_files_on_uninstall', false ); if ( $aat_remove_files ) { $aat_upload_dir = wp_upload_dir(); // Only remove our specific directories. $aat_plugin_upload_dirs = array( $aat_upload_dir['basedir'] . '/accessibility-audio-tts/', ); foreach ( $aat_plugin_upload_dirs as $aat_dir ) { if ( is_dir( $aat_dir ) && false !== strpos( $aat_dir, 'accessibility-audio-tts-text-to-speech-for-articles' ) ) { accessibility_audio_tts_remove_directory( $aat_dir ); } } } /** * ======================================== * REMOVE SCHEDULED EVENTS * ======================================== */ // Define all plugin cron hooks. $aat_hooks_to_clear = array( 'accessibility_audio_tts_process_queue', 'accessibility_audio_tts_process_background_posts', 'accessibility_audio_tts_auto_trigger_cron', 'accessibility_audio_tts_security_cleanup', ); // Clear all scheduled events for each hook. foreach ( $aat_hooks_to_clear as $aat_hook ) { // Clear all instances of this hook. wp_clear_scheduled_hook( $aat_hook ); // Also clear any scheduled events with arguments. $aat_scheduled_events = wp_get_scheduled_event( $aat_hook ); if ( $aat_scheduled_events ) { wp_unschedule_event( $aat_scheduled_events->timestamp, $aat_hook, $aat_scheduled_events->args ); } } // Clear any cached data. wp_cache_flush(); /** * ======================================== * FINAL CLEANUP AND LOGGING * ======================================== */ // Clear any remaining cached data. if ( function_exists( 'wp_cache_flush' ) ) { wp_cache_flush(); } // Remove any remaining transients. delete_transient( 'accessibility_audio_tts_cache' ); // Log uninstall completion (only if WP_DEBUG is enabled). if ( defined( 'WP_DEBUG' ) && WP_DEBUG && function_exists( 'accessibility_audio_tts_error_log' ) ) { accessibility_audio_tts_error_log( sprintf( 'Accessibility Audio TTS v%s: Uninstall completed successfully at %s', defined( 'ACCESSIBILITY_AUDIO_TTS_VERSION' ) ? ACCESSIBILITY_AUDIO_TTS_VERSION : 'unknown', current_time( 'mysql' ) ) ); } } /** * Recursively remove directory and all contents. * * @param string $dir Directory path to remove. * @return bool True on success, false on failure. * @since 1.0.0 */ function accessibility_audio_tts_remove_directory( $dir ) { // Validate input. if ( empty( $dir ) || ! is_string( $dir ) ) { return false; } // Check if directory exists. if ( ! is_dir( $dir ) ) { return false; } // Security check - ensure we're only removing our directories. if ( false === strpos( $dir, 'accessibility-audio-tts-audio' ) ) { return false; } // Get directory contents. $files = scandir( $dir ); if ( false === $files ) { return false; } $files = array_diff( $files, array( '.', '..' ) ); // Remove all files and subdirectories. foreach ( $files as $file ) { $path = $dir . DIRECTORY_SEPARATOR . $file; if ( is_dir( $path ) ) { // Recursively remove subdirectory. if ( ! accessibility_audio_tts_remove_directory( $path ) ) { return false; } continue; } // Remove file. if ( ! wp_delete_file( $path ) ) { return false; } } // Remove the directory itself using WP_Filesystem if available. global $wp_filesystem; if ( ! $wp_filesystem ) { require_once ABSPATH . 'wp-admin/includes/file.php'; WP_Filesystem(); } if ( $wp_filesystem && method_exists( $wp_filesystem, 'rmdir' ) ) { return (bool) $wp_filesystem->rmdir( $dir, true ); } // WP_Filesystem not available; do not use direct PHP rmdir per WPCS. return false; } // Execute uninstall. accessibility_audio_tts_uninstall();