Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/quiz-master-next/mlw_quizmaster2.php
+++ b/quiz-master-next/mlw_quizmaster2.php
@@ -2,7 +2,7 @@
/**
* Plugin Name: Quiz And Survey Master
* Description: Easily and quickly add quizzes and surveys to your website.
- * Version: 10.3.5
+ * Version: 11.0.0
* Author: ExpressTech
* Author URI: https://quizandsurveymaster.com/
* Plugin URI: https://expresstech.io/
@@ -43,7 +43,7 @@
* @var string
* @since 4.0.0
*/
- public $version = '10.3.5';
+ public $version = '11.0.0';
/**
* QSM Alert Manager Object
@@ -284,6 +284,7 @@
include_once 'php/admin/admin-results-page.php';
include_once 'php/admin/admin-results-details-page.php';
include_once 'php/admin/tools-page.php';
+ include_once 'php/admin/question-bank-page.php';
include_once 'php/classes/class-qsm-changelog-generator.php';
include_once 'php/admin/about-page.php';
include_once 'php/admin/dashboard-widgets.php';
@@ -306,6 +307,10 @@
include_once 'php/classes/class-qsm-emails.php';
include_once 'php/classes/class-qmn-quiz-manager.php';
+ // Load new rendering system files
+ include_once 'renderer/frontend/template-loader.php';
+ include_once 'renderer/frontend/class-qsm-render-pagination.php';
+ include_once 'renderer/frontend/class-qsm-new-renderer.php';
include_once 'php/template-variables.php';
include_once 'php/adverts-generate.php';
include_once 'php/question-types.php';
@@ -359,6 +364,48 @@
add_action( 'admin_init', array( $this, 'qsm_overide_old_setting_options' ) );
add_action( 'admin_notices', array( $this, 'qsm_admin_notices' ) );
add_filter( 'manage_edit-qsm_category_columns', array( $this, 'modify_qsm_category_columns' ) );
+ add_action( 'wp_ajax_qsm_mark_setup_wizard_completed', array( $this, 'qsm_mark_setup_wizard_completed' ) );
+ add_action( 'wp_ajax_qsm_reset_setup_wizard_completed', array( $this, 'qsm_reset_setup_wizard_completed' ) );
+ }
+
+ /**
+ * Marks setup wizard as completed for current user.
+ *
+ * @since 0.0.0
+ * @return void
+ */
+ public function qsm_mark_setup_wizard_completed() {
+ if ( ! function_exists( 'is_admin' ) || ! is_admin() || ! current_user_can( 'edit_posts' ) ) {
+ wp_send_json_error(
+ array(
+ 'message' => __( 'Unauthorized!', 'quiz-master-next' ),
+ )
+ );
+ }
+ check_ajax_referer( 'qsm_setup_wizard_nonce', 'nonce' );
+ $user_id = get_current_user_id();
+ update_user_meta( $user_id, 'qsm_setup_wizard_completed', 1 );
+ wp_send_json_success( array( 'completed' => 1 ) );
+ }
+
+ /**
+ * Resets setup wizard completion for current user.
+ *
+ * @since 0.0.0
+ * @return void
+ */
+ public function qsm_reset_setup_wizard_completed() {
+ if ( ! function_exists( 'is_admin' ) || ! is_admin() || ! current_user_can( 'edit_posts' ) ) {
+ wp_send_json_error(
+ array(
+ 'message' => __( 'Unauthorized!', 'quiz-master-next' ),
+ )
+ );
+ }
+ check_ajax_referer( 'qsm_setup_wizard_nonce', 'nonce' );
+ $user_id = get_current_user_id();
+ delete_user_meta( $user_id, 'qsm_setup_wizard_completed' );
+ wp_send_json_success( array( 'completed' => 0 ) );
}
/**
@@ -437,7 +484,7 @@
wp_enqueue_script( 'ChartJS', QSM_PLUGIN_JS_URL . '/chart.min.js', array(), '3.6.0', true );
}
// quiz option pages
- if ( 'admin_page_mlw_quiz_options' === $hook ) {
+ if ( 'admin_page_mlw_quiz_options' === $hook || 'qsm_page_qmn_global_settings' === $hook ) {
wp_enqueue_script( 'wp-tinymce' );
wp_enqueue_script( 'micromodal_script', plugins_url( 'js/micromodal.min.js', __FILE__ ), array( 'jquery', 'qsm_admin_js' ), $this->version, true );
$current_tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : 'questions';
@@ -451,6 +498,16 @@
wp_add_inline_script( 'math_jax', self::$default_MathJax_script, 'before' );
wp_enqueue_editor();
wp_enqueue_media();
+ wp_enqueue_style( 'wp-pointer' );
+ wp_enqueue_script( 'wp-pointer' );
+ wp_enqueue_script( 'qsm_admin_tour_js', plugins_url( 'js/qsm-admin-tour.js', __FILE__ ), array( 'jquery', 'wp-pointer' ), $this->version, true );
+ wp_localize_script(
+ 'qsm_admin_tour_js',
+ 'qsmAdminTourData',
+ array(
+ 'quiz_id' => isset( $_GET['quiz_id'] ) ? intval( $_GET['quiz_id'] ) : 0,
+ )
+ );
break;
case 'style':
wp_enqueue_style( 'wp-color-picker' );
@@ -473,6 +530,7 @@
break;
case 'results-pages':
case 'emails':
+ case 'quiz-default-template':
wp_enqueue_script( 'select2-js', QSM_PLUGIN_JS_URL.'/jquery.select2.min.js', array( 'jquery' ), $this->version,true);
wp_enqueue_style( 'select2-css', QSM_PLUGIN_CSS_URL . '/jquery.select2.min.css', array(), $this->version );
wp_enqueue_editor();
@@ -484,15 +542,35 @@
break;
}
}
+
+ if ( ! wp_script_is( 'select2-js', 'registered' ) ) {
+ wp_register_script( 'select2-js', QSM_PLUGIN_JS_URL . '/jquery.select2.min.js', array( 'jquery' ), $this->version, true );
+ }
+ if ( ! wp_style_is( 'select2-css', 'registered' ) ) {
+ wp_register_style( 'select2-css', QSM_PLUGIN_CSS_URL . '/jquery.select2.min.css', array(), $this->version );
+ }
// load admin JS after all dependencies are loaded
/** Fixed wpApiSettings is not defined js error by using 'wp-api-request' core script to allow the use of localized version of wpApiSettings. **/
- wp_enqueue_script( 'qsm_admin_js', plugins_url( 'js/qsm-admin.js', __FILE__ ), array( 'jquery', 'backbone', 'underscore', 'wp-util', 'jquery-ui-sortable', 'jquery-touch-punch', 'qsm-jquery-multiselect-js', 'wp-api-request' ), $this->version, true );
+ wp_enqueue_script( 'qsm_admin_js', plugins_url( 'js/qsm-admin.js', __FILE__ ), array( 'jquery', 'backbone', 'underscore', 'wp-util', 'jquery-ui-sortable', 'jquery-touch-punch', 'qsm-jquery-multiselect-js', 'wp-api-request', 'select2-js' ), $this->version, true );
wp_enqueue_style( 'jquer-multiselect-css', QSM_PLUGIN_CSS_URL . '/jquery.multiselect.min.css', array(), $this->version );
wp_enqueue_script( 'qsm-jquery-multiselect-js', QSM_PLUGIN_JS_URL . '/jquery.multiselect.min.js', array( 'jquery' ), $this->version, true );
wp_enqueue_script( 'micromodal_script', plugins_url( 'js/micromodal.min.js', __FILE__ ), array( 'jquery', 'qsm_admin_js' ), $this->version, true );
$qsm_variables = function_exists( 'qsm_text_template_variable_list' ) ? qsm_text_template_variable_list() : array();
$qsm_variables_name = array();
$qsm_quizzes = $wpdb->get_results("SELECT quiz_id, quiz_name FROM {$wpdb->prefix}mlw_quizzes");
+ $current_quiz_id = isset( $_GET['quiz_id'] ) ? intval( $_GET['quiz_id'] ) : 0;
+ $other_quizzes_count = 0;
+ if ( is_array( $qsm_quizzes ) ) {
+ foreach ( $qsm_quizzes as $quiz_obj ) {
+ if ( ! isset( $quiz_obj->quiz_id ) ) {
+ continue;
+ }
+ if ( $current_quiz_id && intval( $quiz_obj->quiz_id ) === $current_quiz_id ) {
+ continue;
+ }
+ $other_quizzes_count++;
+ }
+ }
foreach ( $qsm_variables as $key => $value ) {
// Iterate over each key of the nested object
if ( is_array( $value ) && ! empty($value) ) {
@@ -593,6 +671,7 @@
'insert_variable' => __("Insert QSM variables", 'quiz-master-next'),
'select_all' => __("Select All", 'quiz-master-next'),
'select' => __("Select", 'quiz-master-next'),
+ 'quiz_count' => $other_quizzes_count,
'qsmQuizzesObject' => $qsm_quizzes,
'arrow_up_image' => esc_url(QSM_PLUGIN_URL . 'assets/arrow-up-s-line.svg'),
'arrow_down_image' => esc_url(QSM_PLUGIN_URL . 'assets/arrow-down-s-line.svg'),
@@ -616,6 +695,54 @@
'warning_icon' => esc_url(QSM_PLUGIN_URL . 'assets/warning-message.png'),
'info_icon' => esc_url(QSM_PLUGIN_URL . 'assets/info-message.png'),
'question_shuffle' => __('Question shuffled successfully!', 'quiz-master-next'),
+ 'is_migration_done' => get_option( 'qsm_migration_results_processed', 0 ),
+ 'guided_wizard' => array(
+ 'storage_key' => 'qsm_setup_wizard_completed',
+ 'completed' => (int) get_user_meta( get_current_user_id(), 'qsm_setup_wizard_completed', true ),
+ 'nonce' => wp_create_nonce( 'qsm_setup_wizard_nonce' ),
+ 'guided_wizard' => __('Guided Wizard', 'quiz-master-next'),
+ 'answer_limit_area' => __('Set how many answers users can select.', 'quiz-master-next'),
+ 'grading_mode_area' => __('Choose how this question should be graded.', 'quiz-master-next'),
+ 'add_poll_type_area' => __('Turn this into a poll to show how others responded.', 'quiz-master-next'),
+ 'correct_answer_info_area' => __('Add an explanation to support the correct answer.', 'quiz-master-next'),
+ 'comments_area' => __('Allow users to add comments for this question.', 'quiz-master-next'),
+ 'hint_area' => __('Provide a hint to guide users before answering.', 'quiz-master-next'),
+ 'first_question' => __('Create your first question', 'quiz-master-next'),
+ 'question_type' => __('Choose your question type.', 'quiz-master-next'),
+ 'question_title' => __('Question Title', 'quiz-master-next'),
+ 'question_title_desc' => __('Write the question you want to ask your users.', 'quiz-master-next'),
+ 'add_answer' => __('Add Answers', 'quiz-master-next'),
+ 'add_answer_text' => __('Add all possible answers for this question.', 'quiz-master-next'),
+ 'add_answer_desc1' => __('Use the', 'quiz-master-next'),
+ 'add_answer_desc2' => __('buttons to add or remove answers.', 'quiz-master-next'),
+ 'add_answer_desc3' => __('Assign', 'quiz-master-next'),
+ 'add_answer_desc4' => __('points', 'quiz-master-next'),
+ 'add_answer_desc5' => __('and mark the', 'quiz-master-next'),
+ 'add_answer_desc6' => __('correct answer', 'quiz-master-next'),
+ 'add_answer_desc7' => __('Select the appropriate', 'quiz-master-next'),
+ 'add_answer_desc8' => __('label', 'quiz-master-next'),
+ 'add_answer_desc9' => __('(Optional).', 'quiz-master-next'),
+ 'save_question' => __('Save Question', 'quiz-master-next'),
+ 'save_question_desc' => __('Click <strong>Save Question</strong> to save your first question.', 'quiz-master-next'),
+ 'feature_image' => __( 'Featured Image (Optional)', 'quiz-master-next'),
+ 'feature_image_desc' => __( 'Add an image to visually enhance this question.', 'quiz-master-next'),
+ 'category' => __( 'Category (Optional)', 'quiz-master-next'),
+ 'category_desc' => __( 'Assign this question to one or more categories to organize, filter, and reuse it across quizzes.', 'quiz-master-next'),
+ 'question_status' => __( 'Published / Draft', 'quiz-master-next'),
+ 'question_status_desc1' => __( 'Use the toggle to switch between Draft and Published.', 'quiz-master-next'),
+ 'question_status_desc2' => __( 'Set it to Published to make the question available in quizzes, or keep it as Draft to continue editing.', 'quiz-master-next'),
+ 'advance_setting' => __( 'Advanced Settings', 'quiz-master-next'),
+ 'advance_setting_desc1' => __( 'Here you can configure advanced settings for this question.', 'quiz-master-next'),
+ 'advance_setting_desc2' => __( 'Use this section to control evaluation and learner feedback.', 'quiz-master-next'),
+ 'save_updates' => __( 'Save your updates', 'quiz-master-next'),
+ 'save_updates_desc' => __( 'Click “Save Question” to apply your changes and complete the setup', 'quiz-master-next'),
+ 'congrats2' => __( 'Congratulations!', 'quiz-master-next'),
+ 'congrats2_desc1' => __( 'Your advanced settings have been saved successfully.', 'quiz-master-next'),
+ 'congrats2_desc2' => __( 'The question logic and behavior are now updated.', 'quiz-master-next'),
+ 'congrats1' => __( 'Great start!', 'quiz-master-next'),
+ 'congrats1_desc1' => __( 'Your question is ready with basic settings.', 'quiz-master-next'),
+ 'congrats1_desc2' => __( 'Now you can customize logic and behavior to unlock its full potential.', 'quiz-master-next'),
+ ),
);
$qsm_admin_messages = apply_filters( 'qsm_admin_messages_after', $qsm_admin_messages );
wp_localize_script( 'qsm_admin_js', 'qsm_admin_messages', $qsm_admin_messages );
@@ -797,7 +924,7 @@
return;
}
$roles = (array) $user->roles;
- if ( empty( $roles ) || !isset($roles[0]) || !is_string($roles[0]) ) {
+ if ( empty( $roles ) || ! isset($roles[0]) || ! is_string($roles[0]) ) {
return;
}
$rolename = $roles[0];
@@ -900,6 +1027,7 @@
add_submenu_page( 'qsm_dashboard', __( 'Tools', 'quiz-master-next' ), __( 'Tools', 'quiz-master-next' ), $capabilities[2], 'qsm_quiz_tools', 'qsm_generate_quiz_tools' );
add_submenu_page( 'qsm_dashboard', __( 'Stats', 'quiz-master-next' ), __( 'Stats', 'quiz-master-next' ), $capabilities[2], 'qmn_stats', 'qmn_generate_stats_page' );
add_submenu_page( 'qsm_dashboard', __( 'About', 'quiz-master-next' ), __( 'About', 'quiz-master-next' ), $capabilities[2], 'qsm_quiz_about', 'qsm_generate_about_page' );
+ add_submenu_page( 'qsm_dashboard', __( 'Question Bank', 'quiz-master-next' ), __( 'Question Bank', 'quiz-master-next' ), $capabilities[6], 'qsm_question_bank', 'qsm_render_question_bank_page', 2 );
add_submenu_page( 'qsm_dashboard', __( 'Extensions Settings', 'quiz-master-next' ), '<span style="color:#f39c12;">' . __( 'Extensions', 'quiz-master-next' ) . '</span>', $capabilities[2], 'qmn_addons', 'qmn_addons_page', 34 );
add_submenu_page( 'qsm_dashboard', __( 'Free Add-ons', 'quiz-master-next' ), '<span style="color:#f39c12;">' . esc_html__( 'Free Add-ons', 'quiz-master-next' ) . '</span>', $capabilities[2], 'qsm-free-addon', 'qsm_display_optin_page', 90 );
--- a/quiz-master-next/php/admin/admin-dashboard.php
+++ b/quiz-master-next/php/admin/admin-dashboard.php
@@ -75,6 +75,8 @@
<?php
}
}
+
+ do_action( 'qsm_admin_dashboard_compatibility_after' );
}
function qsm_dashboard_display_change_log_section() {
@@ -277,7 +279,7 @@
*/
function qsm_dashboard_recent_taken_quiz() {
- global $wpdb;
+ global $wpdb, $mlwQuizMasterNext;
$mlw_result_data = $wpdb->get_row( "SELECT DISTINCT COUNT(result_id) as total_result FROM {$wpdb->prefix}mlw_results WHERE deleted=0", ARRAY_A );
if ( 0 != $mlw_result_data['total_result'] ) {
?>
@@ -342,7 +344,13 @@
|
<?php
$mlw_complete_time = '';
- $mlw_qmn_results_array = maybe_unserialize( $single_result_arr['quiz_results'] );
+ $is_new_format = $mlwQuizMasterNext->pluginHelper->is_new_format_result( $single_result_arr );
+ if ( $is_new_format ) {
+ // Load new format result structure
+ $mlw_qmn_results_array = $mlwQuizMasterNext->pluginHelper->get_formated_result_data( $single_result_arr['result_id'] );
+ } else {
+ $mlw_qmn_results_array = maybe_unserialize( $single_result_arr['quiz_results'] );
+ }
if ( is_array( $mlw_qmn_results_array ) ) {
$mlw_complete_hours = floor( $mlw_qmn_results_array[0] / 3600 );
if ( $mlw_complete_hours > 0 ) {
@@ -411,6 +419,8 @@
</div>
<?php
+ qsm_display_migration_tools_redirect_button();
+
$qsm_admin_dd = qsm_get_parsing_script_data();
if ( $qsm_admin_dd ) {
$popular_addons = isset( $qsm_admin_dd['popular_products'] ) ? $qsm_admin_dd['popular_products'] : array();
@@ -561,4 +571,51 @@
}
}
-add_action( 'admin_init', 'qsm_create_new_quiz_from_wizard' );
No newline at end of file
+add_action( 'admin_init', 'qsm_create_new_quiz_from_wizard' );
+
+/**
+ * Displays a redirect button to the migration tools page on the dashboard.
+ *
+ * This function outputs a styled section on the dashboard that encourages users
+ * to perform a database migration. It includes a heading, a brief description,
+ * and a button that links to the migration tools page.
+ *
+ * @since 11.0.0
+ * @return void
+ */
+function qsm_display_migration_tools_redirect_button() {
+ // Only show this section if the migration has not been completed.
+ if ( 1 == get_option( 'qsm_migration_results_processed' ) ) {
+ return;
+ }
+ ?>
+ <div class="qsm-dashboard-migration-section qsm-dashboard-page-common-style">
+ <div class="qsm-dashboard-page-header">
+ <h3 class="qsm-dashboard-card-title"><?php esc_html_e( 'Database Migration', 'quiz-master-next' ); ?></h3>
+ </div>
+ <div class="qsm-db-migration-container">
+ <div class="qsm-migration-notice qsm-migration-info">
+ <div class="qsm-migration-notice-header">
+ <strong><?php esc_html_e( 'You’ve updated to QSM 11', 'quiz-master-next' ); ?></strong>
+ </div>
+ <p><?php esc_html_e( 'Complete a one-time database migration to ensure your quizzes and results work smoothly with the new version and its improved rendering experience.', 'quiz-master-next' ); ?></p>
+ </div>
+ <div class="qsm-migration-notice qsm-migration-warning">
+ <div class="qsm-migration-notice-header">
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M10 0C4.48 0 0 4.48 0 10C0 15.52 4.48 20 10 20C15.52 20 20 15.52 20 10C20 4.48 15.52 0 10 0ZM11 15H9V13H11V15ZM11 11H9V5H11V11Z" fill="#F59E0B"/>
+ </svg>
+ <strong><?php esc_html_e( 'Important', 'quiz-master-next' ); ?></strong>
+ </div>
+ <p><?php esc_html_e( 'After migration, new quiz results will not be compatible with older versions of QSM. If you downgrade later, these results may not be accessible.', 'quiz-master-next' ); ?></p>
+ </div>
+ <div class="qsm-migration-action">
+ <a class="button button-primary qsm-dashboard-section-migration" href="<?php echo esc_url( admin_url( 'admin.php?page=qsm_quiz_tools&tab=qsm_tools_page_migration' ) ); ?>">
+ <?php esc_html_e( 'Go To Migration', 'quiz-master-next' ); ?>
+ </a>
+ <p class="qsm-migration-note"><?php esc_html_e( 'Your data will remain safe during migration', 'quiz-master-next' ); ?></p>
+ </div>
+ </div>
+ </div>
+ <?php
+}
No newline at end of file
--- a/quiz-master-next/php/admin/admin-results-details-page.php
+++ b/quiz-master-next/php/admin/admin-results-details-page.php
@@ -75,18 +75,18 @@
if ( empty($results_data) ) {
$resultpage_link = admin_url('admin.php?page=mlw_quiz_results');
- ?>
- <div id="qsm-dashboard-error-container">
- <div class="qsm-dashboard-error-content">
- <h3><?php esc_html_e('Quiz Result Not Available', 'quiz-master-next'); ?></h3>
- <p><?php esc_html_e('The quiz result you are trying to view could not be found. Please return to the results page.', 'quiz-master-next'); ?></p>
- <a href="<?php echo esc_url($resultpage_link); ?>" class="qsm-dashboard-error-btn">
- <?php esc_html_e('Back to All Results', 'quiz-master-next'); ?>
- </a>
+ ?>
+ <div id="qsm-dashboard-error-container">
+ <div class="qsm-dashboard-error-content">
+ <h3><?php esc_html_e('Quiz Result Not Available', 'quiz-master-next'); ?></h3>
+ <p><?php esc_html_e('The quiz result you are trying to view could not be found. Please return to the results page.', 'quiz-master-next'); ?></p>
+ <a href="<?php echo esc_url($resultpage_link); ?>" class="qsm-dashboard-error-btn">
+ <?php esc_html_e('Back to All Results', 'quiz-master-next'); ?>
+ </a>
+ </div>
</div>
- </div>
- <?php
- return;
+ <?php
+ return;
}
// Prepare plugin helper.
$quiz_id = intval( $results_data->quiz_id );
@@ -129,7 +129,13 @@
// Prepare responses array.
$total_hidden_questions = 0;
- $results = maybe_unserialize( $results_data->quiz_results );
+ $is_new_format = $mlwQuizMasterNext->pluginHelper->is_new_format_result( $results_data );
+ if ( $is_new_format ) {
+ // Load new format result structure
+ $mlw_qmn_results_array = $results = $mlwQuizMasterNext->pluginHelper->get_formated_result_data( $results_data->result_id );
+ } else {
+ $mlw_qmn_results_array = $results = maybe_unserialize( $results_data->quiz_results );
+ }
if ( is_array( $results ) ) {
$total_hidden_questions = ! empty( $results['hidden_questions'] ) && is_array( $results['hidden_questions'] ) ? count( $results['hidden_questions'] ) : 0;
if ( ! isset( $results["contact"] ) ) {
@@ -179,7 +185,6 @@
}
if ( 1 === intval( $new_template_result_detail ) ) {
$template = '';
- $mlw_qmn_results_array = maybe_unserialize( $results_data->quiz_results );
if ( is_array( $mlw_qmn_results_array ) ) {
$span_start = '<span class="result-candidate-span"><label>';
$span_end = '</label><span>';
@@ -306,7 +311,7 @@
}
}
- if ( ! is_array( maybe_unserialize( $results_data->quiz_results ) ) ) {
+ if ( ! is_array( maybe_unserialize( $results_data->quiz_results ) ) && '' != $results_data->quiz_results ) {
$template = str_replace( "%QUESTIONS_ANSWERS%" , $results_data->quiz_results, $template );
$template = str_replace( "%TIMER%" , '', $template );
$template = str_replace( "%COMMENT_SECTION%" , '', $template );
--- a/quiz-master-next/php/admin/admin-results-page.php
+++ b/quiz-master-next/php/admin/admin-results-page.php
@@ -30,6 +30,9 @@
<?php } ?>
</h2>
</div>
+ <?php
+ qsm_show_results_migration_warning();
+ ?>
<?php $mlwQuizMasterNext->alertManager->showAlerts(); ?>
<?php qsm_show_adverts(); ?>
<h2 class="nav-tab-wrapper">
@@ -90,10 +93,17 @@
* @return void
*/
function qsm_delete_results_attachments( $rows_before_update ) {
+ global $mlwQuizMasterNext;
// Loop through each row in the results
foreach ( $rows_before_update as $row ) {
// Unserialize the quiz results
- $mlw_qmn_results_array = maybe_unserialize( $row->quiz_results );
+ $is_new_format = $mlwQuizMasterNext->pluginHelper->is_new_format_result( $row );
+ if ( $is_new_format ) {
+ // Load new format result structure
+ $mlw_qmn_results_array = $mlwQuizMasterNext->pluginHelper->get_formated_result_data( $row->result_id );
+ } else {
+ $mlw_qmn_results_array = maybe_unserialize( $row->quiz_results );
+ }
// Ensure the results array exists and has the expected structure
foreach ( $mlw_qmn_results_array[1] as $key => $value ) {
// Check if the question type is 11 and user answer is not empty
@@ -424,7 +434,13 @@
foreach ( $mlw_quiz_data as $mlw_quiz_info ) {
$quiz_infos[] = $mlw_quiz_info;
$mlw_complete_time = '';
- $mlw_qmn_results_array = maybe_unserialize( $mlw_quiz_info->quiz_results );
+ $is_new_format = $mlwQuizMasterNext->pluginHelper->is_new_format_result( $mlw_quiz_info );
+ if ( $is_new_format ) {
+ // Load new format result structure
+ $mlw_qmn_results_array = $mlwQuizMasterNext->pluginHelper->get_formated_result_data( $mlw_quiz_info->result_id );
+ } else {
+ $mlw_qmn_results_array = maybe_unserialize( $mlw_quiz_info->quiz_results );
+ }
$hidden_questions = ! empty( $mlw_qmn_results_array['hidden_questions'] ) && is_array($mlw_qmn_results_array['hidden_questions']) ? count( $mlw_qmn_results_array['hidden_questions'] ) : 0;
if ( is_array( $mlw_qmn_results_array ) ) {
$mlw_complete_hours = floor( $mlw_qmn_results_array[0] / 3600 );
@@ -486,19 +502,14 @@
if ( isset( $values['start_date'] ) ) {
if ( isset($mlw_qmn_results_array['quiz_start_date']) ) {
- $sdate = gmdate( get_option( 'date_format' ), strtotime( $mlw_qmn_results_array['quiz_start_date'] ) );
- $stime = gmdate( "h:i:s A", strtotime( $mlw_qmn_results_array['quiz_start_date'] ) );
- $values['start_date']['content'][] = $sdate .' '. $stime;
+ $values['start_date']['content'][] = $mlw_qmn_results_array['quiz_start_date'];
} else {
$values['start_date']['content'][] = ' ';
}
}
- $date = gmdate( get_option( 'date_format' ), strtotime( $mlw_quiz_info->time_taken ) );
- $time = gmdate( "h:i:s A", strtotime( $mlw_quiz_info->time_taken ) );
-
if ( isset( $values['time_taken'] ) ) {
- $values['time_taken']['content'][] = $date .' '. $time;
+ $values['time_taken']['content'][] = $mlw_quiz_info->time_taken;
}
if ( isset( $values['ip'] ) ) {
$values['ip']['content'][] = $mlw_quiz_info->user_ip;
@@ -605,10 +616,13 @@
<a class="qsm-popup__close qsm-popup-upgrade-close" aria-label="Close modal" data-micromodal-close></a>
</header>
<main class="qsm-popup__content" id="modal-2-content">
- <div class="qsm-result-page-delete-message"><?php esc_html_e( 'Are you sure you want to delete these results?', 'quiz-master-next' ); ?></div>
- <?php wp_nonce_field( 'delete_results', 'delete_results_nonce' ); ?>
- <input type='hidden' id='result_id' name='result_id' value='' />
- <input type='hidden' id='delete_quiz_name' name='delete_quiz_name' value='' />
+ <div class="qsm-result-page-delete-message">
+ <?php esc_html_e( 'Are you sure you want to delete these results?', 'quiz-master-next' ); ?><br/>
+ <p><em><?php esc_html_e( 'This will permanently remove all associated data and metadata', 'quiz-master-next' ); ?></em></p>
+ </div>
+ <?php wp_nonce_field( 'delete_results', 'delete_results_nonce' ); ?>
+ <input type='hidden' id='result_id' name='result_id' value='' />
+ <input type='hidden' id='delete_quiz_name' name='delete_quiz_name' value='' />
</main>
<footer class="qsm-popup__footer">
<button class="qsm-popup__btn" data-micromodal-close aria-label="Close this dialog window"><?php esc_html_e( 'Cancel', 'quiz-master-next' ); ?></button>
--- a/quiz-master-next/php/admin/class-qsm-database-migration.php
+++ b/quiz-master-next/php/admin/class-qsm-database-migration.php
@@ -0,0 +1,815 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * QSM Database Migration
+ *
+ * Behavior:
+ * - Migration log table (one row per result_id) prevents duplicate migration
+ * - Each result_id's question inserts are done atomically with a transaction:
+ * - success -> log success
+ * - failure -> rollback result and log error; continue with other results
+ */
+class QSM_Database_Migration {
+
+ public $wpdb;
+ const BATCH_SIZE = 10;
+
+ public function __construct() {
+ global $wpdb;
+ $this->wpdb = $wpdb;
+
+ // Register AJAX endpoints
+ add_action('wp_ajax_qsm_start_migration', array( $this, 'qsm_initial_migration_start_callback' ));
+ add_action('wp_ajax_qsm_process_migration_batch', array( $this, 'qsm_process_migration_batch_callback' ));
+ }
+
+ private function qsm_is_migration_allowed() {
+ if ( function_exists( 'qsm_migration_evaluate_addon_requirements' ) ) {
+ $compatibility = qsm_migration_evaluate_addon_requirements();
+ return ! empty( $compatibility['allowed'] );
+ }
+
+ return true;
+ }
+
+ private function qsm_get_migration_block_message() {
+ if ( function_exists( 'qsm_migration_evaluate_addon_requirements' ) ) {
+ $compatibility = qsm_migration_evaluate_addon_requirements();
+ if ( ! empty( $compatibility['message'] ) ) {
+ return $compatibility['message'];
+ }
+ }
+
+ return __( 'Migration is currently disabled due to version compatibility requirements.', 'quiz-master-next' );
+ }
+
+ function create_migration_tables() {
+
+ $charset_collate = $this->wpdb->get_charset_collate();
+ $mlw_results_table = $this->wpdb->prefix . 'mlw_results';
+ $results_questions = $this->wpdb->prefix . 'qsm_results_questions';
+ if ( $this->wpdb->get_var( "SHOW TABLES LIKE '{$results_questions}'" ) != $results_questions ) {
+ $sql_results_answers = "CREATE TABLE {$results_questions} (
+ `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `result_id` MEDIUMINT(9) NOT NULL,
+ `quiz_id` MEDIUMINT(9) NOT NULL,
+ `question_id` MEDIUMINT(9) NOT NULL,
+ `question_title` TEXT,
+ `question_description` LONGTEXT,
+ `question_comment` TEXT,
+ `question_type` VARCHAR(50),
+ `answer_type` VARCHAR(50) DEFAULT 'text',
+ `correct_answer` TEXT,
+ `user_answer` TEXT,
+ `user_answer_comma` TEXT,
+ `correct_answer_comma` TEXT,
+ `points` FLOAT DEFAULT 0,
+ `correct` TINYINT(1) DEFAULT 0,
+ `category` TEXT,
+ `multicategories` TEXT,
+ `other_settings` TEXT,
+ PRIMARY KEY (`id`),
+ KEY `result_id` (`result_id`),
+ KEY `question_id` (`question_id`),
+ KEY `quiz_id` (`quiz_id`),
+ KEY `result_question` (`result_id`, `question_id`)
+ ) ENGINE=InnoDB {$charset_collate};";
+
+ require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
+ dbDelta($sql_results_answers);
+
+ $this->maybe_add_foreign_key(
+ $results_questions,
+ 'qsm_fk_results_questions_result_id',
+ "ALTER TABLE {$results_questions} ADD CONSTRAINT `qsm_fk_results_questions_result_id` FOREIGN KEY (`result_id`) REFERENCES `{$mlw_results_table}` (`result_id`) ON DELETE CASCADE"
+ );
+ }
+
+ // Ensure results meta table
+ $results_meta_table = $this->wpdb->prefix . 'qsm_results_meta';
+ if ( $this->wpdb->get_var( "SHOW TABLES LIKE '{$results_meta_table}'" ) != $results_meta_table ) {
+ $mlw_results_table = $this->wpdb->prefix . 'mlw_results';
+ $sql_results_meta = "CREATE TABLE {$results_meta_table} (
+ `meta_id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `result_id` MEDIUMINT(9) NOT NULL,
+ `meta_key` VARCHAR(191) NOT NULL,
+ `meta_value` LONGTEXT,
+ PRIMARY KEY (`meta_id`),
+ KEY `result_id` (`result_id`),
+ KEY `meta_key` (`meta_key`)
+ ) ENGINE=InnoDB {$charset_collate};";
+
+ require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
+ dbDelta($sql_results_meta);
+
+ $this->maybe_add_foreign_key(
+ $results_meta_table,
+ 'qsm_fk_results_meta_result_id',
+ "ALTER TABLE {$results_meta_table} ADD CONSTRAINT `qsm_fk_results_meta_result_id` FOREIGN KEY (`result_id`) REFERENCES `{$mlw_results_table}` (`result_id`) ON DELETE CASCADE"
+ );
+ }
+
+ // Add any missing indexes (safe checks)
+ $this->maybe_add_index(
+ $results_questions,
+ 'idx_qra_result_id',
+ "CREATE INDEX idx_qra_result_id ON {$results_questions} (result_id)"
+ );
+
+ $this->maybe_add_index(
+ $results_questions,
+ 'idx_qra_result_question',
+ "CREATE INDEX idx_qra_result_question ON {$results_questions} (result_id, question_id)"
+ );
+
+ $this->maybe_add_index(
+ $results_meta_table,
+ 'idx_qsm_meta_result_id',
+ "CREATE INDEX idx_qsm_meta_result_id ON {$results_meta_table} (result_id)"
+ );
+ }
+
+ /**
+ * Helper to add an index only if it doesn't exist.
+ */
+ private function maybe_add_index( $table, $index_name, $sql ) {
+ $exists = $this->wpdb->get_var(
+ $this->wpdb->prepare(
+ "SELECT COUNT(1) FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = %s AND index_name = %s",
+ $table, $index_name
+ )
+ );
+
+ if ( ! $exists ) {
+ $this->wpdb->query($sql);
+ }
+ }
+
+ private function maybe_add_foreign_key( $table, $constraint_name, $sql ) {
+ $exists = $this->wpdb->get_var(
+ $this->wpdb->prepare(
+ "SELECT COUNT(1) FROM information_schema.table_constraints WHERE constraint_schema = DATABASE() AND table_name = %s AND constraint_name = %s",
+ $table,
+ $constraint_name
+ )
+ );
+
+ if ( ! $exists ) {
+ $this->wpdb->query( $sql );
+ }
+ }
+
+ /**
+ * AJAX callback to initiate migration: create tables & return totals
+ */
+ public function qsm_initial_migration_start_callback() {
+ // Verify nonce
+ if ( ! check_ajax_referer('qsm_migration_nonce', 'nonce', false) ) {
+ wp_send_json_error(array( 'message' => __('Security check failed.', 'quiz-master-next') ));
+ }
+
+ // Capability check
+ if ( ! current_user_can('manage_options') ) {
+ wp_send_json_error(array( 'message' => __('You do not have permission to perform migrations.', 'quiz-master-next') ));
+ }
+
+ if ( ! $this->qsm_is_migration_allowed() ) {
+ wp_send_json_error( array( 'message' => $this->qsm_get_migration_block_message() ) );
+ }
+
+ // Ensure tables exist and indexes applied
+ $this->create_migration_tables();
+
+ // --- Calculate Total Records to Migrate ---
+ $mlw_results_table = $this->wpdb->prefix . 'mlw_results';
+ $results_meta_table = $this->wpdb->prefix . 'qsm_results_meta';
+
+ // Count total results (the target count)
+ $total_records = (int) $this->wpdb->get_var( "SELECT COUNT(*) FROM {$mlw_results_table}" );
+
+ // Count how many results have been *logged* (migrated or failed)
+ $logged_records = (int) $this->wpdb->get_var(
+ "SELECT COUNT(DISTINCT r.result_id)
+ FROM {$mlw_results_table} r
+ INNER JOIN {$results_meta_table} m
+ ON m.result_id = r.result_id
+ AND m.meta_key = 'result_meta'"
+ );
+
+ // Number of records already processed (logged)
+ $processed_count = $logged_records;
+
+ // If all records are logged and there are no failed IDs, it's complete.
+ $stored_failed_ids = get_option('qsm_migration_results_failed_ids', array());
+ if ( $processed_count === $total_records && empty($stored_failed_ids) ) {
+ update_option( 'qsm_migration_results_processed', 1 );
+ wp_send_json_success(array(
+ 'message' => __('Migration has already been completed. No further action is required.', 'quiz-master-next'),
+ 'already_done' => true,
+ 'total_records' => $total_records, // Send original total for progress bar
+ 'processed_count' => $processed_count,
+ 'batch_size' => self::BATCH_SIZE,
+ ));
+ }
+
+ try {
+
+ wp_send_json_success(array(
+ 'message' => __('Migration initiated. Starting batch processing.', 'quiz-master-next'),
+ 'total_records' => $total_records,
+ 'processed_count' => $processed_count, // Send back how many are already logged
+ 'batch_size' => self::BATCH_SIZE,
+ ));
+
+ } catch ( Exception $e ) {
+ wp_send_json_error(array( 'message' => __('Error during migration initialization: ', 'quiz-master-next') . $e->getMessage() ));
+ }
+ }
+
+ /**
+ * AJAX callback to process a single batch of results.
+ * Expects POST parameter: offset (int) - this is the running count of processed records for UI only.
+ */
+ public function qsm_process_migration_batch_callback() {
+ // Verify nonce
+ if ( ! check_ajax_referer( 'qsm_migration_nonce', 'nonce', false ) ) {
+ wp_send_json_error( array( 'message' => __( 'Security check failed.', 'quiz-master-next' ) ) );
+ }
+
+ // Check user capabilities
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_send_json_error( array( 'message' => __( 'You do not have permission to perform migrations.', 'quiz-master-next' ) ) );
+ }
+
+ if ( ! $this->qsm_is_migration_allowed() ) {
+ wp_send_json_error( array( 'message' => $this->qsm_get_migration_block_message() ) );
+ }
+
+ $current_processed_count = isset( $_POST['current_processed_count'] ) ? intval( $_POST['current_processed_count'] ) : 0;
+ $process_failed_only = ! empty( $_POST['process_failed_only'] );
+ $batch_size = self::BATCH_SIZE;
+
+ $mlw_results_table = $this->wpdb->prefix . 'mlw_results';
+ $results_meta_table = $this->wpdb->prefix . 'qsm_results_meta';
+
+ $results_processed = 0;
+ $inserted_count = 0;
+
+ try {
+
+ // Re-check status before processing
+ if ( $this->qsm_check_migration_status() ) {
+ wp_send_json_success(
+ array(
+ 'message' => __( 'Migration has already been completed. No further action is required.', 'quiz-master-next' ),
+ 'results_processed' => 0,
+ 'migrated_results' => 0,
+ 'failed_results' => 0,
+ 'next_offset' => $current_processed_count,
+ 'completed' => true,
+ 'success_ids' => array(),
+ 'failed_ids' => array(),
+ )
+ );
+ }
+
+ // --------------------------------------------------
+ // 1. Fetch a batch of results to migrate
+ // --------------------------------------------------
+ $failed_ids = get_option( 'qsm_migration_results_failed_ids', array() );
+ $failed_ids = array_map( 'intval', (array) $failed_ids );
+
+ $query = "SELECT r.* FROM {$mlw_results_table} r
+ LEFT JOIN {$results_meta_table} m
+ ON m.result_id = r.result_id
+ AND m.meta_key = 'result_meta'
+ WHERE m.meta_id IS NULL";
+
+ $params = array();
+
+ if ( $process_failed_only ) {
+ // Failed-only mode: process only the IDs currently marked as failed.
+ if ( empty( $failed_ids ) ) {
+ // Nothing to process in this mode.
+ wp_send_json_success(
+ array(
+ 'message' => __( 'No failed results to reprocess.', 'quiz-master-next' ),
+ 'results_processed' => 0,
+ 'migrated_results' => 0,
+ 'failed_results' => 0,
+ 'next_offset' => $current_processed_count,
+ 'completed' => true, // Signal completion for this mode
+ 'success_ids' => array(),
+ 'failed_ids' => array(),
+ )
+ );
+ }
+
+ // In failed-only mode, we are only processing records in $failed_ids
+ $placeholders = implode( ',', array_fill( 0, count( $failed_ids ), '%d' ) );
+ $query = "SELECT r.* FROM {$mlw_results_table} r WHERE r.result_id IN ($placeholders)";
+ $params = array_merge( $params, $failed_ids );
+ } else {
+ // Normal mode: avoid repeatedly retrying known failed IDs (if they are still pending)
+ if ( ! empty( $failed_ids ) ) {
+ $placeholders = implode( ',', array_fill( 0, count( $failed_ids ), '%d' ) );
+ $query .= " AND r.result_id NOT IN ($placeholders)";
+ $params = array_merge( $params, $failed_ids );
+ }
+ }
+
+ // NOTE: Do NOT use OFFSET here. Always take the next BATCH_SIZE pending rows.
+ $query .= " ORDER BY r.result_id ASC LIMIT %d";
+ $params[] = $batch_size;
+
+ // Build prepared SQL safely
+ if ( ! empty( $params ) ) {
+ $prepare_args = array_merge( array( $query ), $params );
+ $prepared_sql = call_user_func_array( array( $this->wpdb, 'prepare' ), $prepare_args );
+ } else {
+ $prepared_sql = $query;
+ }
+
+ $results = $this->wpdb->get_results( $prepared_sql );
+
+ if ( empty( $results ) ) {
+ // Nothing left to process in this batch (either normal or failed-only mode)
+ $is_completed = $this->qsm_check_migration_status();
+
+ wp_send_json_success(
+ array(
+ 'message' => __( 'No more results to process in this batch.', 'quiz-master-next' ),
+ 'results_processed' => 0,
+ 'inserted_count' => 0,
+ 'completed' => (bool) $is_completed,
+ 'migrated_results' => 0,
+ 'failed_results' => 0,
+ 'next_offset' => $current_processed_count, // Offset remains the same
+ )
+ );
+ }
+
+ // Track IDs processed in this batch
+ $batch_success_ids = array();
+ $batch_failed_ids = array();
+
+ // --------------------------------------------------
+ // 2. Loop through results and migrate each one
+ // --------------------------------------------------
+ foreach ( $results as $row ) {
+ $row_stats = $this->qsm_do_process_result_row( $row );
+
+ if ( isset( $row_stats['results_processed'] ) ) {
+ $results_processed += (int) $row_stats['results_processed'];
+ }
+ if ( isset( $row_stats['inserted_count'] ) ) {
+ $inserted_count += (int) $row_stats['inserted_count'];
+ }
+ if ( ! empty( $row_stats['success_ids'] ) && is_array( $row_stats['success_ids'] ) ) {
+ $batch_success_ids = array_merge( $batch_success_ids, $row_stats['success_ids'] );
+ }
+ if ( ! empty( $row_stats['failed_ids'] ) && is_array( $row_stats['failed_ids'] ) ) {
+ $batch_failed_ids = array_merge( $batch_failed_ids, $row_stats['failed_ids'] );
+ }
+ }
+
+ $stored_failed_ids = get_option( 'qsm_migration_results_failed_ids', array() );
+
+ // 3. Update the stored failed IDs list (remove success, add new fails)
+ if ( ! empty( $batch_success_ids ) ) {
+ $stored_failed_ids = array_diff(
+ array_map( 'intval', (array) $stored_failed_ids ),
+ array_map( 'intval', (array) $batch_success_ids )
+ );
+ }
+
+ // Newly failed IDs are appended to the failed list
+ if ( ! empty( $batch_failed_ids ) ) {
+ $stored_failed_ids = array_unique(
+ array_map(
+ 'intval',
+ array_merge( (array) $stored_failed_ids, $batch_failed_ids )
+ )
+ );
+ }
+
+ update_option( 'qsm_migration_results_failed_ids', $stored_failed_ids );
+
+ // Count how many results have been *logged* (migrated or failed)
+ $logged_records = (int) $this->wpdb->get_var(
+ "SELECT COUNT(DISTINCT r.result_id)
+ FROM {$mlw_results_table} r
+ INNER JOIN {$results_meta_table} m
+ ON m.result_id = r.result_id
+ AND m.meta_key = 'result_meta'"
+ );
+
+ $next_offset = $logged_records;
+
+ $failed_total = ! empty( $stored_failed_ids ) ? count( (array) $stored_failed_ids ) : 0;
+
+ $total_success_count = max( 0, $logged_records - $failed_total );
+ $total_failed_count = $failed_total;
+
+ $completed = $this->qsm_check_migration_status();
+
+ wp_send_json_success(
+ array(
+ 'message' => __( 'Batch processed.', 'quiz-master-next' ),
+ 'results_processed' => (int) $results_processed,
+ 'migrated_results' => (int) $total_success_count,
+ 'failed_results' => (int) $total_failed_count,
+ 'next_offset' => $next_offset, // This is the total number of logged records
+ 'completed' => (bool) $completed,
+ 'success_ids' => $batch_success_ids,
+ 'failed_ids' => $batch_failed_ids,
+ )
+ );
+ } catch ( Exception $e ) {
+ wp_send_json_error(
+ array(
+ 'message' => __( 'Error during migration batch: ', 'quiz-master-next' ) . $e->getMessage(),
+ )
+ );
+ }
+ }
+
+ public function qsm_check_migration_status(){
+ $results_table_name = $this->wpdb->prefix . 'mlw_results';
+ $results_meta_table = $this->wpdb->prefix . 'qsm_results_meta';
+
+ $total_results = (int) $this->wpdb->get_var( "SELECT COUNT(*) FROM {$results_table_name}" );
+
+ // Count how many results have been *logged* (migrated or failed)
+ $logged_records = (int) $this->wpdb->get_var(
+ "SELECT COUNT(DISTINCT r.result_id)
+ FROM {$results_table_name} r
+ INNER JOIN {$results_meta_table} m
+ ON m.result_id = r.result_id
+ AND m.meta_key = 'result_meta'"
+ );
+
+ $stored_failed_ids = get_option( 'qsm_migration_results_failed_ids', array() );
+
+ // Migration is completed if ALL original results have a 'result_meta' entry AND there are no failed IDs left.
+ $completed = ( $logged_records === $total_results && empty( $stored_failed_ids ) );
+
+ if ( $completed ) {
+ update_option( 'qsm_migration_results_processed', 1 );
+ }
+ return $completed;
+ }
+
+
+ function qsm_do_process_result_row( $row ) {
+ global $mlwQuizMasterNext;
+
+ $stats = array(
+ 'success_ids' => array(),
+ 'failed_ids' => array(),
+ 'results_processed' => 0,
+ 'inserted_count' => 0,
+ );
+
+ if ( empty( $row ) || ! is_object( $row ) ) {
+ return $stats;
+ }
+
+ $stats['results_processed']++;
+
+ $result_id = isset( $row->result_id ) ? intval( $row->result_id ) : 0;
+
+ if ( ! $result_id ) {
+ $stats['failed_ids'][] = 0;
+ return $stats;
+ }
+
+ $results_questions = $this->wpdb->prefix . 'qsm_results_questions';
+ $results_meta_table = $this->wpdb->prefix . 'qsm_results_meta';
+
+ // ----------------------------------------------
+ // Parse quiz_results and insert per-question answers
+ // ----------------------------------------------
+ $unserializedResults = maybe_unserialize( $row->quiz_results );
+
+ // *************** TRANSACTION START ***************
+ $this->wpdb->query( 'START TRANSACTION' );
+ $transaction_failed = false;
+
+ if ( ! is_array( $unserializedResults ) || ! isset( $unserializedResults[1] ) || ! is_array( $unserializedResults[1] ) ) {
+ // No questions found; treat as migrated without inserts
+ // Insert result_meta to mark as processed.
+ // Continue below to insert result_meta to log processing.
+ } else {
+
+ $results_meta_table_data = array();
+ $results_meta_table_ans_label = '';
+ $results_table_meta_contact = '';
+ $results_table_meta_addons = array();
+ $allowed_result_meta_keys = array(
+ 'total_seconds',
+ 'quiz_comments',
+ 'timer_ms',
+ 'pagetime',
+ 'hidden_questions',
+ 'total_possible_points',
+ 'total_attempted_questions',
+ 'minimum_possible_points',
+ 'quiz_start_date',
+ );
+
+ foreach ( $unserializedResults as $result_meta_key => $result_meta_value ) {
+ if ( 1 == $result_meta_key ) {
+ // results question loop (answers table) – collect all rows then bulk insert
+ $answer_rows = array();
+
+ foreach ( $result_meta_value as $question_key => $question_value ) {
+ if ( ! is_array( $question_value ) || ! isset( $question_value['id'] ) ) {
+ continue;
+ }
+
+ // incorrect = 0, correct = 1, unanswered = 2
+ $correcIncorrectUnanswered = 0;
+
+ if ( 'correct' == $question_value['correct'] || ( isset( $question_value[0]['correct'] ) && 'correct' == $question_value[0]['correct'] ) ) {
+ $correcIncorrectUnanswered = 1;
+ } else {
+
+ if ( empty( $question_value['user_answer'] ) ) {
+
+ if ( '13' == $question_value['question_type'] && 'incorrect' == $question_value['correct'] && ( 0 == $question_value[1] || ! empty( $question_value[1] ) ) ) {
+ $correcIncorrectUnanswered = 0;
+ } else {
+ if ( '13' != $question_value['question_type'] && 'incorrect' == $question_value['correct'] ) {
+ if ( '7' == $question_value['question_type'] && ! empty( $question_value[1] ) && $question_value[1] != $question_value[2] ) {
+ $correcIncorrectUnanswered = 0;
+ } else {
+ $correcIncorrectUnanswered = 2;
+ }
+ } else {
+ if ( empty( $question_value[1] ) ) {
+ $correcIncorrectUnanswered = 2;
+ }
+ }
+ }
+ } elseif ( 'incorrect' == $question_value['correct'] ) {
+ $ans_loop = 0;
+ $is_unanswer = 0;
+ if ( in_array( $question_value['question_type'], array( '14', '12', '3', '5' ), true ) ) {
+ foreach ( $question_value['user_answer'] as $ans_key => $ans_value ) {
+ if ( '' == $ans_value ) {
+ $is_unanswer++;
+ }
+ $ans_loop++;
+ }
+ }
+ if ( 0 != $is_unanswer && $ans_loop == $is_unanswer ) {
+ $correcIncorrectUnanswered = 2;
+ } else {
+ $correcIncorrectUnanswered = 0;
+ }
+
+ if ( isset( $question_value['question_type'] ) && 4 != $question_value['question_type'] && isset( $question_value[1] ) && isset( $question_value[2] ) && $question_value[1] == $question_value[2] ) {
+ // Advanced question types conditions here
+ if ( ( '17' == $question_value['question_type'] || '16' == $question_value['question_type'] ) && empty( $question_value['correct_answer'] ) ) {
+ if ( '16' == $question_value['question_type'] && empty( $question_value['user_answer'] ) ) {
+ $correcIncorrectUnanswered = 2;
+ }
+ if ( '17' == $question_value['question_type'] && empty( $question_value['user_answer'] ) ) {
+ $correcIncorrectUnanswered = 2;
+ }
+ } else {
+ $correcIncorrectUnanswered = 1;
+ }
+ }
+ }
+ }
+
+ // Normalize user_answer and correct_answer fields for storage
+ $user_answer_to_store = isset( $question_value['user_answer'] ) ? $question_value['user_answer'] : array();
+ $correct_answer_to_store = isset( $question_value['correct_answer'] ) ? $question_value['correct_answer'] : array();
+
+ // Map fields (use fallbacks for numeric keys)
+ $question_description = isset( $question_value[0] ) ? $question_value[0] : '';
+
+ $user_answer_comma = isset( $question_value[1] ) ? $question_value[1] : '';
+
+ $correct_answer_comma = isset( $question_value[2] ) ? $question_value[2] : '';
+
+ $question_comment = isset( $question_value[3] ) ? $question_value[3] : '';
+
+ $question_title = '';
+ if ( isset( $question_value['question_title'] ) ) {
+ $question_title = $question_value['question_title'];
+ } elseif ( isset( $question_value['question'] ) ) {
+ $question_title = (string) $question_value['question'];
+ }
+
+ $question_type = isset( $question_value['question_type'] ) ? $question_value['question_type'] : '';
+
+ // Determine answer_type using heuristic
+ $answerEditor = $mlwQuizMasterNext->pluginHelper->get_question_setting( $question_value['id'], 'answerEditor' );
+ $answer_type = '' != $answerEditor ? $answerEditor : 'text';
+
+ $points = isset( $question_value['points'] ) ? $question_value['points'] : 0;
+ $correct = intval( $correcIncorrectUnanswered );
+
+ $category = isset( $question_value['category'] ) ? $question_value['category'] : '';
+
+ $multicategories = isset( $question_value['multicategories'] ) ? $question_value['multicategories'] : array();
+
+ // Ensure values passed to $wpdb->prepare are scalars; serialize any arrays.
+ if ( is_array( $category ) ) {
+ $category = maybe_serialize( $category );
+ }
+
+ if ( is_array( $multicategories ) ) {
+ $multicategories = maybe_serialize( $multicategories );
+ }
+
+ // other_settings from question fields (user_compare_text, case_sensitive, answer_limit_keys)
+ $other_settings_arr = array();
+ if ( isset( $question_value['user_compare_text'] ) ) {
+ $other_settings_arr['user_compare_text'] = $question_value['user_compare_text'];
+ }
+ if ( isset( $question_value['case_sensitive'] ) ) {
+ $other_settings_arr['case_sensitive'] = $question_value['case_sensitive'];
+ }
+ if ( isset( $question_value['answer_limit_keys'] ) ) {
+ $other_settings_arr['answer_limit_keys'] = $question_value['answer_limit_keys'];
+ }
+ $other_settings_serialized = maybe_serialize( $other_settings_arr );
+
+ // Prepare values for insert (order must match placeholders below)
+ $quiz_id = isset( $row->quiz_id ) ? intval( $row->quiz_id ) : 0;
+
+ $answer_rows[] = array(
+ intval( $result_id ), // result_id %d
+ intval( $quiz_id ), // quiz_id %d
+ intval( $question_value['id'] ), // question_id %d
+ $question_title, // question_title %s
+ $question_description, // question_description %s (LONGTEXT)
+ $question_comment, // question_comment %s
+ $question_type, // question_type %s
+ $answer_type, // answer_type %s
+ maybe_serialize( $correct_answer_to_store ), // correct_answer %s
+ maybe_serialize( $user_answer_to_store ), // user_answer %s
+ $user_answer_comma, // user_answer_comma %s
+ $correct_answer_comma, // correct_answer_comma %s
+ floatval( $points ), // points %f
+ intval( $correct ), // correct %d
+ $category, // category %s
+ $multicategories, // multicategories %s
+ $other_settings_serialized, // other_settings %s
+ );
+ }
+
+ if ( ! empty( $answer_rows ) ) {
+ $placeholders = array();
+ $params = array();
+
+ foreach ( $answer_rows as $values ) {
+ // match the order from $answer_rows above
+ $placeholders[] = '( %d, %d, %d, %s, %s, %s, %s, %s, %s, %s, %s, %s, %f, %d, %s, %s, %s )';
+ $params = array_merge( $params, $values );
+ }
+
+ $sql = "INSERT INTO {$results_questions}
+ ( result_id, quiz_id, question_id, question_title, question_description, question_comment,
+ question_type, answer_type, correct_answer, user_answer, user_answer_comma, correct_answer_comma,
+ points, correct, category, multicategories