Plugin Directory

Changeset 3372406


Ignore:
Timestamp:
10/03/2025 02:06:59 PM (5 months ago)
Author:
expresstech
Message:

10.2.8 to trunk

Location:
quiz-master-next/trunk
Files:
1 added
10 edited

Legend:

Unmodified
Added
Removed
  • quiz-master-next/trunk/css/qsm-admin-question.css

    r3317056 r3372406  
    8989    color: #dc3232;
    9090}
    91 .page-header .edit-page-button {
    92     margin: 0 10px;
     91.page-header .page-header-buttons {
     92    display: flex;
     93    min-width: 246px;
     94    justify-content: space-between;
     95}
     96.page-header-icons{
     97    display: flex;
     98    align-items: center;
     99    flex-wrap: wrap;
     100    border-radius: 5px;
     101    border: 1px solid #8a8a8a;
     102    justify-content: space-around;
     103    min-height: 28px;
     104}
     105.page-header-icons a {
     106    padding: 0 6px 4px 6px;
     107    color: #03396e;
     108    cursor: pointer;
     109}
     110.page-header-icons a:not(:last-child) {
     111    border-right: 1px solid #d6d6d6;
     112}
     113.page-header-icons a:first-child {
     114    border-radius: 5px 0 0 5px;
     115}
     116.page-header-icons a:last-child {
     117    border-radius: 0 5px 5px 0;
     118}
     119.page-header-icons a:hover {
     120    outline: none;
     121    box-shadow: none;
     122    background: #eee;
    93123}
    94124.page-header .edit-page-button span {
  • quiz-master-next/trunk/js/qsm-admin.js

    r3336048 r3372406  
    28582858                    });
    28592859                },
     2860                shuffleQuestions: function (pageID) {
     2861                    let $targetPage = jQuery('.page[data-page-id="' + pageID + '"]');
     2862                    let $questions = $targetPage.children('.question');
     2863                    if ($questions.length <= 1) {
     2864                        return;
     2865                    }
     2866                    if ($targetPage.find('.qsm-shuffle-loader').length > 0) {
     2867                        return;
     2868                    }
     2869                    jQuery('.qsm-admin-randomize-page-questions[data-page-id="' + pageID + '"]').prop('disabled', true);
     2870                    let allQuestions = [];
     2871                    $questions.each(function() {
     2872                        allQuestions.push(jQuery(this).detach());
     2873                    });
     2874                    let $loader = jQuery('<div style="text-align: center; padding: 30px; margin: 20px 0;"><div class="qsm-spinner-loader"></div></div>');
     2875                    $targetPage.append($loader);
     2876                    for (let i = allQuestions.length - 1; i > 0; i--) {
     2877                        let j = crypto.getRandomValues(new Uint32Array(1))[0] % (i + 1);
     2878                        let temp = allQuestions[i];
     2879                        allQuestions[i] = allQuestions[j];
     2880                        allQuestions[j] = temp;
     2881                    }
     2882                    setTimeout(function() {
     2883                        $loader.fadeOut(200, function() {
     2884                            $loader.remove();
     2885                            QSMQuestion.handleQuestions($targetPage, allQuestions, pageID);
     2886                        });
     2887                    }, 1000);
     2888                },
     2889                handleQuestions: function($targetPage, allQuestions, pageID) {
     2890                    let questionIndex = 0;
     2891                    function addNextQuestion() {
     2892                        if (questionIndex < allQuestions.length) {
     2893                            let $question = allQuestions[questionIndex];
     2894                            $question.hide().appendTo($targetPage);
     2895                            $question.fadeIn(300);
     2896                            questionIndex++;
     2897                            setTimeout(addNextQuestion, 500);
     2898                        } else {
     2899                            QSMQuestion.finalizeQuestionShuffle(pageID);
     2900                        }
     2901                    }
     2902                    addNextQuestion();
     2903                },
     2904                finalizeQuestionShuffle: function(pageID) {
     2905                    QSMQuestion.updateQuestionModelsForPage(pageID);
     2906                    QSMQuestion.countTotal();
     2907                    QSMQuestion.savePages();
     2908                    jQuery('.qsm-admin-randomize-page-questions[data-page-id="' + pageID + '"]').prop('disabled', false);
     2909                    QSMAdmin.displayAlert(qsm_admin_messages.question_shuffle, 'success');
     2910                },
     2911                updateQuestionModelsForPage: function(pageID) {
     2912                    jQuery('.page[data-page-id="' + pageID + '"]').each(function() {
     2913                        let $page = jQuery(this);
     2914                        let pageId = $page.data('page-id');
     2915                       
     2916                        $page.children('.question').each(function() {
     2917                            let questionId = jQuery(this).data('question-id');
     2918                            let model = QSMQuestion.questions.get(questionId);
     2919                            if (model) {
     2920                                model.set('page', pageId - 1);
     2921                            }
     2922                        });
     2923                    });
     2924                },
    28602925                savePages: function () {
    28612926                    var pages = [];
     
    39213986                });
    39223987
     3988                $(document).on('click', '.qsm-admin-randomize-page-questions', function () {
     3989                    let pageId = $(this).data('page-id');
     3990                    QSMQuestion.shuffleQuestions(pageId);
     3991                });
     3992
    39233993                $(document).on('click', '.qsm-admin-select-question-input', function () {
    39243994                    if (!$(this).prop('checked')) {
  • quiz-master-next/trunk/js/qsm-quiz.js

    r3357783 r3372406  
    20882088jQuery(document).keydown(function(event) {
    20892089    if (jQuery('.qsm-quiz-container.qsm-recently-active').length) {
    2090         jQuery(document).trigger('qsm_keyboard_quiz_action_start', event);
     2090        let qsm_keypress_action = { allowed: true };
     2091        jQuery(document).trigger('qsm_keyboard_quiz_action_start', [event, qsm_keypress_action]);
     2092        if(qsm_keypress_action.allowed === false){
     2093            return;
     2094        }
    20912095        if (jQuery(event.target).is('input')) {
    20922096            // Check if the parent div has the class 'qsm_contact_div'
     
    21102114        }
    21112115        if (event.keyCode === 13 && jQuery('textarea:focus').length === 0) {
    2112             jQuery('.qsm-quiz-container.qsm-recently-active').find('.qsm-submit-btn:visible').click();
    2113             jQuery('.qsm-quiz-container.qsm-recently-active').find('.mlw_next:visible').click();
     2116                jQuery('.qsm-quiz-container.qsm-recently-active').find('.qsm-submit-btn:visible').click();
     2117                jQuery('.qsm-quiz-container.qsm-recently-active').find('.mlw_next:visible').click();
    21142118        }
    21152119        if (event.keyCode === 40 && jQuery('.qsm-quiz-container.qsm-recently-active .qsm-question-wrapper.qsm-active-question:visible .qmn_radio_answers:not(.qsm_multiple_grid_answers)').length) {
  • quiz-master-next/trunk/mlw_quizmaster2.php

    r3357783 r3372406  
    33 * Plugin Name: Quiz And Survey Master
    44 * Description: Easily and quickly add quizzes and surveys to your website.
    5  * Version: 10.2.7
     5 * Version: 10.2.8
    66 * Author: ExpressTech
    77 * Author URI: https://quizandsurveymaster.com/
     
    4444     * @since 4.0.0
    4545     */
    46     public $version = '10.2.7';
     46    public $version = '10.2.8';
    4747
    4848    /**
     
    616616            'warning_icon'               => esc_url(QSM_PLUGIN_URL . 'assets/warning-message.png'),
    617617            'info_icon'                  => esc_url(QSM_PLUGIN_URL . 'assets/info-message.png'),
     618            'question_shuffle'           => __('Question shuffled successfully!', 'quiz-master-next'),
    618619        );
    619620        $qsm_admin_messages = apply_filters( 'qsm_admin_messages_after', $qsm_admin_messages );
  • quiz-master-next/trunk/php/admin/options-page-questions-tab.php

    r3357783 r3372406  
    14171417            <div class="page-header">
    14181418                <div><span class="dashicons dashicons-move"></span> <span class="page-number"></span></div>
    1419                 <div>
    1420                     <a href="javascript:void(0)" class="edit-page-button" title="Edit Page"><span class="dashicons dashicons-admin-generic"></span></a>
     1419                <div class="page-header-buttons">
     1420                    <div class="page-header-icons">
     1421                        <a href="javascript:void(0)" class="qsm-admin-randomize-page-questions" title="Shuffle Questions" id="qsm-admin-randomize-page-question-{{data.id}}" data-page-id="{{data.id}}"><img class="qsm-common-svg-image-class" src="<?php echo esc_url(QSM_PLUGIN_URL . 'assets/randomize.svg'); ?>" alt="randomize.svg"/></a>
     1422                        <a href="javascript:void(0)" class="edit-page-button" title="Edit Page"><img class="qsm-common-svg-image-class" src="<?php echo esc_url(QSM_PLUGIN_URL . 'assets/gear.svg'); ?>" alt="gear.svg"/></a>
     1423                    </div>
    14211424                    <a href="javascript:void(0)" class="add-question-bank-button button button-primary qsm-common-button-styles"><?php esc_html_e( 'Import', 'quiz-master-next' ); ?></a>
    14221425                    <a href="javascript:void(0)" class="new-question-button button button-primary qsm-common-button-styles"><?php esc_html_e( 'Add Question', 'quiz-master-next' ); ?></a>
  • quiz-master-next/trunk/php/classes/class-qmn-plugin-helper.php

    r3341668 r3372406  
    11<?php
    2 if ( ! defined( 'ABSPATH' ) ) {
    3     exit;
     2if (! defined('ABSPATH')) {
     3    exit;
    44}
    55include_once ABSPATH . 'wp-admin/includes/plugin.php';
     
    1212 * @since 4.0.0
    1313 */
    14 class QMNPluginHelper {
    15 
    16     /**
    17      * Addon Page tabs array
    18      *
    19      * @var   array
    20      * @since 4.0.0
    21      */
    22     public $addon_tabs = array();
    23 
    24     /**
    25      * Stats Page tabs array
    26      *
    27      * @var   array
    28      * @since 4.0.0
    29      */
    30     public $stats_tabs = array();
    31 
    32     /**
    33      * Admin Results Page tabs array
    34      *
    35      * @var   array
    36      * @since 5.0.0
    37      */
    38     public $admin_results_tabs = array();
    39 
    40     /**
    41      * Results Details Page tabs array
    42      *
    43      * @var   array
    44      * @since 4.1.0
    45      */
    46     public $results_tabs = array();
    47 
    48     /**
    49      * Settings Page tabs array
    50      *
    51      * @var   array
    52      * @since 4.0.0
    53      */
    54     public $settings_tabs = array();
    55 
    56     /**
    57      * Question types array
    58      *
    59      * @var   array
    60      * @since 4.0.0
    61      */
    62     public $question_types = array();
    63 
    64     /**
    65      * Template array
    66      *
    67      * @var   array
    68      * @since 4.5.0
    69      */
    70     public $quiz_templates = array();
    71 
    72     /**
    73      * Main Construct Function
    74      *
    75      * Call functions within class
    76      *
    77      * @since  4.0.0
    78      * @return void
    79      */
    80     public function __construct() {
    81         add_action( 'wp_ajax_qmn_question_type_change', array( $this, 'get_question_type_edit_content' ) );
    82         add_action( 'admin_init', array( $this, 'qsm_add_default_translations' ), 9999 );
    83         add_action( 'qsm_saved_question', array( $this, 'qsm_add_question_translations' ), 10, 2 );
    84         add_action( 'qsm_saved_text_message', array( $this, 'qsm_add_text_message_translations' ), 10, 3 );
    85         add_action( 'qsm_saved_quiz_settings', array( $this, 'qsm_add_quiz_settings_translations' ), 10, 3 );
    86 
    87         add_action( 'qsm_register_language_support', array( $this, 'qsm_register_language_support' ), 10, 3 );
    88         add_filter( 'qsm_language_support', array( $this, 'qsm_language_support' ), 10, 3 );
    89         add_action( 'wp_ajax_qsm_validate_result_submission', array( $this, 'qsm_validate_result_submission' ), 10, 3 );
    90         add_action( 'wp_ajax_nopriv_qsm_validate_result_submission', array( $this, 'qsm_validate_result_submission' ), 10, 3 );
    91     }
    92 
    93     public function qsm_validate_result_submission() {
    94         global $wpdb, $mlwQuizMasterNext;
    95 
    96         $user_email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
    97         $quiz_id = isset( $_POST['quiz_id'] ) ? intval( wp_unslash( $_POST['quiz_id'] ) ) : 0;
    98         $total_user_tries = isset( $_POST['total_user_tries'] ) ? intval( wp_unslash( $_POST['total_user_tries'] ) ) : 0;
    99         $mlw_qmn_user_try_count = 0;
    100 
    101         if ( ! empty( $user_email ) && is_email( $user_email ) ) {
    102             $mlw_qmn_user_try_count = $wpdb->get_var(
    103                 $wpdb->prepare(
    104                     "SELECT COUNT(*) FROM {$wpdb->prefix}mlw_results WHERE email = %s AND deleted = 0 AND quiz_id = %d",
    105                     $user_email,
    106                     $quiz_id
    107                 )
    108             );
    109         }
    110         if ( $mlw_qmn_user_try_count < $total_user_tries ) {
    111             wp_send_json_success();
    112         } else {
    113             wp_send_json_error();
    114         }
    115         wp_die();
    116     }
    117 
    118     /**
    119      * Calls all class functions to check if quiz is setup properly
    120      *
    121      * @param  int $quiz_id The ID of the quiz or survey to load.
    122      * @return array An array which contains boolean result of has_proper_quiz, message and/or qmn_quiz_options
    123      */
    124     public function has_proper_quiz( $quiz_id ) {
    125         if ( empty( $quiz_id ) ) {
    126             return array(
    127                 'res'     => false,
    128                 'message' => __( 'Empty Quiz ID.', 'quiz-master-next' ),
    129             );
    130         }
    131 
    132         $quiz_id = intval( $quiz_id );
    133 
    134         // Tries to load quiz name to ensure this is a valid ID.
    135         global $mlwQuizMasterNext, $qmn_allowed_visit, $qmn_json_data;
    136         $qmn_json_data     = array();
    137         $qmn_allowed_visit = true;
    138         if ( false === $this->prepare_quiz( $quiz_id ) ) {
    139             return array(
    140                 'res'     => false,
    141                 'message' => __( 'It appears that this quiz is not set up correctly.', 'quiz-master-next' ),
    142             );
    143         }
    144 
    145         $has_result_id = ( ! isset( $_GET['result_id'] ) || '' === $_GET['result_id'] );
    146 
    147         if ( $has_result_id ) {
    148             global $mlw_qmn_quiz;
    149             $mlw_qmn_quiz = $quiz_id;
    150         }
    151 
    152         $qmn_quiz_options = $mlwQuizMasterNext->quiz_settings->get_quiz_options();
    153 
    154         if ( $has_result_id ) {
    155             /**
    156              * Filter Quiz Options before Quiz Display
    157              */
    158             $qmn_quiz_options = apply_filters( 'qsm_shortcode_quiz_options', $qmn_quiz_options );
    159         }
    160 
    161         // If quiz options isn't found, stop function.
    162         if ( is_null( $qmn_quiz_options ) || ( ! empty( $qmn_quiz_options->deleted ) && 1 == $qmn_quiz_options->deleted ) ) {
    163             return array(
    164                 'res'     => false,
    165                 'message' => __( 'This quiz is no longer available.', 'quiz-master-next' ),
    166             );
    167         }
    168 
    169         // If quiz options isn't found, stop function.
    170         if ( is_null( $qmn_quiz_options ) || empty( $qmn_quiz_options->quiz_name ) ) {
    171             return array(
    172                 'res'     => false,
    173                 'message' => __( 'It appears that this quiz is not set up correctly.', 'quiz-master-next' ),
    174             );
    175         }
    176 
    177         return array(
    178             'res'              => true,
    179             'message'          => __( 'Quiz is setup properly.', 'quiz-master-next' ),
    180             'qmn_quiz_options' => $qmn_quiz_options,
    181         );
    182     }
    183 
    184     /**
    185      * Calls all class functions to initialize quiz
    186      *
    187      * @param  int $quiz_id The ID of the quiz or survey to load.
    188      * @return bool True or False if ID is valid.
    189      */
    190     public function prepare_quiz( $quiz_id ) {
    191         $quiz_id = intval( $quiz_id );
    192 
    193         // Tries to load quiz name to ensure this is a valid ID.
    194         global $wpdb;
    195         $quiz_name = $wpdb->get_var( $wpdb->prepare( "SELECT quiz_name FROM {$wpdb->prefix}mlw_quizzes WHERE quiz_id=%d LIMIT 1", $quiz_id ) );
    196         if ( is_null( $quiz_name ) ) {
    197             return false;
    198         }
    199 
    200         global $mlwQuizMasterNext;
    201         $mlwQuizMasterNext->quizCreator->set_id( $quiz_id );
    202         $mlwQuizMasterNext->quiz_settings->prepare_quiz( $quiz_id );
    203 
    204         return true;
    205     }
    206 
    207     /**
    208      * Retrieves all quizzes.
    209      *
    210      * @param  bool   $include_deleted If set to true, returned array will include all deleted quizzes
    211      * @param  string $order_by        The column the quizzes should be ordered by
    212      * @param  string $order           whether the $order_by should be ordered as ascending or decending. Can be "ASC" or "DESC"
    213      * @param  arr    $user_role       role of current user
    214      * @param  int    $user_id         Get the quiz based on user id
    215      * @return array All of the quizzes as a numerical array of objects
    216      */
    217     public function get_quizzes( $include_deleted = false, $order_by = 'quiz_id', $order = 'DESC', $user_role = array(), $user_id = '', $limit = '', $offset = '', $where = '' ) {
    218         global $wpdb;
    219 
    220         // Set order direction
    221         $order_direction = 'DESC';
    222         if ( 'ASC' === $order ) {
    223             $order_direction = 'ASC';
    224         }
    225 
    226         // Set field to sort by
    227         switch ( $order_by ) {
    228             case 'last_activity':
    229                 $order_field = 'last_activity';
    230                 break;
    231 
    232             case 'quiz_views':
    233                 $order_field = 'quiz_views';
    234                 break;
    235 
    236             case 'quiz_taken':
    237                 $order_field = 'quiz_taken';
    238                 break;
    239 
    240             case 'title':
    241                 $order_field = 'quiz_name';
    242                 break;
    243 
    244             default:
    245                 $order_field = 'quiz_id';
    246                 break;
    247         }
    248 
    249         // Should we include deleted?
    250         $delete = 'WHERE deleted=0';
    251         if ( '' !== $where ) {
    252             $delete = $delete . ' AND ' . $where;
    253         }
    254         if ( $include_deleted ) {
    255             $delete = '';
    256         }
    257         $user_str = '';
    258         if ( in_array( 'author', (array) $user_role, true ) ) {
    259             if ( $user_id && '' === $delete ) {
    260                 $user_str = "WHERE quiz_author_id = '$user_id'";
    261             } elseif ( $user_id && '' !== $delete ) {
    262                 $user_str = " AND quiz_author_id = '$user_id'";
    263             }
    264         }
    265         if ( '' !== $where && '' !== $user_str ) {
    266             $user_str = $user_str . ' AND ' . $where;
    267         }
    268         $where_str = '';
    269         if ( '' === $user_str && '' === $delete && '' !== $where ) {
    270             $where_str = "WHERE $where";
    271         }
    272         if ( '' !== $limit ) {
    273             $limit = ' limit ' . $offset . ', ' . $limit;
    274         }
    275         // Get quizzes and return them
    276         $delete  = apply_filters( 'quiz_query_delete_clause', $delete );
    277         $quizzes = $wpdb->get_results( stripslashes( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}mlw_quizzes %1s %2s %3s ORDER BY %4s %5s %6s", $delete, $user_str, $where_str, $order_field, $order_direction, $limit ) ) );
    278         return $quizzes;
    279     }
    280 
    281     /**
    282      * Registers a quiz setting
    283      *
    284      * @since 5.0.0
    285      * @param array $field_array An array of the components for the settings field
    286      */
    287     public function register_quiz_setting( $field_array, $section = 'quiz_options' ) {
    288         global $mlwQuizMasterNext;
    289         $mlwQuizMasterNext->quiz_settings->register_setting( $field_array, $section );
    290     }
    291 
    292     /**
    293      * Retrieves a setting value from a section based on name of section and setting
    294      *
    295      * @since  5.0.0
    296      * @param  string $section The name of the section the setting is registered in
    297      * @param  string $setting The name of the setting whose value we need to retrieve
    298      * @param  mixed  $default What we need to return if no setting exists with given $setting
    299      * @return $mixed Value set for $setting or $default if setting does not exist
    300      */
    301     public function get_section_setting( $section, $setting, $default = false ) {
    302         global $mlwQuizMasterNext;
    303         return apply_filters( 'qsm_section_setting_text', $mlwQuizMasterNext->quiz_settings->get_section_setting( $section, $setting, $default ) );
    304     }
    305 
    306     /**
    307      * Retrieves setting value based on name of setting
    308      *
    309      * @since  4.0.0
    310      * @param  string $setting The name of the setting whose value we need to retrieve
    311      * @param  mixed  $default What we need to return if no setting exists with given $setting
    312      * @return $mixed Value set for $setting or $default if setting does not exist
    313      */
    314     public function get_quiz_setting( $setting, $default = false, $caller = '' ) {
    315         global $mlwQuizMasterNext;
    316         if ( ( 'pages' == $setting || 'qpages' == $setting ) && empty( $caller ) ) {
    317             $pages = $mlwQuizMasterNext->quiz_settings->get_setting( $setting, $default );
    318             $temp_pages = array();
    319             foreach ( $pages as $index => $page ) {
    320                 $page_should_display = array();
    321                 $page = 'qpages' == $setting ? $page['questions'] : $page;
    322                 foreach ( $page as $key => $question_id ) {
    323                     $isPublished = $mlwQuizMasterNext->pluginHelper->get_question_setting( $question_id, 'isPublished' );
    324                     if ( '' == $isPublished || ( '' != $isPublished && 1 === intval( $isPublished ) ) ) {
    325                         $page_should_display[]  = true;
    326                     } elseif ( '' != $isPublished && 0 === intval( $isPublished ) ) {
    327                         $page_should_display[] = false;
    328                         unset( $page[ $key ] );
    329                     }
    330                 }
    331                 if ( in_array( true, $page_should_display, true ) ) {
    332                     if ( 'qpages' == $setting ) {
    333                         $pages[ $index ]['questions'] = $page;
    334                         $temp_pages[] = $pages[ $index ];
    335                     } else {
    336                         $temp_pages[] = $page;
    337                     }
    338                 }
    339             }
    340             return $temp_pages;
    341         }
    342         return $mlwQuizMasterNext->quiz_settings->get_setting( $setting, $default );
    343     }
    344 
    345     /**
    346      * Updates a settings value, adding it if it didn't already exist
    347      *
    348      * @since  4.0.0
    349      * @param  string $setting The name of the setting whose value we need to retrieve
    350      * @param  mixed  $value   The value that needs to be stored for the setting
    351      * @return bool True if successful or false if fails
    352      */
    353     public function update_quiz_setting( $setting, $value ) {
    354         global $mlwQuizMasterNext;
    355         return $mlwQuizMasterNext->quiz_settings->update_setting( $setting, $value );
    356     }
    357 
    358     /**
    359      * Outputs the section of input fields
    360      *
    361      * @since 5.0.0
    362      * @since 7.0 Added new parameter settings_fields for default setting
    363      * @param string $section The section that the settings were registered with
    364      */
    365     public function generate_settings_section( $section = 'quiz_options', $settings_fields = array() ) {
    366         global $mlwQuizMasterNext;
    367         if ( empty( $settings_fields ) ) {
    368             $settings_fields = $mlwQuizMasterNext->quiz_settings->load_setting_fields( $section );
    369         }
    370         QSM_Fields::generate_section( $settings_fields, $section );
    371     }
    372 
    373     /**
    374      * Registers Quiz Templates
    375      *
    376      * @since 4.5.0
    377      * @param $name      String of the name of the template
    378      * @param $file_path String of the path to the css file
    379      */
    380     public function register_quiz_template( $name, $file_path ) {
    381         $slug                          = strtolower( str_replace( ' ', '-', $name ) );
    382         $this->quiz_templates[ $slug ] = array(
    383             'name' => $name,
    384             'path' => $file_path,
    385         );
    386     }
    387 
    388     /**
    389      * Returns Template Array
    390      *
    391      * @since  4.5.0
    392      * @param  $name String of the name of the template. If left empty, will return all templates
    393      * @return array The array of quiz templates
    394      */
    395     public function get_quiz_templates( $slug = null ) {
    396         if ( is_null( $slug ) ) {
    397             return $this->quiz_templates;
    398         } elseif ( isset( $this->quiz_templates[ $slug ] ) ) {
    399             return $this->quiz_templates[ $slug ];
    400         } else {
    401             return false;
    402         }
    403     }
    404 
    405     /**
    406      * Register Question Types
    407      *
    408      * Adds a question type to the question type array using the parameters given
    409      *
    410      * @since  4.0.0
    411      * @param  string $name             The name of the Question Type which will be shown when selecting type
    412      * @param  string $display_function The name of the function to call when displaying the question
    413      * @param  bool   $graded           Tells the plugin if this question is graded or not. This will affect scoring.
    414      * @param  string $review_function  The name of the function to call when scoring the question
    415      * @param  string $slug             The slug of the question type to be stored with question in database
    416      * @param  array  $options          The options for show and hide question validation settings and answer types
    417      * @return void
    418      */
    419     public function register_question_type( $name, $display_function, $graded, $review_function = null, $edit_args = null, $save_edit_function = null, $slug = null, $options = array() ) {
    420         if ( is_null( $slug ) ) {
    421             $slug = strtolower( str_replace( ' ', '-', $name ) );
    422         } else {
    423             $slug = strtolower( str_replace( ' ', '-', $slug ) );
    424         }
    425         if ( is_null( $edit_args ) || ! is_array( $edit_args ) ) {
    426             $validated_edit_function = array(
    427                 'inputs'       => array(
    428                     'question',
    429                     'answer',
    430                     'hint',
    431                     'correct_info',
    432                     'comments',
    433                     'category',
    434                     'required',
    435                 ),
    436                 'information'  => '',
    437                 'extra_inputs' => array(),
    438                 'function'     => '',
    439             );
    440         } else {
    441             $validated_edit_function = array(
    442                 'inputs'       => $edit_args['inputs'],
    443                 'information'  => $edit_args['information'],
    444                 'extra_inputs' => $edit_args['extra_inputs'],
    445                 'function'     => $edit_args['function'],
    446             );
    447         }
    448         if ( is_null( $save_edit_function ) ) {
    449             $save_edit_function = '';
    450         }
    451         $new_type                      = array(
    452             'name'    => $name,
    453             'display' => $display_function,
    454             'review'  => $review_function,
    455             'graded'  => $graded,
    456             'edit'    => $validated_edit_function,
    457             'save'    => $save_edit_function,
    458             'slug'    => $slug,
    459             'options' => $options,
    460         );
    461         $new_type = apply_filters( 'register_question_type_new_type',$new_type );
    462         $this->question_types[ $slug ] = $new_type;
    463     }
    464 
    465     /**
    466      * Retrieves List Of Question Types
    467      *
    468      * retrieves a list of the slugs and names of the question types
    469      *
    470      * @since  4.0.0
    471      * @return array An array which contains the slug and name of question types that have been registered
    472      */
    473     public function get_question_type_options() {
    474         $type_array = array();
    475         foreach ( $this->question_types as $type ) {
    476             $type_array[] = array(
    477                 'slug'    => $type['slug'],
    478                 'name'    => $type['name'],
    479                 'options' => $type['options'],
    480             );
    481         }
    482         return $type_array;
    483     }
    484 
    485     /**
    486      *
    487      */
    488     public function set_question_type_meta( $type_id, $meta_key, $meta_value ) {
    489 
    490         $this->question_types[ $type_id ][ $meta_key ] = $meta_value;
    491 
    492     }
    493 
    494     public function get_question_type_edit_fields() {
    495         $type_array = array();
    496         foreach ( $this->question_types as $type ) {
    497             $type_array[ $type['slug'] ] = $type['edit'];
    498         }
    499         return $type_array;
    500     }
    501 
    502     /**
    503      * Displays A Question
    504      *
    505      * Retrieves the question types display function and creates the HTML for the question
    506      *
    507      * @since  4.0.0
    508      * @param  string $slug         The slug of the question type that the question is
    509      * @param  int    $question_id  The id of the question
    510      * @param  array  $quiz_options An array of the columns of the quiz row from the database
    511      * @return string The HTML for the question
    512      */
    513     public function display_question( $slug, $question_id, $quiz_options ) {
    514         global $wpdb;
    515         global $qmn_total_questions, $qmn_all_questions_count;
    516         $question = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM ' . $wpdb->prefix . 'mlw_questions WHERE question_id=%d', intval( $question_id ) ) );
    517         $answers  = array();
    518         if ( is_serialized( $question->answer_array ) && is_array( maybe_unserialize( $question->answer_array ) ) ) {
    519             $answers = maybe_unserialize( $question->answer_array );
    520         } else {
    521             $mlw_answer_array_correct                                  = array( 0, 0, 0, 0, 0, 0 );
    522             $mlw_answer_array_correct[ $question->correct_answer - 1 ] = 1;
    523             $answers = array(
    524                 array( $question->answer_one, $question->answer_one_points, $mlw_answer_array_correct[0] ),
    525                 array( $question->answer_two, $question->answer_two_points, $mlw_answer_array_correct[1] ),
    526                 array( $question->answer_three, $question->answer_three_points, $mlw_answer_array_correct[2] ),
    527                 array( $question->answer_four, $question->answer_four_points, $mlw_answer_array_correct[3] ),
    528                 array( $question->answer_five, $question->answer_five_points, $mlw_answer_array_correct[4] ),
    529                 array( $question->answer_six, $question->answer_six_points, $mlw_answer_array_correct[5] ),
    530             );
    531         }
    532         $answers_original = $answers;
    533         if ( 2 === intval( $quiz_options->randomness_order ) || 3 === intval( $quiz_options->randomness_order ) ) {
    534             $answers = self::qsm_shuffle_assoc( $answers );
    535             global $quiz_answer_random_ids;
    536             $answer_ids = array_keys($answers);
    537             $quiz_answer_random_ids[ $question_id ] = $answer_ids;
    538         }
    539 
    540         // convert answer array into key value pair
    541         $answers_kvpair = array();
    542         foreach ( $answers as $answer_item ) {
    543             $key                    = array_search( $answer_item, $answers_original, true );
    544             $answers_kvpair[ $key ] = $answer_item;
    545         }
    546         unset( $answer_item );
    547         $answers = $answers_kvpair;
    548 
    549         /**
    550          * Filter Answers of specific question before display
    551          */
    552         $answers = apply_filters( 'qsm_single_question_answers', $answers, $question, $quiz_options );
    553         foreach ( $this->question_types as $type ) {
    554             if ( strtolower( str_replace( ' ', '-', $slug ) ) === $type['slug'] && ! empty( $type['display'] ) && function_exists( $type['display'] ) ) {
    555                 $qmn_all_questions_count += 1;
    556                 if ( $type['graded'] ) {
    557                     $qmn_total_questions += 1;
    558                     if ( 1 === intval( $quiz_options->question_numbering ) ) { ?>
    559                         <span class='mlw_qmn_question_number'><?php echo esc_html( $qmn_total_questions ); ?>.&nbsp;</span>
    560                         <?php
    561                     }
    562                 }
    563                 if ( $quiz_options->show_category_on_front ) {
    564                     $categories = QSM_Questions::get_question_categories( $question_id );
    565                     if ( ! empty( $categories['category_name'] ) ) {
    566                         $cat_name = implode( ',', $categories['category_name'] );
    567                         ?>
    568                         <div class="quiz-cat"><?php echo esc_html( $cat_name ); ?></div>
    569                         <?php
    570                     }
    571                 }
    572 
    573                 call_user_func( $type['display'], intval( $question_id ), $question->question_name, $answers );
    574                 do_action( 'qsm_after_question', $question );
    575             }
    576         }
    577     }
    578 
    579     public function get_questions_count( $quiz_id = 0 ) {
    580         global $wpdb;
    581         $quiz_id = intval( $quiz_id );
    582         $count   = 0;
    583         if ( empty( $quiz_id ) || 0 == $quiz_id ) {
    584             return $count;
    585         }
    586 
    587         $quiz_settings = $wpdb->get_var( $wpdb->prepare( "SELECT `quiz_settings` FROM `{$wpdb->prefix}mlw_quizzes` WHERE `quiz_id`=%d", $quiz_id ) );
    588         if ( ! empty( $quiz_settings ) ) {
    589             $settings    = maybe_unserialize( $quiz_settings );
    590             $pages       = isset( $settings['pages'] ) ? maybe_unserialize( $settings['pages'] ) : array();
    591             if ( ! empty( $pages ) ) {
    592                 foreach ( $pages as $page ) {
    593                     $count += count( $page );
    594                 }
    595             }
    596         }
    597         return $count;
    598     }
    599 
    600     public function get_questions_ids( $quiz_id = 0 ) {
    601         global $wpdb;
    602         $quiz_id = intval( $quiz_id );
    603         $ids   = array();
    604         if ( empty( $quiz_id ) || 0 == $quiz_id ) {
    605             return $ids;
    606         }
    607 
    608         $quiz_settings = $wpdb->get_var( $wpdb->prepare( "SELECT `quiz_settings` FROM `{$wpdb->prefix}mlw_quizzes` WHERE `quiz_id`=%d", $quiz_id ) );
    609         if ( ! empty( $quiz_settings ) ) {
    610             $settings    = maybe_unserialize( $quiz_settings );
    611             $pages       = isset( $settings['pages'] ) ? maybe_unserialize( $settings['pages'] ) : array();
    612             if ( ! empty( $pages ) ) {
    613                 foreach ( $pages as $page ) {
    614                     $ids = array_merge($ids, $page );
    615                 }
    616             }
    617         }
    618         return $ids;
    619     }
    620 
    621     /**
    622      * Shuffle assoc array
    623      *
    624      * @since  7.3.11
    625      * @param  array $list An array
    626      * @return array
    627      */
    628     public static function qsm_shuffle_assoc( $list ) {
    629         if ( ! is_array( $list ) ) {
    630             return $list;
    631         }
    632         $keys    = array_keys( $list );
    633         shuffle( $keys );
    634         $random  = array();
    635         foreach ( $keys as $key ) {
    636             $random[ $key ] = $list[ $key ];
    637         }
    638         return $random;
    639     }
    640 
    641     /**
    642      * Find the key of the first occurrence of a substring in an array
    643      */
    644     public static function qsm_stripos_array( $str, array $arr ) {
    645         if ( is_array( $arr ) ) {
    646             foreach ( $arr as $a ) {
    647                 if ( stripos( $str, $a ) !== false ) {
    648                     return $a;
    649                 }
    650             }
    651         }
    652         return false;
    653     }
    654 
    655     /**
    656      * Default strings
    657      * Translation not added in empty string due to warning ( WordPress.WP.I18n.NoEmptyStrings )
    658      */
    659     public static function get_default_texts() {
    660         $defaults = array(
    661             'message_before'                   => __('Welcome to your %QUIZ_NAME%', 'quiz-master-next'),
    662             'message_comment'                  => __('Please fill in the comment box below.', 'quiz-master-next'),
    663             'message_end_template'             => '',
    664             'question_answer_template'         => __('%QUESTION%<br />%USER_ANSWERS_DEFAULT%<br/>%CORRECT_ANSWER_INFO%', 'quiz-master-next'),
    665             'question_answer_email_template'   => __('%QUESTION%<br />Answer Provided: %USER_ANSWER%<br/>Correct Answer: %CORRECT_ANSWER%<br/>Comments Entered: %USER_COMMENTS%', 'quiz-master-next'),
    666             'total_user_tries_text'            => __('You have utilized all of your attempts to pass this quiz.', 'quiz-master-next'),
    667             'require_log_in_text'              => __('This quiz is for logged in users only.', 'quiz-master-next'),
    668             'limit_total_entries_text'         => __('Unfortunately, this quiz has a limited amount of entries it can recieve and has already reached that limit.', 'quiz-master-next'),
    669             'scheduled_timeframe_text'         => '',
    670             'twitter_sharing_text'             => __('I just scored %CORRECT_SCORE%% on %QUIZ_NAME%!', 'quiz-master-next'),
    671             'facebook_sharing_text'            => __('I just scored %CORRECT_SCORE%% on %QUIZ_NAME%!', 'quiz-master-next'),
    672             'linkedin_sharing_text'            => __('I just scored %CORRECT_SCORE%% on %QUIZ_NAME%!', 'quiz-master-next'),
    673             'submit_button_text'               => __('Submit', 'quiz-master-next'),
    674             'retake_quiz_button_text'          => __('Retake Quiz', 'quiz-master-next'),
    675             'previous_button_text'             => __('Previous', 'quiz-master-next'),
    676             'next_button_text'                 => __('Next', 'quiz-master-next'),
    677             'deselect_answer_text'             => __('Deselect Answer', 'quiz-master-next'),
    678             'empty_error_text'                 => __('Please complete all required fields!', 'quiz-master-next'),
    679             'email_error_text'                 => __('Not a valid e-mail address!', 'quiz-master-next'),
    680             'number_error_text'                => __('This field must be a number!', 'quiz-master-next'),
    681             'incorrect_error_text'             => __('The entered text is not correct!', 'quiz-master-next'),
    682             'url_error_text'                   => __('The entered URL is not valid!', 'quiz-master-next'),
    683             'minlength_error_text'             => __('Required atleast %minlength% characters.', 'quiz-master-next'),
    684             'maxlength_error_text'             => __('Minimum %maxlength% characters allowed.', 'quiz-master-next'),
    685             'comment_field_text'               => __('Comments', 'quiz-master-next'),
    686             'hint_text'                        => __('Hint', 'quiz-master-next'),
    687             'quick_result_correct_answer_text' => __('Correct! You have selected correct answer.', 'quiz-master-next'),
    688             'quick_result_wrong_answer_text'   => __('Wrong! You have selected wrong answer.', 'quiz-master-next'),
    689             'quiz_processing_message'          => '',
    690             'quiz_limit_choice'                => __('Limit of choice is reached.', 'quiz-master-next'),
    691             'name_field_text'                  => __('Name', 'quiz-master-next'),
    692             'business_field_text'              => __('Business', 'quiz-master-next'),
    693             'email_field_text'                 => __('Email', 'quiz-master-next'),
    694             'phone_field_text'                 => __('Phone Number', 'quiz-master-next'),
    695             'start_quiz_text'                  => __('Start Quiz', 'quiz-master-next'),
    696             'start_survey_text'                => __('Start Survey', 'quiz-master-next'),
    697         );
    698         return apply_filters( 'qsm_default_texts', $defaults );
    699     }
    700 
    701     /**
    702      * Register string in WPML for translation
    703      */
    704     public static function qsm_register_language_support( $translation_text = '', $translation_slug = '', $domain = 'QSM Meta' ) {
    705         if ( ! empty( $translation_text ) && is_plugin_active( 'wpml-string-translation/plugin.php' ) ) {
    706             $translation_slug = sanitize_title( $translation_slug );
    707             /**
    708              * Register the string for translation
    709              */
    710             do_action( 'wpml_register_single_string', $domain, $translation_slug, $translation_text );
    711         }
    712     }
    713 
    714     /**
    715      * Translate string before display
    716      */
    717     public static function qsm_language_support( $translation_text = '', $translation_slug = '', $domain = 'QSM Meta' ) {
    718 
    719         /**
    720          * Check if WPML String Translation plugin is activated.
    721          */
    722         if ( ! empty( $translation_text ) && is_plugin_active( 'wpml-string-translation/plugin.php' ) ) {
    723             /**
    724              * Decode HTML Special characters.
    725              */
    726             $translation_text = wp_kses_post( htmlspecialchars_decode( $translation_text, ENT_QUOTES ) );
    727             $translation_slug    = sanitize_title( $translation_slug );
    728             $new_text            = apply_filters( 'wpml_translate_single_string', $translation_text, $domain, $translation_slug );
    729             if ( 'QSM Answers' === $domain && $new_text == $translation_text ) {
    730                 if ( 0 === strpos($translation_slug, 'caption-') ) {
    731                     $translation_slug    = sanitize_title( 'caption-' . $translation_text );
    732                 }else {
    733                     $translation_slug    = sanitize_title( 'answer-' . $translation_text );
    734                 }
    735                 $new_text            = apply_filters( 'wpml_translate_single_string', $translation_text, $domain, $translation_slug );
    736             }
    737             $new_text            = wp_kses_post( htmlspecialchars_decode( $new_text, ENT_QUOTES ) );
    738             /**
    739              * Return translation for non-default strings.
    740              */
    741             if ( "QSM Meta" != $domain ) {
    742                 return $new_text;
    743             }
    744             /**
    745              * Check if translation exist.
    746              */
    747             if ( 0 !== strcasecmp( $translation_text, $new_text ) ) {
    748                 return $new_text;
    749             }
    750             /**
    751              * Check if translation exist for default string.
    752              */
    753             $default_texts   = self::get_default_texts();
    754             $default_key     = self::qsm_stripos_array( $translation_slug, array_keys( $default_texts ) );
    755             if ( false !== $default_key && 0 === strcasecmp( $translation_text, $default_texts[ $default_key ] ) ) {
    756                 return apply_filters( 'wpml_translate_single_string', $translation_text, 'QSM Defaults', 'quiz_' . $default_key );
    757             }
    758         } elseif ( ! empty( $translation_text ) ) {
    759             $translation_text = wp_kses_post( $translation_text );
    760         }
    761 
    762         return $translation_text;
    763     }
    764 
    765     public function qsm_add_default_translations() {
    766         $default_texts = self::get_default_texts();
    767         if ( empty( $default_texts ) ) {
    768             return;
    769         }
    770         if ( is_plugin_active( 'wpml-string-translation/plugin.php' ) ) {
    771             foreach ( $default_texts as $key => $text ) {
    772                 if ( ! empty( $text ) ) {
    773                     $translation_slug = sanitize_title( 'quiz_' . $key );
    774                     /**
    775                      * Register the string for translation
    776                      */
    777                     do_action( 'wpml_register_single_string', 'QSM Defaults', $translation_slug, $text );
    778                 }
    779             }
    780         }
    781     }
    782 
    783     public function qsm_add_question_translations( $question_id, $question_data ) {
    784         $settings    = isset( $question_data['question_settings'] ) ? maybe_unserialize( $question_data['question_settings'] ) : array();
    785         $hints       = isset( $question_data['hints'] ) ? $question_data['hints'] : '';
    786         $answer_info = isset( $question_data['question_answer_info'] ) ? html_entity_decode( $question_data['question_answer_info'] ) : '';
    787 
    788         $this->qsm_register_language_support( htmlspecialchars_decode( isset($settings['question_title']) ? $settings['question_title'] : '', ENT_QUOTES ), "Question-{$question_id}", "QSM Questions" );
    789         $this->qsm_register_language_support( htmlspecialchars_decode( $question_data['question_name'], ENT_QUOTES ), "question-description-{$question_id}", "QSM Questions" );
    790         $this->qsm_register_language_support( $hints, "hint-{$question_id}" );
    791         $this->qsm_register_language_support( $answer_info, "correctanswerinfo-{$question_id}" );
    792 
    793         $answers = isset( $question_data['answer_array'] ) ? maybe_unserialize( $question_data['answer_array'] ) : array();
    794         if ( ! empty( $answers ) ) {
    795             $answerEditor = isset( $settings['answerEditor'] ) ? $settings['answerEditor'] : 'text';
    796             foreach ( $answers as $key => $ans ) {
    797                 if ( 'image' === $answerEditor ) {
    798                     $caption_text = trim( htmlspecialchars_decode( $ans[3], ENT_QUOTES ) );
    799                     $this->qsm_register_language_support( $caption_text, 'caption-' . $question_id . '-' . $key, 'QSM Answers' );
    800                 } else {
    801                     $answer_text = trim( htmlspecialchars_decode( $ans[0], ENT_QUOTES ) );
    802                     $this->qsm_register_language_support( $answer_text, 'answer-' . $question_id . '-' . $key, 'QSM Answers' );
    803                 }
    804             }
    805         }
    806     }
    807 
    808     public function qsm_add_text_message_translations( $quiz_id, $text_id, $message ) {
    809         $message = htmlspecialchars_decode( $message, ENT_QUOTES );
    810         $this->qsm_register_language_support( $message, "quiz_{$text_id}-{$quiz_id}" );
    811     }
    812 
    813     public function qsm_add_quiz_settings_translations( $quiz_id, $section, $settings_array ) {
    814         if ( 'quiz_text' == $section && ! empty( $settings_array ) ) {
    815             foreach ( $settings_array as $key => $val ) {
    816                 if ( ! empty( $val ) ) {
    817                     $this->qsm_register_language_support( htmlspecialchars_decode( $val, ENT_QUOTES ), "quiz_{$key}-{$quiz_id}" );
    818                 }
    819             }
    820         }
    821     }
    822 
    823     /**
    824      * Calculates Score For Question
    825      *
    826      * Calculates the score for the question based on the question type
    827      *
    828      * @since  4.0.0
    829      * @param  string $slug        The slug of the question type that the question is
    830      * @param  int    $question_id The id of the question
    831      * @return array An array of the user's score from the question
    832      */
    833     public function display_review( $slug, $question_id ) {
    834         $results_array = array();
    835         global $wpdb;
    836         $question = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM ' . $wpdb->prefix . 'mlw_questions WHERE question_id=%d', intval( $question_id ) ) );
    837         $answers  = maybe_unserialize( $question->answer_array );
    838         if ( empty( $answers ) || ! is_array( $answers ) ) {
    839             $mlw_answer_array_correct                                  = array( 0, 0, 0, 0, 0, 0 );
    840             $mlw_answer_array_correct[ $question->correct_answer - 1 ] = 1;
    841             $answers = array(
    842                 array( $question->answer_one, $question->answer_one_points, $mlw_answer_array_correct[0] ),
    843                 array( $question->answer_two, $question->answer_two_points, $mlw_answer_array_correct[1] ),
    844                 array( $question->answer_three, $question->answer_three_points, $mlw_answer_array_correct[2] ),
    845                 array( $question->answer_four, $question->answer_four_points, $mlw_answer_array_correct[3] ),
    846                 array( $question->answer_five, $question->answer_five_points, $mlw_answer_array_correct[4] ),
    847                 array( $question->answer_six, $question->answer_six_points, $mlw_answer_array_correct[5] ),
    848             );
    849         }
    850         foreach ( $this->question_types as $type ) {
    851             if ( strtolower( str_replace( ' ', '-', $slug ) ) === $type['slug'] ) {
    852                 if ( ! is_null( $type['review'] ) ) {
    853                     $results_array = call_user_func( $type['review'], intval( $question_id ), $question->question_name, $answers );
    854                 } else {
    855                     $results_array = array( 'null_review' => true );
    856                 }
    857             }
    858         }
    859         return $results_array;
    860     }
    861 
    862     /**
    863      * Retrieves A Question Setting
    864      *
    865      * Retrieves a setting stored in the question settings array
    866      *
    867      * @since  4.0.0
    868      * @param  int    $question_id The id of the question
    869      * @param  string $setting     The name of the setting
    870      * @return string The value stored for the setting
    871      */
    872     public function get_question_setting( $question_id, $setting ) {
    873         global $wpdb;
    874         $settings           = $wpdb->get_var( $wpdb->prepare( 'SELECT question_settings FROM ' . $wpdb->prefix . 'mlw_questions WHERE question_id=%d', $question_id ) );
    875         $qmn_settings_array = maybe_unserialize( $settings );
    876 
    877         if ( is_array( $qmn_settings_array ) && isset( $qmn_settings_array[ $setting ] ) ) {
    878             return $qmn_settings_array[ $setting ];
    879         } else {
    880             return '';
    881         }
    882     }
    883 
    884     /**
    885      * Registers Addon Settings Tab
    886      *
    887      * Registers a new tab on the addon settings page
    888      *
    889      * @since  4.0.0
    890      * @param  string $title    The name of the tab
    891      * @param  string $function The function that displays the tab's content
    892      * @return void
    893      */
    894     public function register_addon_settings_tab( $title, $function ) {
    895         $slug               = strtolower( str_replace( ' ', '-', $title ) );
    896         $new_tab            = array(
    897             'title'    => $title,
    898             'function' => $function,
    899             'slug'     => $slug,
    900         );
    901         $this->addon_tabs[] = $new_tab;
    902     }
    903 
    904     /**
    905      * Retrieves Addon Settings Tab Array
    906      *
    907      * Retrieves the array of titles and functions of the registered tabs
    908      *
    909      * @since  4.0.0
    910      * @return array The array of registered tabs
    911      */
    912     public function get_addon_tabs() {
    913         return $this->addon_tabs;
    914     }
    915 
    916     /**
    917      * Registers Stats Tab
    918      *
    919      * Registers a new tab on the stats page
    920      *
    921      * @since  4.3.0
    922      * @param  string $title    The name of the tab
    923      * @param  string $function The function that displays the tab's content
    924      * @return void
    925      */
    926     public function register_stats_settings_tab( $title, $function ) {
    927         $slug               = strtolower( str_replace( ' ', '-', $title ) );
    928         $new_tab            = array(
    929             'title'    => $title,
    930             'function' => $function,
    931             'slug'     => $slug,
    932         );
    933         $this->stats_tabs[] = $new_tab;
    934     }
    935 
    936     /**
    937      * Retrieves Stats Tab Array
    938      *
    939      * Retrieves the array of titles and functions of the registered tabs
    940      *
    941      * @since  4.3.0
    942      * @return array The array of registered tabs
    943      */
    944     public function get_stats_tabs() {
    945         return $this->stats_tabs;
    946     }
    947 
    948     /**
    949      * Registers tabs for the Admin Results page
    950      *
    951      * Registers a new tab on the admin results page
    952      *
    953      * @since  5.0.0
    954      * @param  string $title    The name of the tab
    955      * @param  string $function The function that displays the tab's content
    956      * @return void
    957      */
    958     public function register_admin_results_tab( $title, $function, $priority = 10 ) {
    959         $slug                       = strtolower( str_replace( ' ', '-', $title ) );
    960         $new_tab                    = array(
    961             'title'    => $title,
    962             'function' => $function,
    963             'slug'     => $slug,
    964             'priority' => $priority,
    965         );
    966         $this->admin_results_tabs[] = $new_tab;
    967     }
    968 
    969     /**
    970      * Retrieves Admin Results Tab Array
    971      *
    972      * Retrieves the array of titles and functions for the tabs registered for the admin results page
    973      *
    974      * @since  5.0.0
    975      * @return array The array of registered tabs
    976      */
    977     public function get_admin_results_tabs() {
    978         /**
    979          * Sort tabs by priority
    980          */
    981         array_multisort( array_column($this->admin_results_tabs, 'priority'), SORT_ASC, $this->admin_results_tabs);
    982         return apply_filters( 'qmn_admin_results_tabs', $this->admin_results_tabs );
    983     }
    984 
    985     /**
    986      * Registers Results Tab
    987      *
    988      * Registers a new tab on the results page
    989      *
    990      * @since  4.1.0
    991      * @param  string $title    The name of the tab
    992      * @param  string $function The function that displays the tab's content
    993      * @return void
    994      */
    995     public function register_results_settings_tab( $title, $function ) {
    996         $slug                 = strtolower( str_replace( ' ', '-', $title ) );
    997         $new_tab              = array(
    998             'title'    => $title,
    999             'function' => $function,
    1000             'slug'     => $slug,
    1001         );
    1002         $this->results_tabs[] = $new_tab;
    1003     }
    1004 
    1005     /**
    1006      * Retrieves Results Tab Array
    1007      *
    1008      * Retrieves the array of titles and functions of the registered tabs
    1009      *
    1010      * @since  4.1.0
    1011      * @return array The array of registered tabs
    1012      */
    1013     public function get_results_tabs() {
    1014         return $this->results_tabs;
    1015     }
    1016 
    1017     /**
    1018      * Registers Quiz Settings Tab
    1019      *
    1020      * Registers a new tab on the quiz settings page
    1021      *
    1022      * @since  4.0.0
    1023      * @param  string $title    The name of the tab
    1024      * @param  string $function The function that displays the tab's content
    1025      * @return void
    1026      */
    1027     public function register_quiz_settings_tabs( $title, $function, $slug = '' ) {
    1028         if ( '' === $slug ) {
    1029             $slug = strtolower( str_replace( ' ', '-', $title ) );
    1030         }
    1031         $new_tab               = array(
    1032             'title'    => $title,
    1033             'function' => $function,
    1034             'slug'     => $slug,
    1035         );
    1036         $this->settings_tabs[] = $new_tab;
    1037     }
    1038 
    1039     /**
    1040      * Echos Registered Tabs Title Link
    1041      *
    1042      * Echos the title link of the registered tabs
    1043      *
    1044      * @since  4.0.0
    1045      * @return array The array of registered tabs
    1046      */
    1047     public function get_settings_tabs() {
    1048         return apply_filters( 'qmn_quiz_setting_tabs', $this->settings_tabs );
    1049     }
    1050 
    1051     /**
    1052      * global animatiocv array return
    1053      *
    1054      * @since 4.7.1
    1055      */
    1056     public function quiz_animation_effect() {
    1057 
    1058         return array(
    1059             array(
    1060                 'label' => __( 'bounce', 'quiz-master-next' ),
    1061                 'value' => 'bounce',
    1062             ),
    1063             array(
    1064                 'label' => __( 'flash', 'quiz-master-next' ),
    1065                 'value' => 'flash',
    1066             ),
    1067             array(
    1068                 'label' => __( 'pulse', 'quiz-master-next' ),
    1069                 'value' => 'pulse',
    1070             ),
    1071             array(
    1072                 'label' => __( 'rubberBand', 'quiz-master-next' ),
    1073                 'value' => 'rubberBand',
    1074             ),
    1075             array(
    1076                 'label' => __( 'shake', 'quiz-master-next' ),
    1077                 'value' => 'shake',
    1078             ),
    1079             array(
    1080                 'label' => __( 'swing', 'quiz-master-next' ),
    1081                 'value' => 'swing',
    1082             ),
    1083             array(
    1084                 'label' => __( 'tada', 'quiz-master-next' ),
    1085                 'value' => 'tada',
    1086             ),
    1087             array(
    1088                 'label' => __( 'wobble', 'quiz-master-next' ),
    1089                 'value' => 'wobble',
    1090             ),
    1091             array(
    1092                 'label' => __( 'jello', 'quiz-master-next' ),
    1093                 'value' => 'jello',
    1094             ),
    1095             array(
    1096                 'label' => __( 'heartBeat', 'quiz-master-next' ),
    1097                 'value' => 'heartBeat',
    1098             ),
    1099             array(
    1100                 'label' => __( 'Select Quiz Animation', 'quiz-master-next' ),
    1101                 'value' => '',
    1102             ),
    1103         );
    1104 
    1105     }
    1106 
    1107     /**
    1108      * converts dates into preferred date format
    1109      *
    1110      * @since  7.3.3
    1111      * @param  array $qsm_qna_array The array of results for the quiz
    1112      * @uses   QMNQuizManager:submit_results() submits and displays results
    1113      * @uses   qsm_generate_results_details_tab() generates admin results page
    1114      * @return array $qsm_qna_array date formatted array of results for the quiz
    1115      */
    1116 
    1117     public function convert_to_preferred_date_format( $qsm_qna_array ) {
    1118         global $mlwQuizMasterNext;
    1119         $quiz_options        = $mlwQuizMasterNext->quiz_settings->get_quiz_options();
    1120         $qsm_quiz_settings   = maybe_unserialize( $quiz_options->quiz_settings );
    1121         $qsm_quiz_options    = maybe_unserialize( $qsm_quiz_settings['quiz_options'] );
    1122         $qsm_global_settings = get_option( 'qsm-quiz-settings' );
    1123         // check if preferred date format is set at quiz level or plugin level. Default to WP date format otherwise
    1124         if ( isset( $qsm_quiz_options['preferred_date_format'] ) ) {
    1125             $preferred_date_format = $qsm_quiz_options['preferred_date_format'];
    1126         } elseif ( isset( $qsm_global_settings['preferred_date_format'] ) ) {
    1127             $preferred_date_format = isset( $qsm_global_settings['preferred_date_format'] );
    1128         } else {
    1129             $preferred_date_format = get_option( 'date_format' );
    1130         }
    1131         // filter date format
    1132         $GLOBALS['qsm_date_format'] = apply_filters( 'qms_preferred_date_format', $preferred_date_format );
    1133 
    1134         $qsm_qna_array = $this->convert_contacts_to_preferred_date_format( $qsm_qna_array );
    1135         $qsm_qna_array = $this->convert_answers_to_preferred_date_format( $qsm_qna_array );
    1136         $this->convert_questions_to_preferred_date_format();
    1137 
    1138         return $qsm_qna_array;
    1139     }
    1140 
    1141     /**
    1142      * converts contacts into preferred date format
    1143      *
    1144      * @since  7.3.3
    1145      * @param  array $qsm_qna_array The array of results for the quiz
    1146      * @uses   convert_to_preferred_date_format()
    1147      * @return array $qsm_qna_array date formatted array of results for the quiz
    1148      */
    1149 
    1150     public function convert_contacts_to_preferred_date_format( $qsm_qna_array ) {
    1151 
    1152         $qsm_contact_array = $qsm_qna_array['contact'];
    1153         foreach ( $qsm_contact_array as $qsm_contact_id => $qsm_contact ) {
    1154             if ( ( isset($qsm_contact['type']) && 'date' === $qsm_contact['type'] ) && ( isset($qsm_contact['value']) && '' !== $qsm_contact['value'] ) && null !== $GLOBALS['qsm_date_format'] ) {
    1155                 $qsm_qna_array['contact'][ $qsm_contact_id ]['value'] = date_i18n( $GLOBALS['qsm_date_format'], strtotime( ( $qsm_contact['value'] ) ) );
    1156             }
    1157         }
    1158         return $qsm_qna_array;
    1159     }
    1160 
    1161     /**
    1162      * converts answers into preferred date format
    1163      *
    1164      * @since  7.3.3
    1165      * @param  array $qsm_qna_array The array of results for the quiz
    1166      * @uses   convert_to_preferred_date_format()
    1167      * @return array $qsm_qna_array date formatted array of results for the quiz
    1168      */
    1169 
    1170     public function convert_answers_to_preferred_date_format( $qsm_qna_array ) {
    1171 
    1172         $qsm_qna_list = $qsm_qna_array['question_answers_array'];
    1173         foreach ( $qsm_qna_list as $qna_id => $qna ) {
    1174             if ( '12' === $qna['question_type'] && null !== $GLOBALS['qsm_date_format'] ) {
    1175                 $qsm_qna_array['question_answers_array'][ $qna_id ]['1'] = date_i18n( $GLOBALS['qsm_date_format'], strtotime( ( $qna['1'] ) ) );
    1176                 $qsm_qna_array['question_answers_array'][ $qna_id ]['2'] = date_i18n( $GLOBALS['qsm_date_format'], strtotime( ( $qna['2'] ) ) );
    1177             }
    1178         }
    1179         return $qsm_qna_array;
    1180     }
    1181 
    1182     /**
    1183      * converts questions into preferred date format
    1184      *
    1185      * @since  7.3.3
    1186      * @param  array $qsm_qna_array The array of results for the quiz
    1187      * @uses   convert_to_preferred_date_format()
    1188      * @return array $qsm_qna_array date formatted array of results for the quiz
    1189      */
    1190 
    1191     public function convert_questions_to_preferred_date_format() {
    1192         if ( ! function_exists( 'qsm_convert_question_array_date_format' ) ) {
    1193             function qsm_convert_question_array_date_format( $questions ) {
    1194                 foreach ( $questions as $question_id => $question_to_convert ) {
    1195                     if ( '12' === $question_to_convert['question_type_new'] ) {
    1196                         foreach ( $question_to_convert['answers'] as $answer_id => $answer_value ) {
    1197                             $questions[ $question_id ]['answers'][ $answer_id ][0] = date_i18n( $GLOBALS['qsm_date_format'], strtotime( $answer_value[0] ) );
    1198                         }
    1199                     }
    1200                 }
    1201                 return $questions;
    1202             }
    1203         }
    1204         add_filter( 'qsm_load_questions_by_pages', 'qsm_convert_question_array_date_format' );
    1205     }
    1206 
    1207     /**
    1208      *
    1209      *
    1210      * @since  7.3.5
    1211      * @param  array
    1212      * @uses
    1213      * @return array
    1214      */
    1215 
    1216     public function qsm_results_css_inliner( $html ) {
    1217 
    1218         global $mlwQuizMasterNext;
    1219         $grading = $mlwQuizMasterNext->pluginHelper->get_section_setting( 'quiz_options', 'system' );
    1220         $wr_sign = 1 != $grading ? "&#x2715;&nbsp;" : "";
    1221 
    1222         $html    = str_replace( '<br/>', '<br>', $html );
    1223         $html    = str_replace( '<br />', '<br>', $html );
    1224         $html    = str_replace( "class='qmn_question_answer", "style='margin-bottom:30px' class='", $html );
    1225         $html    = preg_replace( '/<span class="qsm-text-simple-option(.*?)">(.*?)<\/span>/', "<span style='color:#808080;display:block;margin-bottom:5px;'>&#8226;&nbsp;$2</span>", $html );
    1226         $html    = preg_replace( '/<span class="qsm-text-wrong-option(.*?)">(.*?)<\/span>/', "<span style='color:red;display:block;margin-bottom:5px;'>&#x2715;&nbsp;$2</span>", $html );
    1227         $html    = preg_replace( '/<span class="qmn_user_incorrect_answer(.*?)">(.*?)<\/span>/', "<span style='color:red;display:block;margin-bottom:5px;'>".$wr_sign."$2</span>", $html );
    1228         $html    = preg_replace( '/<span class="qsm-text-correct-option(.*?)">(.*?)<\/span>/', "<span style='color:green;display:block;margin-bottom:5px;'>&#10003;&nbsp;$2</span>", $html );
    1229         $html    = preg_replace( '/<span class="qmn_user_correct_answer(.*?)">(.*?)<\/span>/', "<span style='color:green;display:block;margin-bottom:5px;'>&#10003;&nbsp;$2</span>", $html );
    1230 
    1231         return $html;
    1232     }
    1233 
    1234     /** */
    1235     public function categorize_question_types() {
    1236         $question_type_categorized   = array();
    1237         $question_type_uncategorized = array();
    1238         foreach ( $this->question_types as $question_type ) {
    1239             $is_categorized = isset( $question_type ['category'] ) && '' !== $question_type ['category'];
    1240             if ( $is_categorized ) {
    1241                 $question_type_categorized[ $question_type ['category'] ] [ $question_type['slug'] ] = array(
    1242                     'slug'     => $question_type['slug'],
    1243                     'name'     => $question_type['name'],
    1244                     'disabled' => (isset( $question_type['display'] ) && '-1' == $question_type['display']) ? true : false,
    1245                 );
    1246             } else {
    1247                 $question_type_uncategorized['uncategorized'][ $question_type['slug'] ] = array(
    1248                     'slug'     => $question_type['slug'],
    1249                     'name'     => $question_type['name'],
    1250                     'disabled' => (isset( $question_type['display'] ) && '-1' == $question_type['display']) ? true : false,
    1251                 );
    1252             }
    1253         }
    1254         $question_type_categorized = array_merge( $question_type_categorized, $question_type_uncategorized );
    1255         return $question_type_categorized;
    1256     }
    1257 
    1258     public function description_array() {
    1259         return array(
    1260             array(
    1261                 'question_type_id' => 11,
    1262                 'description'      => __( 'For this question type, users will see a file upload field on front end.', 'quiz-master-next' ),
    1263             ),
    1264             array(
    1265                 'question_type_id' => '14',
    1266                 'description'      => __( 'Use %BLANK% variable in the description field to display input boxes.', 'quiz-master-next' ),
    1267             ),
    1268             array(
    1269                 'question_type_id' => '12',
    1270                 'description'      => __( 'For this question type, users will see a date input field on front end.', 'quiz-master-next' ),
    1271             ),
    1272             array(
    1273                 'question_type_id' => '3',
    1274                 'description'      => __( 'For this question type, users will see a standard input box on front end.', 'quiz-master-next' ),
    1275             ),
    1276             array(
    1277                 'question_type_id' => '5',
    1278                 'description'      => __( 'For this question type, users will see a standard textarea input box on front end.', 'quiz-master-next' ),
    1279             ),
    1280             array(
    1281                 'question_type_id' => '6',
    1282                 'description'      => __( 'Displays a simple section on front end. Description is mandatory. ', 'quiz-master-next' ),
    1283             ),
    1284             array(
    1285                 'question_type_id' => '7',
    1286                 'description'      => __( 'For this question type, users will see an input box which accepts only number values on front end.', 'quiz-master-next' ),
    1287             ),
    1288             array(
    1289                 'question_type_id' => '8',
    1290                 'description'      => __( "For this question type, users will see a checkbox on front end. The text in description field will act like it's label.", 'quiz-master-next' ),
    1291             ),
    1292             array(
    1293                 'question_type_id' => '9',
    1294                 'description'      => __( 'For this question type, users will see a Captcha field on front end.', 'quiz-master-next' ),
    1295             ),
    1296             // array(
    1297             // 'question_type_id' => '13',
    1298             // 'description'      => __( 'Use points based grading system for Polar questions.', 'quiz-master-next' ),
    1299             // ),
    1300         );
    1301     }
    1302 
    1303     public function qsm_get_limited_options( $options, $limit ) {
    1304         $correct = array_filter( $options, fn( $o, $k ) => 1 == $o[2], ARRAY_FILTER_USE_BOTH );
    1305         $incorrect = array_filter( $options, fn( $o, $k ) => 0 == $o[2], ARRAY_FILTER_USE_BOTH );
    1306         shuffle( $incorrect );
    1307         $final = array_merge( $correct, array_slice( $incorrect, 0, $limit - count( $correct ) ) );
    1308         shuffle( $final );
    1309         $final_keys = array_map( fn( $k ) => array_search( $k, array_values( $options ), true ),  $final );
    1310         return array(
    1311             'final'             => $final,
    1312             'answer_limit_keys' => implode( ',', $final_keys ),
    1313         );
    1314     }
    1315 
    1316     public function qsm_get_limited_options_by_keys( $options, $keys ) {
    1317         return array_map( fn( $k ) => $options[ $k ], explode( ',', $keys ) );
    1318     }
     14class QMNPluginHelper
     15{
     16
     17    /**
     18     * Addon Page tabs array
     19     *
     20     * @var   array
     21     * @since 4.0.0
     22     */
     23    public $addon_tabs = [];
     24
     25    /**
     26     * Stats Page tabs array
     27     *
     28     * @var   array
     29     * @since 4.0.0
     30     */
     31    public $stats_tabs = [];
     32
     33    /**
     34     * Admin Results Page tabs array
     35     *
     36     * @var   array
     37     * @since 5.0.0
     38     */
     39    public $admin_results_tabs = [];
     40
     41    /**
     42     * Results Details Page tabs array
     43     *
     44     * @var   array
     45     * @since 4.1.0
     46     */
     47    public $results_tabs = [];
     48
     49    /**
     50     * Settings Page tabs array
     51     *
     52     * @var   array
     53     * @since 4.0.0
     54     */
     55    public $settings_tabs = [];
     56
     57    /**
     58     * Question types array
     59     *
     60     * @var   array
     61     * @since 4.0.0
     62     */
     63    public $question_types = [];
     64
     65    /**
     66     * Template array
     67     *
     68     * @var   array
     69     * @since 4.5.0
     70     */
     71    public $quiz_templates = [];
     72
     73    /**
     74     * Main Construct Function
     75     *
     76     * Call functions within class
     77     *
     78     * @since  4.0.0
     79     * @return void
     80     */
     81    public function __construct()
     82    {
     83        add_action('wp_ajax_qmn_question_type_change', [ $this, 'get_question_type_edit_content' ]);
     84        add_action('admin_init', [ $this, 'qsm_add_default_translations' ], 9999);
     85        add_action('qsm_saved_question', [ $this, 'qsm_add_question_translations' ], 10, 2);
     86        add_action('qsm_saved_text_message', [ $this, 'qsm_add_text_message_translations' ], 10, 3);
     87        add_action('qsm_saved_quiz_settings', [ $this, 'qsm_add_quiz_settings_translations' ], 10, 3);
     88
     89        add_action('qsm_register_language_support', [ $this, 'qsm_register_language_support' ], 10, 3);
     90        add_filter('qsm_language_support', [ $this, 'qsm_language_support' ], 10, 3);
     91        add_action('wp_ajax_qsm_validate_result_submission', [ $this, 'qsm_validate_result_submission' ], 10, 3);
     92        add_action('wp_ajax_nopriv_qsm_validate_result_submission', [ $this, 'qsm_validate_result_submission' ], 10, 3);
     93    }
     94
     95    public function qsm_validate_result_submission()
     96    {
     97        global $wpdb, $mlwQuizMasterNext;
     98
     99        $user_email = isset($_POST['email']) ? sanitize_email(wp_unslash($_POST['email'])) : '';
     100        $quiz_id = isset($_POST['quiz_id']) ? intval(wp_unslash($_POST['quiz_id'])) : 0;
     101        $total_user_tries = isset($_POST['total_user_tries']) ? intval(wp_unslash($_POST['total_user_tries'])) : 0;
     102        $mlw_qmn_user_try_count = 0;
     103
     104        if (! empty($user_email) && is_email($user_email)) {
     105            $mlw_qmn_user_try_count = $wpdb->get_var(
     106                $wpdb->prepare(
     107                    "SELECT COUNT(*) FROM {$wpdb->prefix}mlw_results WHERE email = %s AND deleted = 0 AND quiz_id = %d",
     108                    $user_email,
     109                    $quiz_id
     110                )
     111            );
     112        }
     113        if ($mlw_qmn_user_try_count < $total_user_tries) {
     114            wp_send_json_success();
     115        } else {
     116            wp_send_json_error();
     117        }
     118        wp_die();
     119    }
     120
     121    /**
     122     * Calls all class functions to check if quiz is setup properly
     123     *
     124     * @param  int $quiz_id The ID of the quiz or survey to load.
     125     * @return array An array which contains boolean result of has_proper_quiz, message and/or qmn_quiz_options
     126     */
     127    public function has_proper_quiz($quiz_id)
     128    {
     129        if (empty($quiz_id)) {
     130            return [
     131                'res'     => false,
     132                'message' => __('Empty Quiz ID.', 'quiz-master-next'),
     133            ];
     134        }
     135
     136        $quiz_id = intval($quiz_id);
     137
     138        // Tries to load quiz name to ensure this is a valid ID.
     139        global $mlwQuizMasterNext, $qmn_allowed_visit, $qmn_json_data;
     140        $qmn_json_data     = [];
     141        $qmn_allowed_visit = true;
     142        if (false === $this->prepare_quiz($quiz_id)) {
     143            return [
     144                'res'     => false,
     145                'message' => __('It appears that this quiz is not set up correctly.', 'quiz-master-next'),
     146            ];
     147        }
     148
     149        $has_result_id = ( ! isset($_GET['result_id']) || '' === $_GET['result_id'] );
     150
     151        if ($has_result_id) {
     152            global $mlw_qmn_quiz;
     153            $mlw_qmn_quiz = $quiz_id;
     154        }
     155
     156        $qmn_quiz_options = $mlwQuizMasterNext->quiz_settings->get_quiz_options();
     157
     158        if ($has_result_id) {
     159            /**
     160             * Filter Quiz Options before Quiz Display
     161             */
     162            $qmn_quiz_options = apply_filters('qsm_shortcode_quiz_options', $qmn_quiz_options);
     163        }
     164
     165        // If quiz options isn't found, stop function.
     166        if (is_null($qmn_quiz_options) || ( ! empty($qmn_quiz_options->deleted) && 1 == $qmn_quiz_options->deleted )) {
     167            return [
     168                'res'     => false,
     169                'message' => __('This quiz is no longer available.', 'quiz-master-next'),
     170            ];
     171        }
     172
     173        // If quiz options isn't found, stop function.
     174        if (is_null($qmn_quiz_options) || empty($qmn_quiz_options->quiz_name)) {
     175            return [
     176                'res'     => false,
     177                'message' => __('It appears that this quiz is not set up correctly.', 'quiz-master-next'),
     178            ];
     179        }
     180
     181        return [
     182            'res'              => true,
     183            'message'          => __('Quiz is setup properly.', 'quiz-master-next'),
     184            'qmn_quiz_options' => $qmn_quiz_options,
     185        ];
     186    }
     187
     188    /**
     189     * Calls all class functions to initialize quiz
     190     *
     191     * @param  int $quiz_id The ID of the quiz or survey to load.
     192     * @return bool True or False if ID is valid.
     193     */
     194    public function prepare_quiz($quiz_id)
     195    {
     196        $quiz_id = intval($quiz_id);
     197
     198        // Tries to load quiz name to ensure this is a valid ID.
     199        global $wpdb;
     200        $quiz_name = $wpdb->get_var($wpdb->prepare("SELECT quiz_name FROM {$wpdb->prefix}mlw_quizzes WHERE quiz_id=%d LIMIT 1", $quiz_id));
     201        if (is_null($quiz_name)) {
     202            return false;
     203        }
     204
     205        global $mlwQuizMasterNext;
     206        $mlwQuizMasterNext->quizCreator->set_id($quiz_id);
     207        $mlwQuizMasterNext->quiz_settings->prepare_quiz($quiz_id);
     208
     209        return true;
     210    }
     211
     212    /**
     213     * Retrieves all quizzes.
     214     *
     215     * @param  bool   $include_deleted If set to true, returned array will include all deleted quizzes
     216     * @param  string $order_by        The column the quizzes should be ordered by
     217     * @param  string $order           whether the $order_by should be ordered as ascending or decending. Can be "ASC" or "DESC"
     218     * @param  arr    $user_role       role of current user
     219     * @param  int    $user_id         Get the quiz based on user id
     220     * @return array All of the quizzes as a numerical array of objects
     221     */
     222    public function get_quizzes($include_deleted = false, $order_by = 'quiz_id', $order = 'DESC', $user_role = [], $user_id = '', $limit = '', $offset = '', $where = '')
     223    {
     224        global $wpdb;
     225
     226        // Set order direction
     227        $order_direction = 'DESC';
     228        if ('ASC' === $order) {
     229            $order_direction = 'ASC';
     230        }
     231
     232        // Set field to sort by
     233        switch ($order_by) {
     234            case 'last_activity':
     235                $order_field = 'last_activity';
     236                break;
     237
     238            case 'quiz_views':
     239                $order_field = 'quiz_views';
     240                break;
     241
     242            case 'quiz_taken':
     243                $order_field = 'quiz_taken';
     244                break;
     245
     246            case 'title':
     247                $order_field = 'quiz_name';
     248                break;
     249
     250            default:
     251                $order_field = 'quiz_id';
     252                break;
     253        }
     254
     255        // Should we include deleted?
     256        $delete = 'WHERE deleted=0';
     257        if ('' !== $where) {
     258            $delete = $delete . ' AND ' . $where;
     259        }
     260        if ($include_deleted) {
     261            $delete = '';
     262        }
     263        $user_str = '';
     264        if (in_array('author', (array) $user_role, true)) {
     265            if ($user_id && '' === $delete) {
     266                $user_str = "WHERE quiz_author_id = '$user_id'";
     267            } elseif ($user_id && '' !== $delete) {
     268                $user_str = " AND quiz_author_id = '$user_id'";
     269            }
     270        }
     271        if ('' !== $where && '' !== $user_str) {
     272            $user_str = $user_str . ' AND ' . $where;
     273        }
     274        $where_str = '';
     275        if ('' === $user_str && '' === $delete && '' !== $where) {
     276            $where_str = "WHERE $where";
     277        }
     278        if ('' !== $limit) {
     279            $limit = ' limit ' . $offset . ', ' . $limit;
     280        }
     281        // Get quizzes and return them
     282        $delete  = apply_filters('quiz_query_delete_clause', $delete);
     283        $quizzes = $wpdb->get_results(stripslashes($wpdb->prepare("SELECT * FROM {$wpdb->prefix}mlw_quizzes %1s %2s %3s ORDER BY %4s %5s %6s", $delete, $user_str, $where_str, $order_field, $order_direction, $limit)));
     284        return $quizzes;
     285    }
     286
     287    /**
     288     * Registers a quiz setting
     289     *
     290     * @since 5.0.0
     291     * @param array $field_array An array of the components for the settings field
     292     */
     293    public function register_quiz_setting($field_array, $section = 'quiz_options')
     294    {
     295        global $mlwQuizMasterNext;
     296        $mlwQuizMasterNext->quiz_settings->register_setting($field_array, $section);
     297    }
     298
     299    /**
     300     * Retrieves a setting value from a section based on name of section and setting
     301     *
     302     * @since  5.0.0
     303     * @param  string $section The name of the section the setting is registered in
     304     * @param  string $setting The name of the setting whose value we need to retrieve
     305     * @param  mixed  $default What we need to return if no setting exists with given $setting
     306     * @return $mixed Value set for $setting or $default if setting does not exist
     307     */
     308    public function get_section_setting($section, $setting, $default = false)
     309    {
     310        global $mlwQuizMasterNext;
     311        return apply_filters('qsm_section_setting_text', $mlwQuizMasterNext->quiz_settings->get_section_setting($section, $setting, $default));
     312    }
     313
     314    /**
     315     * Retrieves setting value based on name of setting
     316     *
     317     * @since  4.0.0
     318     * @param  string $setting The name of the setting whose value we need to retrieve
     319     * @param  mixed  $default What we need to return if no setting exists with given $setting
     320     * @return $mixed Value set for $setting or $default if setting does not exist
     321     */
     322    public function get_quiz_setting($setting, $default = false, $caller = '')
     323    {
     324        global $mlwQuizMasterNext;
     325        if (( 'pages' == $setting || 'qpages' == $setting ) && empty($caller)) {
     326            $pages = $mlwQuizMasterNext->quiz_settings->get_setting($setting, $default);
     327            $temp_pages = [];
     328            foreach ($pages as $index => $page) {
     329                $page_should_display = [];
     330                $page = 'qpages' == $setting ? $page['questions'] : $page;
     331                foreach ($page as $key => $question_id) {
     332                    $isPublished = $mlwQuizMasterNext->pluginHelper->get_question_setting($question_id, 'isPublished');
     333                    if ('' == $isPublished || ( '' != $isPublished && 1 === intval($isPublished) )) {
     334                        $page_should_display[]  = true;
     335                    } elseif ('' != $isPublished && 0 === intval($isPublished)) {
     336                        $page_should_display[] = false;
     337                        unset($page[ $key ]);
     338                    }
     339                }
     340                if (in_array(true, $page_should_display, true)) {
     341                    if ('qpages' == $setting) {
     342                        $pages[ $index ]['questions'] = $page;
     343                        $temp_pages[] = $pages[ $index ];
     344                    } else {
     345                        $temp_pages[] = $page;
     346                    }
     347                }
     348            }
     349            return $temp_pages;
     350        }
     351        return $mlwQuizMasterNext->quiz_settings->get_setting($setting, $default);
     352    }
     353
     354    /**
     355     * Updates a settings value, adding it if it didn't already exist
     356     *
     357     * @since  4.0.0
     358     * @param  string $setting The name of the setting whose value we need to retrieve
     359     * @param  mixed  $value   The value that needs to be stored for the setting
     360     * @return bool True if successful or false if fails
     361     */
     362    public function update_quiz_setting($setting, $value)
     363    {
     364        global $mlwQuizMasterNext;
     365        return $mlwQuizMasterNext->quiz_settings->update_setting($setting, $value);
     366    }
     367
     368    /**
     369     * Outputs the section of input fields
     370     *
     371     * @since 5.0.0
     372     * @since 7.0 Added new parameter settings_fields for default setting
     373     * @param string $section The section that the settings were registered with
     374     */
     375    public function generate_settings_section($section = 'quiz_options', $settings_fields = [])
     376    {
     377        global $mlwQuizMasterNext;
     378        if (empty($settings_fields)) {
     379            $settings_fields = $mlwQuizMasterNext->quiz_settings->load_setting_fields($section);
     380        }
     381        QSM_Fields::generate_section($settings_fields, $section);
     382    }
     383
     384    /**
     385     * Registers Quiz Templates
     386     *
     387     * @since 4.5.0
     388     * @param $name      String of the name of the template
     389     * @param $file_path String of the path to the css file
     390     */
     391    public function register_quiz_template($name, $file_path)
     392    {
     393        $slug                          = strtolower(str_replace(' ', '-', $name));
     394        $this->quiz_templates[ $slug ] = [
     395            'name' => $name,
     396            'path' => $file_path,
     397        ];
     398    }
     399
     400    /**
     401     * Returns Template Array
     402     *
     403     * @since  4.5.0
     404     * @param  $name String of the name of the template. If left empty, will return all templates
     405     * @return array The array of quiz templates
     406     */
     407    public function get_quiz_templates($slug = null)
     408    {
     409        if (is_null($slug)) {
     410            return $this->quiz_templates;
     411        } elseif (isset($this->quiz_templates[ $slug ])) {
     412            return $this->quiz_templates[ $slug ];
     413        } else {
     414            return false;
     415        }
     416    }
     417
     418    /**
     419     * Register Question Types
     420     *
     421     * Adds a question type to the question type array using the parameters given
     422     *
     423     * @since  4.0.0
     424     * @param  string $name             The name of the Question Type which will be shown when selecting type
     425     * @param  string $display_function The name of the function to call when displaying the question
     426     * @param  bool   $graded           Tells the plugin if this question is graded or not. This will affect scoring.
     427     * @param  string $review_function  The name of the function to call when scoring the question
     428     * @param  string $slug             The slug of the question type to be stored with question in database
     429     * @param  array  $options          The options for show and hide question validation settings and answer types
     430     * @return void
     431     */
     432    public function register_question_type($name, $display_function, $graded, $review_function = null, $edit_args = null, $save_edit_function = null, $slug = null, $options = [])
     433    {
     434        if (is_null($slug)) {
     435            $slug = strtolower(str_replace(' ', '-', $name));
     436        } else {
     437            $slug = strtolower(str_replace(' ', '-', $slug));
     438        }
     439        if (is_null($edit_args) || ! is_array($edit_args)) {
     440            $validated_edit_function = [
     441                'inputs'       => [
     442                    'question',
     443                    'answer',
     444                    'hint',
     445                    'correct_info',
     446                    'comments',
     447                    'category',
     448                    'required',
     449                ],
     450                'information'  => '',
     451                'extra_inputs' => [],
     452                'function'     => '',
     453            ];
     454        } else {
     455            $validated_edit_function = [
     456                'inputs'       => $edit_args['inputs'],
     457                'information'  => $edit_args['information'],
     458                'extra_inputs' => $edit_args['extra_inputs'],
     459                'function'     => $edit_args['function'],
     460            ];
     461        }
     462        if (is_null($save_edit_function)) {
     463            $save_edit_function = '';
     464        }
     465        $new_type                      = [
     466            'name'    => $name,
     467            'display' => $display_function,
     468            'review'  => $review_function,
     469            'graded'  => $graded,
     470            'edit'    => $validated_edit_function,
     471            'save'    => $save_edit_function,
     472            'slug'    => $slug,
     473            'options' => $options,
     474        ];
     475        $new_type = apply_filters('register_question_type_new_type', $new_type);
     476        $this->question_types[ $slug ] = $new_type;
     477    }
     478
     479    /**
     480     * Retrieves List Of Question Types
     481     *
     482     * retrieves a list of the slugs and names of the question types
     483     *
     484     * @since  4.0.0
     485     * @return array An array which contains the slug and name of question types that have been registered
     486     */
     487    public function get_question_type_options()
     488    {
     489        $type_array = [];
     490        foreach ($this->question_types as $type) {
     491            $type_array[] = [
     492                'slug'    => $type['slug'],
     493                'name'    => $type['name'],
     494                'options' => $type['options'],
     495            ];
     496        }
     497        return $type_array;
     498    }
     499
     500    /**
     501     *
     502     */
     503    public function set_question_type_meta($type_id, $meta_key, $meta_value)
     504    {
     505
     506        $this->question_types[ $type_id ][ $meta_key ] = $meta_value;
     507    }
     508
     509    public function get_question_type_edit_fields()
     510    {
     511        $type_array = [];
     512        foreach ($this->question_types as $type) {
     513            $type_array[ $type['slug'] ] = $type['edit'];
     514        }
     515        return $type_array;
     516    }
     517
     518    /**
     519     * Displays A Question
     520     *
     521     * Retrieves the question types display function and creates the HTML for the question
     522     *
     523     * @since  4.0.0
     524     * @param  string $slug         The slug of the question type that the question is
     525     * @param  int    $question_id  The id of the question
     526     * @param  array  $quiz_options An array of the columns of the quiz row from the database
     527     * @return string The HTML for the question
     528     */
     529    public function display_question($slug, $question_id, $quiz_options)
     530    {
     531        global $wpdb;
     532        global $qmn_total_questions, $qmn_all_questions_count;
     533        $question = $wpdb->get_row($wpdb->prepare('SELECT * FROM ' . $wpdb->prefix . 'mlw_questions WHERE question_id=%d', intval($question_id)));
     534        $answers  = [];
     535        if (is_serialized($question->answer_array) && is_array(maybe_unserialize($question->answer_array))) {
     536            $answers = maybe_unserialize($question->answer_array);
     537        } else {
     538            $mlw_answer_array_correct                                  = [ 0, 0, 0, 0, 0, 0 ];
     539            $mlw_answer_array_correct[ $question->correct_answer - 1 ] = 1;
     540            $answers = [
     541                [ $question->answer_one, $question->answer_one_points, $mlw_answer_array_correct[0] ],
     542                [ $question->answer_two, $question->answer_two_points, $mlw_answer_array_correct[1] ],
     543                [ $question->answer_three, $question->answer_three_points, $mlw_answer_array_correct[2] ],
     544                [ $question->answer_four, $question->answer_four_points, $mlw_answer_array_correct[3] ],
     545                [ $question->answer_five, $question->answer_five_points, $mlw_answer_array_correct[4] ],
     546                [ $question->answer_six, $question->answer_six_points, $mlw_answer_array_correct[5] ],
     547            ];
     548        }
     549        $answers_original = $answers;
     550        if (2 === intval($quiz_options->randomness_order) || 3 === intval($quiz_options->randomness_order)) {
     551            $answers = self::qsm_shuffle_assoc($answers);
     552            global $quiz_answer_random_ids;
     553            $answer_ids = array_keys($answers);
     554            $quiz_answer_random_ids[ $question_id ] = $answer_ids;
     555        }
     556
     557        // convert answer array into key value pair
     558        $answers_kvpair = [];
     559        foreach ($answers as $answer_item) {
     560            $key                    = array_search($answer_item, $answers_original, true);
     561            $answers_kvpair[ $key ] = $answer_item;
     562        }
     563        unset($answer_item);
     564        $answers = $answers_kvpair;
     565
     566        /**
     567         * Filter Answers of specific question before display
     568         */
     569        $answers = apply_filters('qsm_single_question_answers', $answers, $question, $quiz_options);
     570        foreach ($this->question_types as $type) {
     571            if (strtolower(str_replace(' ', '-', $slug)) === $type['slug'] && ! empty($type['display']) && function_exists($type['display'])) {
     572                $qmn_all_questions_count += 1;
     573                if ($type['graded']) {
     574                    $qmn_total_questions += 1;
     575                    if (1 === intval($quiz_options->question_numbering)) { ?>
     576                        <span class='mlw_qmn_question_number'><?php echo esc_html($qmn_total_questions); ?>.&nbsp;</span>
     577                        <?php
     578                    }
     579                }
     580                if ($quiz_options->show_category_on_front) {
     581                    $categories = QSM_Questions::get_question_categories($question_id);
     582                    if (! empty($categories['category_name'])) {
     583                        $cat_name = implode(',', $categories['category_name']);
     584                        ?>
     585                        <div class="quiz-cat"><?php echo esc_html($cat_name); ?></div>
     586                        <?php
     587                    }
     588                }
     589
     590                call_user_func($type['display'], intval($question_id), $question->question_name, $answers);
     591                do_action('qsm_after_question', $question);
     592            }
     593        }
     594    }
     595
     596    public function get_questions_count($quiz_id = 0)
     597    {
     598        global $wpdb;
     599        $quiz_id = intval($quiz_id);
     600        $count   = 0;
     601        if (empty($quiz_id) || 0 == $quiz_id) {
     602            return $count;
     603        }
     604
     605        $quiz_settings = $wpdb->get_var($wpdb->prepare("SELECT `quiz_settings` FROM `{$wpdb->prefix}mlw_quizzes` WHERE `quiz_id`=%d", $quiz_id));
     606        if (! empty($quiz_settings)) {
     607            $settings    = maybe_unserialize($quiz_settings);
     608            $pages       = isset($settings['pages']) ? maybe_unserialize($settings['pages']) : [];
     609            if (! empty($pages)) {
     610                foreach ($pages as $page) {
     611                    $count += count($page);
     612                }
     613            }
     614        }
     615        return $count;
     616    }
     617
     618    public function get_questions_ids($quiz_id = 0)
     619    {
     620        global $wpdb;
     621        $quiz_id = intval($quiz_id);
     622        $ids   = [];
     623        if (empty($quiz_id) || 0 == $quiz_id) {
     624            return $ids;
     625        }
     626
     627        $quiz_settings = $wpdb->get_var($wpdb->prepare("SELECT `quiz_settings` FROM `{$wpdb->prefix}mlw_quizzes` WHERE `quiz_id`=%d", $quiz_id));
     628        if (! empty($quiz_settings)) {
     629            $settings    = maybe_unserialize($quiz_settings);
     630            $pages       = isset($settings['pages']) ? maybe_unserialize($settings['pages']) : [];
     631            if (! empty($pages)) {
     632                foreach ($pages as $page) {
     633                    $ids = array_merge($ids, $page);
     634                }
     635            }
     636        }
     637        return $ids;
     638    }
     639
     640    /**
     641     * Shuffle assoc array
     642     *
     643     * @since  7.3.11
     644     * @param  array $list An array
     645     * @return array
     646     */
     647    public static function qsm_shuffle_assoc($list)
     648    {
     649        if (! is_array($list)) {
     650            return $list;
     651        }
     652        $keys    = array_keys($list);
     653        shuffle($keys);
     654        $random  = [];
     655        foreach ($keys as $key) {
     656            $random[ $key ] = $list[ $key ];
     657        }
     658        return $random;
     659    }
     660
     661    /**
     662     * Find the key of the first occurrence of a substring in an array
     663     */
     664    public static function qsm_stripos_array($str, array $arr)
     665    {
     666        if (is_array($arr)) {
     667            foreach ($arr as $a) {
     668                if (stripos($str, $a) !== false) {
     669                    return $a;
     670                }
     671            }
     672        }
     673        return false;
     674    }
     675
     676    /**
     677     * Default strings
     678     * Translation not added in empty string due to warning ( WordPress.WP.I18n.NoEmptyStrings )
     679     */
     680    public static function get_default_texts()
     681    {
     682        $defaults = [
     683            'message_before'                   => __('Welcome to your %QUIZ_NAME%', 'quiz-master-next'),
     684            'message_comment'                  => __('Please fill in the comment box below.', 'quiz-master-next'),
     685            'message_end_template'             => '',
     686            'question_answer_template'         => __('%QUESTION%<br />%USER_ANSWERS_DEFAULT%<br/>%CORRECT_ANSWER_INFO%', 'quiz-master-next'),
     687            'question_answer_email_template'   => __('%QUESTION%<br />Answer Provided: %USER_ANSWER%<br/>Correct Answer: %CORRECT_ANSWER%<br/>Comments Entered: %USER_COMMENTS%', 'quiz-master-next'),
     688            'total_user_tries_text'            => __('You have utilized all of your attempts to pass this quiz.', 'quiz-master-next'),
     689            'require_log_in_text'              => __('This quiz is for logged in users only.', 'quiz-master-next'),
     690            'limit_total_entries_text'         => __('Unfortunately, this quiz has a limited amount of entries it can recieve and has already reached that limit.', 'quiz-master-next'),
     691            'scheduled_timeframe_text'         => '',
     692            'twitter_sharing_text'             => __('I just scored %CORRECT_SCORE%% on %QUIZ_NAME%!', 'quiz-master-next'),
     693            'facebook_sharing_text'            => __('I just scored %CORRECT_SCORE%% on %QUIZ_NAME%!', 'quiz-master-next'),
     694            'linkedin_sharing_text'            => __('I just scored %CORRECT_SCORE%% on %QUIZ_NAME%!', 'quiz-master-next'),
     695            'submit_button_text'               => __('Submit', 'quiz-master-next'),
     696            'retake_quiz_button_text'          => __('Retake Quiz', 'quiz-master-next'),
     697            'previous_button_text'             => __('Previous', 'quiz-master-next'),
     698            'next_button_text'                 => __('Next', 'quiz-master-next'),
     699            'deselect_answer_text'             => __('Deselect Answer', 'quiz-master-next'),
     700            'empty_error_text'                 => __('Please complete all required fields!', 'quiz-master-next'),
     701            'email_error_text'                 => __('Not a valid e-mail address!', 'quiz-master-next'),
     702            'number_error_text'                => __('This field must be a number!', 'quiz-master-next'),
     703            'incorrect_error_text'             => __('The entered text is not correct!', 'quiz-master-next'),
     704            'url_error_text'                   => __('The entered URL is not valid!', 'quiz-master-next'),
     705            'minlength_error_text'             => __('Required atleast %minlength% characters.', 'quiz-master-next'),
     706            'maxlength_error_text'             => __('Minimum %maxlength% characters allowed.', 'quiz-master-next'),
     707            'comment_field_text'               => __('Comments', 'quiz-master-next'),
     708            'hint_text'                        => __('Hint', 'quiz-master-next'),
     709            'quick_result_correct_answer_text' => __('Correct! You have selected correct answer.', 'quiz-master-next'),
     710            'quick_result_wrong_answer_text'   => __('Wrong! You have selected wrong answer.', 'quiz-master-next'),
     711            'quiz_processing_message'          => '',
     712            'quiz_limit_choice'                => __('Limit of choice is reached.', 'quiz-master-next'),
     713            'name_field_text'                  => __('Name', 'quiz-master-next'),
     714            'business_field_text'              => __('Business', 'quiz-master-next'),
     715            'email_field_text'                 => __('Email', 'quiz-master-next'),
     716            'phone_field_text'                 => __('Phone Number', 'quiz-master-next'),
     717            'start_quiz_text'                  => __('Start Quiz', 'quiz-master-next'),
     718            'start_survey_text'                => __('Start Survey', 'quiz-master-next'),
     719        ];
     720        return apply_filters('qsm_default_texts', $defaults);
     721    }
     722
     723    /**
     724     * Register string in WPML for translation
     725     */
     726    public static function qsm_register_language_support($translation_text = '', $translation_slug = '', $domain = 'QSM Meta')
     727    {
     728        if (! empty($translation_text) && is_plugin_active('wpml-string-translation/plugin.php')) {
     729            $translation_slug = sanitize_title($translation_slug);
     730            /**
     731             * Register the string for translation
     732             */
     733            do_action('wpml_register_single_string', $domain, $translation_slug, $translation_text);
     734        }
     735    }
     736
     737    /**
     738     * Translate string before display
     739     */
     740    public static function qsm_language_support($translation_text = '', $translation_slug = '', $domain = 'QSM Meta')
     741    {
     742
     743        /**
     744         * Check if WPML String Translation plugin is activated.
     745         */
     746        if (! empty($translation_text) && is_plugin_active('wpml-string-translation/plugin.php')) {
     747            /**
     748             * Decode HTML Special characters.
     749             */
     750            $translation_text = wp_kses_post(htmlspecialchars_decode($translation_text, ENT_QUOTES));
     751            $translation_slug    = sanitize_title($translation_slug);
     752            $new_text            = apply_filters('wpml_translate_single_string', $translation_text, $domain, $translation_slug);
     753            if ('QSM Answers' === $domain && $new_text == $translation_text) {
     754                if (0 === strpos($translation_slug, 'caption-')) {
     755                    $translation_slug    = sanitize_title('caption-' . $translation_text);
     756                } else {
     757                    $translation_slug    = sanitize_title('answer-' . $translation_text);
     758                }
     759                $new_text            = apply_filters('wpml_translate_single_string', $translation_text, $domain, $translation_slug);
     760            }
     761            $new_text            = wp_kses_post(htmlspecialchars_decode($new_text, ENT_QUOTES));
     762            /**
     763             * Return translation for non-default strings.
     764             */
     765            if ("QSM Meta" != $domain) {
     766                return $new_text;
     767            }
     768            /**
     769             * Check if translation exist.
     770             */
     771            if (0 !== strcasecmp($translation_text, $new_text)) {
     772                return $new_text;
     773            }
     774            /**
     775             * Check if translation exist for default string.
     776             */
     777            $default_texts   = self::get_default_texts();
     778            $default_key     = self::qsm_stripos_array($translation_slug, array_keys($default_texts));
     779            if (false !== $default_key && 0 === strcasecmp($translation_text, $default_texts[ $default_key ])) {
     780                return apply_filters('wpml_translate_single_string', $translation_text, 'QSM Defaults', 'quiz_' . $default_key);
     781            }
     782        } elseif (! empty($translation_text)) {
     783            $translation_text = wp_kses_post($translation_text);
     784        }
     785
     786        return $translation_text;
     787    }
     788
     789    public function qsm_add_default_translations()
     790    {
     791        $default_texts = self::get_default_texts();
     792        if (empty($default_texts)) {
     793            return;
     794        }
     795        if (is_plugin_active('wpml-string-translation/plugin.php')) {
     796            foreach ($default_texts as $key => $text) {
     797                if (! empty($text)) {
     798                    $translation_slug = sanitize_title('quiz_' . $key);
     799                    /**
     800                     * Register the string for translation
     801                     */
     802                    do_action('wpml_register_single_string', 'QSM Defaults', $translation_slug, $text);
     803                }
     804            }
     805        }
     806    }
     807
     808    public function qsm_add_question_translations($question_id, $question_data)
     809    {
     810        $settings    = isset($question_data['question_settings']) ? maybe_unserialize($question_data['question_settings']) : [];
     811        $hints       = isset($question_data['hints']) ? $question_data['hints'] : '';
     812        $answer_info = isset($question_data['question_answer_info']) ? html_entity_decode($question_data['question_answer_info']) : '';
     813
     814        $this->qsm_register_language_support(htmlspecialchars_decode(isset($settings['question_title']) ? $settings['question_title'] : '', ENT_QUOTES), "Question-{$question_id}", "QSM Questions");
     815        $this->qsm_register_language_support(htmlspecialchars_decode($question_data['question_name'], ENT_QUOTES), "question-description-{$question_id}", "QSM Questions");
     816        $this->qsm_register_language_support($hints, "hint-{$question_id}");
     817        $this->qsm_register_language_support($answer_info, "correctanswerinfo-{$question_id}");
     818
     819        $answers = isset($question_data['answer_array']) ? maybe_unserialize($question_data['answer_array']) : [];
     820        if (! empty($answers)) {
     821            $answerEditor = isset($settings['answerEditor']) ? $settings['answerEditor'] : 'text';
     822            foreach ($answers as $key => $ans) {
     823                if ('image' === $answerEditor) {
     824                    $caption_text = trim(htmlspecialchars_decode($ans[3], ENT_QUOTES));
     825                    $this->qsm_register_language_support($caption_text, 'caption-' . $question_id . '-' . $key, 'QSM Answers');
     826                } else {
     827                    $answer_text = isset($ans[0]) ? trim(htmlspecialchars_decode($ans[0], ENT_QUOTES)) : '';
     828                    $this->qsm_register_language_support($answer_text, 'answer-' . $question_id . '-' . $key, 'QSM Answers');
     829                }
     830            }
     831        }
     832    }
     833
     834    public function qsm_add_text_message_translations($quiz_id, $text_id, $message)
     835    {
     836        $message = htmlspecialchars_decode($message, ENT_QUOTES);
     837        $this->qsm_register_language_support($message, "quiz_{$text_id}-{$quiz_id}");
     838    }
     839
     840    public function qsm_add_quiz_settings_translations($quiz_id, $section, $settings_array)
     841    {
     842        if ('quiz_text' == $section && ! empty($settings_array)) {
     843            foreach ($settings_array as $key => $val) {
     844                if (! empty($val)) {
     845                    $this->qsm_register_language_support(htmlspecialchars_decode($val, ENT_QUOTES), "quiz_{$key}-{$quiz_id}");
     846                }
     847            }
     848        }
     849    }
     850
     851    /**
     852     * Calculates Score For Question
     853     *
     854     * Calculates the score for the question based on the question type
     855     *
     856     * @since  4.0.0
     857     * @param  string $slug        The slug of the question type that the question is
     858     * @param  int    $question_id The id of the question
     859     * @return array An array of the user's score from the question
     860     */
     861    public function display_review($slug, $question_id)
     862    {
     863        $results_array = [];
     864        global $wpdb;
     865        $question = $wpdb->get_row($wpdb->prepare('SELECT * FROM ' . $wpdb->prefix . 'mlw_questions WHERE question_id=%d', intval($question_id)));
     866        $answers  = maybe_unserialize($question->answer_array);
     867        if (empty($answers) || ! is_array($answers)) {
     868            $mlw_answer_array_correct                                  = [ 0, 0, 0, 0, 0, 0 ];
     869            $mlw_answer_array_correct[ $question->correct_answer - 1 ] = 1;
     870            $answers = [
     871                [ $question->answer_one, $question->answer_one_points, $mlw_answer_array_correct[0] ],
     872                [ $question->answer_two, $question->answer_two_points, $mlw_answer_array_correct[1] ],
     873                [ $question->answer_three, $question->answer_three_points, $mlw_answer_array_correct[2] ],
     874                [ $question->answer_four, $question->answer_four_points, $mlw_answer_array_correct[3] ],
     875                [ $question->answer_five, $question->answer_five_points, $mlw_answer_array_correct[4] ],
     876                [ $question->answer_six, $question->answer_six_points, $mlw_answer_array_correct[5] ],
     877            ];
     878        }
     879        foreach ($this->question_types as $type) {
     880            if (strtolower(str_replace(' ', '-', $slug)) === $type['slug']) {
     881                if (! is_null($type['review'])) {
     882                    $results_array = call_user_func($type['review'], intval($question_id), $question->question_name, $answers);
     883                } else {
     884                    $results_array = [ 'null_review' => true ];
     885                }
     886            }
     887        }
     888        return $results_array;
     889    }
     890
     891    /**
     892     * Retrieves A Question Setting
     893     *
     894     * Retrieves a setting stored in the question settings array
     895     *
     896     * @since  4.0.0
     897     * @param  int    $question_id The id of the question
     898     * @param  string $setting     The name of the setting
     899     * @return string The value stored for the setting
     900     */
     901    public function get_question_setting($question_id, $setting)
     902    {
     903        global $wpdb;
     904        $settings           = $wpdb->get_var($wpdb->prepare('SELECT question_settings FROM ' . $wpdb->prefix . 'mlw_questions WHERE question_id=%d', $question_id));
     905        $qmn_settings_array = maybe_unserialize($settings);
     906
     907        if (is_array($qmn_settings_array) && isset($qmn_settings_array[ $setting ])) {
     908            return $qmn_settings_array[ $setting ];
     909        } else {
     910            return '';
     911        }
     912    }
     913
     914    /**
     915     * Registers Addon Settings Tab
     916     *
     917     * Registers a new tab on the addon settings page
     918     *
     919     * @since  4.0.0
     920     * @param  string $title    The name of the tab
     921     * @param  string $function The function that displays the tab's content
     922     * @return void
     923     */
     924    public function register_addon_settings_tab($title, $function)
     925    {
     926        $slug               = strtolower(str_replace(' ', '-', $title));
     927        $new_tab            = [
     928            'title'    => $title,
     929            'function' => $function,
     930            'slug'     => $slug,
     931        ];
     932        $this->addon_tabs[] = $new_tab;
     933    }
     934
     935    /**
     936     * Retrieves Addon Settings Tab Array
     937     *
     938     * Retrieves the array of titles and functions of the registered tabs
     939     *
     940     * @since  4.0.0
     941     * @return array The array of registered tabs
     942     */
     943    public function get_addon_tabs()
     944    {
     945        return $this->addon_tabs;
     946    }
     947
     948    /**
     949     * Registers Stats Tab
     950     *
     951     * Registers a new tab on the stats page
     952     *
     953     * @since  4.3.0
     954     * @param  string $title    The name of the tab
     955     * @param  string $function The function that displays the tab's content
     956     * @return void
     957     */
     958    public function register_stats_settings_tab($title, $function)
     959    {
     960        $slug               = strtolower(str_replace(' ', '-', $title));
     961        $new_tab            = [
     962            'title'    => $title,
     963            'function' => $function,
     964            'slug'     => $slug,
     965        ];
     966        $this->stats_tabs[] = $new_tab;
     967    }
     968
     969    /**
     970     * Retrieves Stats Tab Array
     971     *
     972     * Retrieves the array of titles and functions of the registered tabs
     973     *
     974     * @since  4.3.0
     975     * @return array The array of registered tabs
     976     */
     977    public function get_stats_tabs()
     978    {
     979        return $this->stats_tabs;
     980    }
     981
     982    /**
     983     * Registers tabs for the Admin Results page
     984     *
     985     * Registers a new tab on the admin results page
     986     *
     987     * @since  5.0.0
     988     * @param  string $title    The name of the tab
     989     * @param  string $function The function that displays the tab's content
     990     * @return void
     991     */
     992    public function register_admin_results_tab($title, $function, $priority = 10)
     993    {
     994        $slug                       = strtolower(str_replace(' ', '-', $title));
     995        $new_tab                    = [
     996            'title'    => $title,
     997            'function' => $function,
     998            'slug'     => $slug,
     999            'priority' => $priority,
     1000        ];
     1001        $this->admin_results_tabs[] = $new_tab;
     1002    }
     1003
     1004    /**
     1005     * Retrieves Admin Results Tab Array
     1006     *
     1007     * Retrieves the array of titles and functions for the tabs registered for the admin results page
     1008     *
     1009     * @since  5.0.0
     1010     * @return array The array of registered tabs
     1011     */
     1012    public function get_admin_results_tabs()
     1013    {
     1014        /**
     1015         * Sort tabs by priority
     1016         */
     1017        array_multisort(array_column($this->admin_results_tabs, 'priority'), SORT_ASC, $this->admin_results_tabs);
     1018        return apply_filters('qmn_admin_results_tabs', $this->admin_results_tabs);
     1019    }
     1020
     1021    /**
     1022     * Registers Results Tab
     1023     *
     1024     * Registers a new tab on the results page
     1025     *
     1026     * @since  4.1.0
     1027     * @param  string $title    The name of the tab
     1028     * @param  string $function The function that displays the tab's content
     1029     * @return void
     1030     */
     1031    public function register_results_settings_tab($title, $function)
     1032    {
     1033        $slug                 = strtolower(str_replace(' ', '-', $title));
     1034        $new_tab              = [
     1035            'title'    => $title,
     1036            'function' => $function,
     1037            'slug'     => $slug,
     1038        ];
     1039        $this->results_tabs[] = $new_tab;
     1040    }
     1041
     1042    /**
     1043     * Retrieves Results Tab Array
     1044     *
     1045     * Retrieves the array of titles and functions of the registered tabs
     1046     *
     1047     * @since  4.1.0
     1048     * @return array The array of registered tabs
     1049     */
     1050    public function get_results_tabs()
     1051    {
     1052        return $this->results_tabs;
     1053    }
     1054
     1055    /**
     1056     * Registers Quiz Settings Tab
     1057     *
     1058     * Registers a new tab on the quiz settings page
     1059     *
     1060     * @since  4.0.0
     1061     * @param  string $title    The name of the tab
     1062     * @param  string $function The function that displays the tab's content
     1063     * @return void
     1064     */
     1065    public function register_quiz_settings_tabs($title, $function, $slug = '')
     1066    {
     1067        if ('' === $slug) {
     1068            $slug = strtolower(str_replace(' ', '-', $title));
     1069        }
     1070        $new_tab               = [
     1071            'title'    => $title,
     1072            'function' => $function,
     1073            'slug'     => $slug,
     1074        ];
     1075        $this->settings_tabs[] = $new_tab;
     1076    }
     1077
     1078    /**
     1079     * Echos Registered Tabs Title Link
     1080     *
     1081     * Echos the title link of the registered tabs
     1082     *
     1083     * @since  4.0.0
     1084     * @return array The array of registered tabs
     1085     */
     1086    public function get_settings_tabs()
     1087    {
     1088        return apply_filters('qmn_quiz_setting_tabs', $this->settings_tabs);
     1089    }
     1090
     1091    /**
     1092     * global animatiocv array return
     1093     *
     1094     * @since 4.7.1
     1095     */
     1096    public function quiz_animation_effect()
     1097    {
     1098
     1099        return [
     1100            [
     1101                'label' => __('bounce', 'quiz-master-next'),
     1102                'value' => 'bounce',
     1103            ],
     1104            [
     1105                'label' => __('flash', 'quiz-master-next'),
     1106                'value' => 'flash',
     1107            ],
     1108            [
     1109                'label' => __('pulse', 'quiz-master-next'),
     1110                'value' => 'pulse',
     1111            ],
     1112            [
     1113                'label' => __('rubberBand', 'quiz-master-next'),
     1114                'value' => 'rubberBand',
     1115            ],
     1116            [
     1117                'label' => __('shake', 'quiz-master-next'),
     1118                'value' => 'shake',
     1119            ],
     1120            [
     1121                'label' => __('swing', 'quiz-master-next'),
     1122                'value' => 'swing',
     1123            ],
     1124            [
     1125                'label' => __('tada', 'quiz-master-next'),
     1126                'value' => 'tada',
     1127            ],
     1128            [
     1129                'label' => __('wobble', 'quiz-master-next'),
     1130                'value' => 'wobble',
     1131            ],
     1132            [
     1133                'label' => __('jello', 'quiz-master-next'),
     1134                'value' => 'jello',
     1135            ],
     1136            [
     1137                'label' => __('heartBeat', 'quiz-master-next'),
     1138                'value' => 'heartBeat',
     1139            ],
     1140            [
     1141                'label' => __('Select Quiz Animation', 'quiz-master-next'),
     1142                'value' => '',
     1143            ],
     1144        ];
     1145    }
     1146
     1147    /**
     1148     * converts dates into preferred date format
     1149     *
     1150     * @since  7.3.3
     1151     * @param  array $qsm_qna_array The array of results for the quiz
     1152     * @uses   QMNQuizManager:submit_results() submits and displays results
     1153     * @uses   qsm_generate_results_details_tab() generates admin results page
     1154     * @return array $qsm_qna_array date formatted array of results for the quiz
     1155     */
     1156
     1157    public function convert_to_preferred_date_format($qsm_qna_array)
     1158    {
     1159        global $mlwQuizMasterNext;
     1160        $quiz_options        = $mlwQuizMasterNext->quiz_settings->get_quiz_options();
     1161        $qsm_quiz_settings   = maybe_unserialize($quiz_options->quiz_settings);
     1162        $qsm_quiz_options    = maybe_unserialize($qsm_quiz_settings['quiz_options']);
     1163        $qsm_global_settings = get_option('qsm-quiz-settings');
     1164        // check if preferred date format is set at quiz level or plugin level. Default to WP date format otherwise
     1165        if (isset($qsm_quiz_options['preferred_date_format'])) {
     1166            $preferred_date_format = $qsm_quiz_options['preferred_date_format'];
     1167        } elseif (isset($qsm_global_settings['preferred_date_format'])) {
     1168            $preferred_date_format = isset($qsm_global_settings['preferred_date_format']);
     1169        } else {
     1170            $preferred_date_format = get_option('date_format');
     1171        }
     1172        // filter date format
     1173        $GLOBALS['qsm_date_format'] = apply_filters('qms_preferred_date_format', $preferred_date_format);
     1174
     1175        $qsm_qna_array = $this->convert_contacts_to_preferred_date_format($qsm_qna_array);
     1176        $qsm_qna_array = $this->convert_answers_to_preferred_date_format($qsm_qna_array);
     1177        $this->convert_questions_to_preferred_date_format();
     1178
     1179        return $qsm_qna_array;
     1180    }
     1181
     1182    /**
     1183     * converts contacts into preferred date format
     1184     *
     1185     * @since  7.3.3
     1186     * @param  array $qsm_qna_array The array of results for the quiz
     1187     * @uses   convert_to_preferred_date_format()
     1188     * @return array $qsm_qna_array date formatted array of results for the quiz
     1189     */
     1190
     1191    public function convert_contacts_to_preferred_date_format($qsm_qna_array)
     1192    {
     1193
     1194        $qsm_contact_array = $qsm_qna_array['contact'];
     1195        foreach ($qsm_contact_array as $qsm_contact_id => $qsm_contact) {
     1196            if (( isset($qsm_contact['type']) && 'date' === $qsm_contact['type'] ) && ( isset($qsm_contact['value']) && '' !== $qsm_contact['value'] ) && null !== $GLOBALS['qsm_date_format']) {
     1197                $qsm_qna_array['contact'][ $qsm_contact_id ]['value'] = date_i18n($GLOBALS['qsm_date_format'], strtotime(( $qsm_contact['value'] )));
     1198            }
     1199        }
     1200        return $qsm_qna_array;
     1201    }
     1202
     1203    /**
     1204     * converts answers into preferred date format
     1205     *
     1206     * @since  7.3.3
     1207     * @param  array $qsm_qna_array The array of results for the quiz
     1208     * @uses   convert_to_preferred_date_format()
     1209     * @return array $qsm_qna_array date formatted array of results for the quiz
     1210     */
     1211
     1212    public function convert_answers_to_preferred_date_format($qsm_qna_array)
     1213    {
     1214
     1215        $qsm_qna_list = $qsm_qna_array['question_answers_array'];
     1216        foreach ($qsm_qna_list as $qna_id => $qna) {
     1217            if ('12' === $qna['question_type'] && null !== $GLOBALS['qsm_date_format']) {
     1218                $qsm_qna_array['question_answers_array'][ $qna_id ]['1'] = date_i18n($GLOBALS['qsm_date_format'], strtotime(( $qna['1'] )));
     1219                $qsm_qna_array['question_answers_array'][ $qna_id ]['2'] = date_i18n($GLOBALS['qsm_date_format'], strtotime(( $qna['2'] )));
     1220            }
     1221        }
     1222        return $qsm_qna_array;
     1223    }
     1224
     1225    /**
     1226     * converts questions into preferred date format
     1227     *
     1228     * @since  7.3.3
     1229     * @param  array $qsm_qna_array The array of results for the quiz
     1230     * @uses   convert_to_preferred_date_format()
     1231     * @return array $qsm_qna_array date formatted array of results for the quiz
     1232     */
     1233
     1234    public function convert_questions_to_preferred_date_format()
     1235    {
     1236        if (! function_exists('qsm_convert_question_array_date_format')) {
     1237            function qsm_convert_question_array_date_format($questions)
     1238            {
     1239                foreach ($questions as $question_id => $question_to_convert) {
     1240                    if ('12' === $question_to_convert['question_type_new']) {
     1241                        foreach ($question_to_convert['answers'] as $answer_id => $answer_value) {
     1242                            $questions[ $question_id ]['answers'][ $answer_id ][0] = date_i18n($GLOBALS['qsm_date_format'], strtotime($answer_value[0]));
     1243                        }
     1244                    }
     1245                }
     1246                return $questions;
     1247            }
     1248        }
     1249        add_filter('qsm_load_questions_by_pages', 'qsm_convert_question_array_date_format');
     1250    }
     1251
     1252    /**
     1253     *
     1254     *
     1255     * @since  7.3.5
     1256     * @param  array
     1257     * @uses
     1258     * @return array
     1259     */
     1260
     1261    public function qsm_results_css_inliner($html)
     1262    {
     1263
     1264        global $mlwQuizMasterNext;
     1265        $grading = $mlwQuizMasterNext->pluginHelper->get_section_setting('quiz_options', 'system');
     1266        $wr_sign = 1 != $grading ? "&#x2715;&nbsp;" : "";
     1267
     1268        $html    = str_replace('<br/>', '<br>', $html);
     1269        $html    = str_replace('<br />', '<br>', $html);
     1270        $html    = str_replace("class='qmn_question_answer", "style='margin-bottom:30px' class='", $html);
     1271        $html    = preg_replace('/<span class="qsm-text-simple-option(.*?)">(.*?)<\/span>/', "<span style='color:#808080;display:block;margin-bottom:5px;'>&#8226;&nbsp;$2</span>", $html);
     1272        $html    = preg_replace('/<span class="qsm-text-wrong-option(.*?)">(.*?)<\/span>/', "<span style='color:red;display:block;margin-bottom:5px;'>&#x2715;&nbsp;$2</span>", $html);
     1273        $html    = preg_replace('/<span class="qmn_user_incorrect_answer(.*?)">(.*?)<\/span>/', "<span style='color:red;display:block;margin-bottom:5px;'>".$wr_sign."$2</span>", $html);
     1274        $html    = preg_replace('/<span class="qsm-text-correct-option(.*?)">(.*?)<\/span>/', "<span style='color:green;display:block;margin-bottom:5px;'>&#10003;&nbsp;$2</span>", $html);
     1275        $html    = preg_replace('/<span class="qmn_user_correct_answer(.*?)">(.*?)<\/span>/', "<span style='color:green;display:block;margin-bottom:5px;'>&#10003;&nbsp;$2</span>", $html);
     1276
     1277        return $html;
     1278    }
     1279
     1280    /** */
     1281    public function categorize_question_types()
     1282    {
     1283        $question_type_categorized   = [];
     1284        $question_type_uncategorized = [];
     1285        foreach ($this->question_types as $question_type) {
     1286            $is_categorized = isset($question_type ['category']) && '' !== $question_type ['category'];
     1287            if ($is_categorized) {
     1288                $question_type_categorized[ $question_type ['category'] ] [ $question_type['slug'] ] = [
     1289                    'slug'     => $question_type['slug'],
     1290                    'name'     => $question_type['name'],
     1291                    'disabled' => (isset($question_type['display']) && '-1' == $question_type['display']) ? true : false,
     1292                ];
     1293            } else {
     1294                $question_type_uncategorized['uncategorized'][ $question_type['slug'] ] = [
     1295                    'slug'     => $question_type['slug'],
     1296                    'name'     => $question_type['name'],
     1297                    'disabled' => (isset($question_type['display']) && '-1' == $question_type['display']) ? true : false,
     1298                ];
     1299            }
     1300        }
     1301        $question_type_categorized = array_merge($question_type_categorized, $question_type_uncategorized);
     1302        return $question_type_categorized;
     1303    }
     1304
     1305    public function description_array()
     1306    {
     1307        return [
     1308            [
     1309                'question_type_id' => 11,
     1310                'description'      => __('For this question type, users will see a file upload field on front end.', 'quiz-master-next'),
     1311            ],
     1312            [
     1313                'question_type_id' => '14',
     1314                'description'      => __('Use %BLANK% variable in the description field to display input boxes.', 'quiz-master-next'),
     1315            ],
     1316            [
     1317                'question_type_id' => '12',
     1318                'description'      => __('For this question type, users will see a date input field on front end.', 'quiz-master-next'),
     1319            ],
     1320            [
     1321                'question_type_id' => '3',
     1322                'description'      => __('For this question type, users will see a standard input box on front end.', 'quiz-master-next'),
     1323            ],
     1324            [
     1325                'question_type_id' => '5',
     1326                'description'      => __('For this question type, users will see a standard textarea input box on front end.', 'quiz-master-next'),
     1327            ],
     1328            [
     1329                'question_type_id' => '6',
     1330                'description'      => __('Displays a simple section on front end. Description is mandatory. ', 'quiz-master-next'),
     1331            ],
     1332            [
     1333                'question_type_id' => '7',
     1334                'description'      => __('For this question type, users will see an input box which accepts only number values on front end.', 'quiz-master-next'),
     1335            ],
     1336            [
     1337                'question_type_id' => '8',
     1338                'description'      => __("For this question type, users will see a checkbox on front end. The text in description field will act like it's label.", 'quiz-master-next'),
     1339            ],
     1340            [
     1341                'question_type_id' => '9',
     1342                'description'      => __('For this question type, users will see a Captcha field on front end.', 'quiz-master-next'),
     1343            ],
     1344            // array(
     1345            // 'question_type_id' => '13',
     1346            // 'description'      => __( 'Use points based grading system for Polar questions.', 'quiz-master-next' ),
     1347            // ),
     1348        ];
     1349    }
     1350
     1351    public function qsm_get_limited_options($options, $limit)
     1352    {
     1353        $correct = array_filter($options, fn($o, $k) => 1 == $o[2], ARRAY_FILTER_USE_BOTH);
     1354        $incorrect = array_filter($options, fn($o, $k) => 0 == $o[2], ARRAY_FILTER_USE_BOTH);
     1355        shuffle($incorrect);
     1356        $final = array_merge($correct, array_slice($incorrect, 0, $limit - count($correct)));
     1357        shuffle($final);
     1358        $final_keys = array_map(fn($k) => array_search($k, array_values($options), true), $final);
     1359        return [
     1360            'final'             => $final,
     1361            'answer_limit_keys' => implode(',', $final_keys),
     1362        ];
     1363    }
     1364
     1365    public function qsm_get_limited_options_by_keys($options, $keys)
     1366    {
     1367        return array_map(fn($k) => $options[ $k ], explode(',', $keys));
     1368    }
    13191369}
  • quiz-master-next/trunk/php/classes/class-qmn-quiz-manager.php

    r3357783 r3372406  
    353353
    354354        $qmn_quiz_options = $has_proper_quiz['qmn_quiz_options'];
     355        $qmn_quiz_options = apply_filters('qsm_quiz_option_before', $qmn_quiz_options);
    355356        $return_display = '';
    356357
     
    12941295        }
    12951296        $is_contact_fields_enabled = array_filter(
    1296             $contact_fields,
     1297            is_array( $contact_fields ) ? $contact_fields : [],
    12971298            function( $sub ) {
    12981299                return isset( $sub['enable'] ) && 'true' === $sub['enable'];
     
    16731674            }
    16741675        }
     1676       
     1677        $errors = apply_filters( 'qsm_validate_contact_field', $errors, $contact_form, $request );
    16751678        return empty( $errors ) ? 1 : "<strong>" . __( 'There was an error with your submission:', 'quiz-master-next' ) . "</strong><ul style='left: -20px; position: relative;'><li>" . implode( "</li><li>", $errors ) . "</li></ul>";
    16761679    }
  • quiz-master-next/trunk/php/classes/class-qsm-install.php

    r3341668 r3372406  
    180180        $field_array = array(
    181181            'id'         => 'randomness_order',
    182             'label'      => __( 'Randomize Question', 'quiz-master-next' ) . '<span class="qsm-opt-desc"> ' . __( 'Randomize the order of questions or answers every time the quiz loads', 'quiz-master-next' ) . ' </span>',
     182            'label'      => __( 'Randomization Mode', 'quiz-master-next' ) . '<span class="qsm-opt-desc"> ' . __( 'Randomize the order of questions or answers every time the quiz loads', 'quiz-master-next' ) . ' </span>',
    183183            'type'       => 'radio',
    184184            'options'    => array(
    185185                array(
    186                     'label' => __( 'Disabled', 'quiz-master-next' ),
     186                    'label' => __( 'None', 'quiz-master-next' ),
    187187                    'value' => 0,
    188188                ),
    189189                array(
    190                     'label' => __( 'Randomize question only', 'quiz-master-next' ),
     190                    'label' => __( 'Questions', 'quiz-master-next' ),
    191191                    'value' => 1,
    192192                ),
    193193                array(
    194                     'label' => __( 'Randomize answers only', 'quiz-master-next' ),
     194                    'label' => __( 'Answers', 'quiz-master-next' ),
    195195                    'value' => 3,
    196196                ),
    197197                array(
    198                     'label' => __( 'Randomize questions and their answers', 'quiz-master-next' ),
     198                    'label' => __( 'Questions & Answers', 'quiz-master-next' ),
    199199                    'value' => 2,
    200200                ),
  • quiz-master-next/trunk/php/classes/class-qsm-questions.php

    r3341668 r3372406  
    1111 * @since 5.2.0
    1212 */
    13 class QSM_Questions {
    14 
    15 
    16     /**
    17      * Loads single question using question ID
    18      *
    19      * @since  5.2.0
    20      * @param  int $question_id The ID of the question.
    21      * @return array The data for the question.
    22      */
    23     public static function load_question( $question_id ) {
    24         global $wpdb;
    25         $question_id = intval( $question_id );
    26         $question    = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}mlw_questions WHERE question_id = %d LIMIT 1", $question_id ), 'ARRAY_A' );
    27         if ( ! is_null( $question ) ) {
    28             $multicategories     = array();
    29             $multicategories_res = $wpdb->get_results( "SELECT `term_id` FROM `{$wpdb->prefix}mlw_question_terms` WHERE `question_id`='{$question['question_id']}' AND `taxonomy`='qsm_category'", ARRAY_A );
    30             if ( ! empty( $multicategories_res ) ) {
    31                 foreach ( $multicategories_res as $cat ) {
    32                     $multicategories[] = $cat['term_id'];
    33                 }
    34             }
    35             $question['multicategories'] = $multicategories;
    36             // Prepare answers.
    37             $answers = maybe_unserialize( $question['answer_array'] );
    38             if ( ! is_array( $answers ) ) {
    39                 $answers = array();
    40             }
    41             $question['answers'] = $answers;
    42 
    43             $settings = maybe_unserialize( $question['question_settings'] );
    44             if ( ! is_array( $settings ) ) {
    45                 $settings = array( 'required' => 1 );
    46             }
    47             $question['settings'] = $settings;
    48 
    49             return apply_filters( 'qsm_load_question', $question, $question_id );
    50         }
    51         return array();
    52     }
    53 
    54     /**
    55      *
    56      */
    57     public static function load_question_data( $question_id, $question_data ) {
    58         global $wpdb;
    59         return $wpdb->get_var("SELECT {$question_data} FROM {$wpdb->prefix}mlw_questions WHERE question_id = {$question_id} LIMIT 1");
    60     }
    61 
    62     /**
    63      * Loads questions for a quiz using the new page system
    64      *
    65      * @since  5.2.0
    66      * @param  int $quiz_id The ID of the quiz.
    67      * @return array The array of questions.
    68      */
    69     public static function load_questions_by_pages( $quiz_id, $caller = '' ) {
    70         // Prepares our variables.
    71         global $wpdb;
    72         global $mlwQuizMasterNext;
    73         $quiz_id      = intval( $quiz_id );
    74         $question_ids = array();
    75         $questions    = array();
    76         $page_for_ids = array();
    77 
    78         // Gets the pages for the quiz.
    79         $mlwQuizMasterNext->pluginHelper->prepare_quiz( $quiz_id );
    80         $pages = $mlwQuizMasterNext->pluginHelper->get_quiz_setting( 'pages', array(), $caller );
    81 
    82         // Get all question IDs needed.
    83         if ( ! empty( $pages ) ) {
    84             $total_pages = count( $pages );
    85             for ( $i = 0; $i < $total_pages; $i++ ) {
    86                 foreach ( $pages[ $i ] as $question ) {
    87                     $question_id                  = intval( $question );
    88                     $question_ids[]               = $question_id;
    89                     $page_for_ids[ $question_id ] = $i;
    90                 }
    91             }
    92         }
    93 
    94         // If we have any question IDs, get the questions.
    95         if ( count( $question_ids ) > 0 ) {
    96 
    97             $question_sql = implode( ', ', $question_ids );
    98 
    99             // Get all questions.
    100             $question_array = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}mlw_questions WHERE question_id IN (%1s)", $question_sql ), 'ARRAY_A' );
    101             // Loop through questions and prepare serialized data.
    102             foreach ( $question_array as $question ) {
    103                 $multicategories = self::get_question_categories( $question['question_id'] );
    104                 // get_question_categories
    105 
    106                 $question['multicategories']       = isset( $multicategories['category_tree'] ) && ! empty( $multicategories['category_tree'] ) ? array_keys( $multicategories['category_name'] ) : array();
    107                 $question['multicategoriesobject'] = isset( $multicategories['category_tree'] ) && ! empty( $multicategories['category_tree'] ) ? $multicategories['category_tree'] : array();
    108                 // Prepares settings.
    109                 $settings = maybe_unserialize( $question['question_settings'] );
    110                 if ( ! is_array( $settings ) ) {
    111                     $settings = array( 'required' => 1 );
    112                 }
    113                 $question['settings'] = $settings;
    114                 // Prepare answers.
    115                 $answers = maybe_unserialize( $question['answer_array'] );
    116                 if ( ! is_array( $answers ) ) {
    117                     $answers = array();
    118                 }
    119                 $question['answers'] = self::sanitize_answers( $answers, $settings );
    120                 // Get the page.
    121                 $question_id      = intval( $question['question_id'] );
    122                 $question['page'] = intval( $page_for_ids[ $question_id ] );
    123 
    124                 $questions[ $question_id ] = $question;
    125             }
    126         } else {
    127             // If we do not have pages on this quiz yet, use older load_questions and add page to them.
    128             $questions = self::load_questions( $quiz_id, $caller );
    129             foreach ( $questions as $key => $question ) {
    130                 $questions[ $key ]['page'] = isset( $question['page'] ) ? $question['page'] : 0;
    131             }
    132         }
    133         return apply_filters( 'qsm_load_questions_by_pages', $questions, $quiz_id );
    134     }
    135 
    136     /**
    137      * Loads questions for a quiz
    138      *
    139      * @since  5.2.0
    140      * @param  int $quiz_id The ID of the quiz.
    141      * @return array The array of questions.
    142      */
    143     public static function load_questions( $quiz_id, $caller = '' ) {
    144 
    145         global $wpdb;
    146         $question_array = array();
    147 
    148         // Get all questions.
    149         if ( 0 !== $quiz_id ) {
    150             $quiz_id   = intval( $quiz_id );
    151             $questions = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}mlw_questions WHERE quiz_id=%d AND deleted='0' ORDER BY question_order ASC", $quiz_id ), 'ARRAY_A' );
    152         } else {
    153             $questions = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}mlw_questions WHERE deleted='0' ORDER BY question_order ASC", 'ARRAY_A' );
    154         }
    155 
    156         // Loop through questions and prepare serialized data.
    157         foreach ( $questions as $question ) {
    158             $multicategories     = array();
    159             $multicategories_res = $wpdb->get_results( "SELECT `term_id` FROM `{$wpdb->prefix}mlw_question_terms` WHERE `question_id`='{$question['question_id']}' AND `taxonomy`='qsm_category'", ARRAY_A );
    160             if ( ! empty( $multicategories_res ) ) {
    161                 foreach ( $multicategories_res as $cat ) {
    162                     $multicategories[] = $cat['term_id'];
    163                 }
    164             }
    165             $question['multicategories'] = $multicategories;
    166             // Prepare answers.
    167             $answers = maybe_unserialize( $question['answer_array'] );
    168             if ( ! is_array( $answers ) ) {
    169                 $answers = array();
    170             }
    171             $question['answers'] = $answers;
    172 
    173             $settings = maybe_unserialize( $question['question_settings'] );
    174             if ( ! is_array( $settings ) ) {
    175                 $settings = array( 'required' => 1 );
    176             }
    177             $question['settings'] = $settings;
    178 
    179             $question_array[ $question['question_id'] ] = $question;
    180         }
    181         $question_array = ! empty( $caller ) ? $question_array : array_filter(
    182             $question_array,
    183             function ( $question ) {
    184                 return ! isset( $question['settings']['isPublished'] ) || 0 !== intval( $question['settings']['isPublished'] );
    185             }
    186         );
    187         return apply_filters( 'qsm_load_questions', $question_array, $quiz_id );
    188     }
    189 
    190     /**
    191      * Creates a new question
    192      *
    193      * @since  5.2.0
    194      * @param  array $data     The question data.
    195      * @param  array $answers  The answers for the question.
    196      * @param  array $settings Any settings for the question.
    197      * @throws Exception Throws exception if wpdb query results in error.
    198      * @return int The ID of the question that was created.
    199      */
    200     public static function create_question( $data, $answers = array(), $settings = array() ) {
    201         return self::create_save_question( $data, $answers, $settings );
    202     }
    203 
    204     /**
    205      * Saves a question
    206      *
    207      * @since  5.2.0
    208      * @param  int   $question_id The ID of the question to be saved.
    209      * @param  array $data        The question data.
    210      * @param  array $answers     The answers for the question.
    211      * @param  array $settings    Any settings for the question.
    212      * @throws Exception Throws exception if wpdb query results in error.
    213      * @return int The ID of the question that was saved.
    214      */
    215     public static function save_question( $question_id, $data, $answers = array(), $settings = array() ) {
    216         $data['ID'] = intval( $question_id );
    217         return self::create_save_question( $data, $answers, $settings, false );
    218     }
    219 
    220     /**
    221      * Deletes a question
    222      *
    223      * @since  5.2.0
    224      * @param  int $question_id The ID for the question.
    225      * @throws Exception Throws exception if wpdb query results in error.
    226      * @return bool True if successful
    227      */
    228     public static function delete_question( $question_id ) {
    229         global $wpdb;
    230 
    231         $results = $wpdb->update(
    232             $wpdb->prefix . 'mlw_questions',
    233             array(
    234                 'deleted' => 1,
    235             ),
    236             array( 'question_id' => intval( $question_id ) ),
    237             array(
    238                 '%d',
    239             ),
    240             array( '%d' )
    241         );
    242 
    243         if ( false === $results ) {
    244             $msg = $wpdb->last_error . ' from ' . $wpdb->last_query;
    245             $mlwQuizMasterNext->log_manager->add( 'Error when deleting question', $msg, 0, 'error' );
    246             throw new Exception( esc_html( $msg ) );
    247         }
    248 
    249         return true;
    250     }
    251 
    252     /**
    253      * Creates or saves a question
    254      *
    255      * This is used internally. Use create_question or save_question instead.
    256      *
    257      * @since  5.2.0
    258      * @param  array $data        The question data.
    259      * @param  array $answers     The answers for the question.
    260      * @param  array $settings    Any settings for the question.
    261      * @param  bool  $is_creating True if question is being created, false if being saved.
    262      * @throws Exception Throws exception if wpdb query results in error.
    263      * @return int The ID of the question that was created/saved.
    264      */
    265     private static function create_save_question( $data, $answers, $settings, $is_creating = true ) {
    266         global $wpdb, $mlwQuizMasterNext;
    267 
    268         // Prepare defaults and parse.
    269         $defaults = array(
    270             'quiz_id'         => 0,
    271             'type'            => '0',
    272             'name'            => '',
    273             'answer_info'     => '',
    274             'comments'        => '1',
    275             'hint'            => '',
    276             'order'           => 1,
    277             'category'        => '',
    278             'multicategories' => '',
    279             'linked_question' => '',
    280         );
    281         $data     = wp_parse_args( $data, $defaults );
    282 
    283         $defaults = array(
    284             'required' => 1,
    285         );
    286         $settings = wp_parse_args( $settings, $defaults );
    287 
    288         $sanitize_answers = self::sanitize_answers( $answers, $settings );
    289         foreach ( $sanitize_answers as $key => $answer ) {
    290             $answers_array = array(
    291                 htmlspecialchars( $answer[0], ENT_QUOTES ),
    292                 floatval( $answer[1] ),
    293                 intval( $answer[2] ),
    294             );
    295             if ( isset( $answer[3] ) ) {
    296                 array_push( $answers_array, htmlspecialchars( $answer[3], ENT_QUOTES ) );
    297             }
    298             $sanitize_answers[ $key ] = $answers_array;
    299         }
    300         $answers = apply_filters( 'qsm_answers_before_save', $sanitize_answers, $answers, $data, $settings );
    301 
    302         $question_name             = htmlspecialchars( $mlwQuizMasterNext->sanitize_html( $data['name'] ), ENT_QUOTES );
    303         $trim_question_description = apply_filters( 'qsm_trim_question_description', true );
    304         if ( $trim_question_description ) {
    305             $question_name = trim( preg_replace( '/\s+/', ' ', $question_name ) );
    306         }
    307         $linked_question = sanitize_text_field( $data['linked_question'] );
    308         $linked_questions_array = array();
    309 
    310         if ( ( $is_creating && isset($data['is_linking']) && 1 <= $data['is_linking'] ) || ! $is_creating ) {
    311             // Convert the existing linked_question into an array
    312             $linked_questions_array = array_filter(array_map('trim', explode(',', $linked_question)));
    313             // Add the new value if it's not already in the array
    314             if ( isset($data['is_linking'] ) && ! in_array($data['is_linking'], $linked_questions_array, true) ) {
    315                 $linked_questions_array[] = $data['is_linking'];
    316             }
    317             $linked_questions_array = array_filter($linked_questions_array);
    318             // Join back into a comma-separated string
    319             $linked_question = implode(',', $linked_questions_array);
    320         } elseif ( isset($data['is_linking']) && 0 == $data['is_linking'] ) {
    321             $linked_question = '';
    322         }
    323 
    324         $values = array(
    325             'quiz_id'              => intval( $data['quiz_id'] ),
    326             'question_name'        => $question_name,
    327             'answer_array'         => maybe_serialize( $answers ),
    328             'question_answer_info' => $mlwQuizMasterNext->sanitize_html( $data['answer_info'] ),
    329             'comments'             => sanitize_text_field( $data['comments'] ),
    330             'hints'                => sanitize_text_field( $data['hint'] ),
    331             'question_order'       => intval( $data['order'] ),
    332             'question_type_new'    => sanitize_text_field( $data['type'] ),
    333             'question_settings'    => maybe_serialize( $settings ),
    334             'category'             => sanitize_text_field( $data['category'] ),
    335             'linked_question'      => $linked_question,
    336             'deleted'              => 0,
    337         );
    338         $values = apply_filters( 'qsm_save_question_data', $values );
    339 
    340         $types = array(
    341             '%d',
    342             '%s',
    343             '%s',
    344             '%s',
    345             '%d',
    346             '%s',
    347             '%d',
    348             '%s',
    349             '%s',
    350             '%s',
    351             '%s',
    352             '%d',
    353         );
    354 
    355         if ( $is_creating ) {
    356             $results     = $wpdb->insert(
    357                 $wpdb->prefix . 'mlw_questions',
    358                 $values,
    359                 $types
    360             );
    361             $question_id = $wpdb->insert_id;
    362         } else {
    363             $question_id = intval( $data['ID'] );
    364             $results     = $wpdb->update(
    365                 $wpdb->prefix . 'mlw_questions',
    366                 $values,
    367                 array( 'question_id' => $question_id ),
    368                 $types,
    369                 array( '%d' )
    370             );
    371         }
    372 
    373         if ( false === $results ) {
    374             $msg = $wpdb->last_error . ' from ' . $wpdb->last_query;
    375             $mlwQuizMasterNext->log_manager->add( 'Error when creating/saving question', $msg, 0, 'error' );
    376             throw new Exception( esc_html( $msg ) );
    377         }
    378 
    379         $base_question_id = $question_id;
    380         $quiz_questions_array = array();
    381         $quiz_questions_array[ intval( $data['quiz_id'] ) ] = $question_id;
    382         $linked_questions_array[] = $question_id;
    383         if ( isset($linked_question) && "" != $linked_question ) {
    384             // preparing array for quiz question id
    385             $imploded_question_ids = implode( ',', array_unique($linked_questions_array) );
    386             if ( ! empty($linked_questions_array) ) {
    387                 $quiz_results = $wpdb->get_results( "SELECT `quiz_id`, `question_id` FROM `{$wpdb->prefix}mlw_questions` WHERE `question_id` IN (" . $imploded_question_ids . ")" );
    388                 foreach ( $quiz_results as $key => $value ) {
    389                     $quiz_questions_array[ $value->quiz_id ] = $value->question_id;
    390                 }
    391             }
    392             $values['linked_question'] = $imploded_question_ids;
    393         }
    394         $question_terms_table = $wpdb->prefix . 'mlw_question_terms';
    395         foreach ( $quiz_questions_array as $quiz_id => $question_id_loop ) {
    396             $values['quiz_id'] = intval( $quiz_id );
    397             $wpdb->update(
    398                 $wpdb->prefix . 'mlw_questions',
    399                 $values,
    400                 array( 'question_id' => intval($question_id_loop) ),
    401                 $types,
    402                 array( '%d' )
    403             );
    404 
    405             /**
    406              * Process Question Categories
    407              */
    408 
    409             $wpdb->delete(
    410                 $question_terms_table,
    411                 array(
    412                     'question_id' => $question_id_loop,
    413                     'taxonomy'    => 'qsm_category',
    414                 )
    415             );
    416             if ( ! empty( $data['multicategories'] ) ) {
    417                 foreach ( $data['multicategories'] as $term_id ) {
    418                     $term_rel_data = array(
    419                         'question_id' => $question_id_loop,
    420                         'quiz_id'     => intval( $quiz_id ),
    421                         'term_id'     => $term_id,
    422                         'taxonomy'    => 'qsm_category',
    423                     );
    424                     // Check if the data already exists in the table
    425                     $data_exists = $wpdb->get_row($wpdb->prepare("SELECT * FROM $question_terms_table WHERE question_id = %s AND quiz_id = %s AND term_id = %s AND taxonomy = %s", $question_id_loop, intval( $quiz_id ), $term_id, 'qsm_category' ));
    426                     if ( ! $data_exists ) {
    427                         $wpdb->insert( $question_terms_table, $term_rel_data );
    428                     }
    429                 }
    430             }
    431 
    432             /**
    433              * Hook after saving question
    434              */
    435 
    436             if ( $is_creating && $base_question_id == $question_id_loop ) {
    437                 do_action( 'qsm_question_added', $question_id_loop, $values );
    438             } else {
    439                 do_action( 'qsm_question_updated', $question_id_loop, $values );
    440             }
    441             do_action( 'qsm_saved_question', $question_id_loop, $values );
    442 
    443         }
    444         return $base_question_id;
    445     }
    446 
    447     /**
    448      * Creates or saves a question
    449      *
    450      * sanitizes answers
    451      *
    452      * @since  7.3.5
    453      * @param  array $answers The answers for the question.
    454      * @return array sanitized $answers The answers for the question.
    455      */
    456     public static function sanitize_answers( $answers, $settings ) {
    457         global $mlwQuizMasterNext;
    458         foreach ( $answers as $key => $answer ) {
    459             if ( isset($answer[0]) ) {
    460                 if ( isset( $settings['answerEditor'] ) && 'rich' == $settings['answerEditor'] ) {
    461                     $answer[0] = $mlwQuizMasterNext->sanitize_html( $answer[0] );
    462                 } else {
    463                     $answer[0] = $mlwQuizMasterNext->sanitize_html( $answer[0], false );
    464                 }
    465                 $answers[ $key ] = $answer;
    466             }
    467         }
    468 
    469         return $answers;
    470     }
    471 
    472     /**
    473      * Get categories for a quiz
    474      *
    475      * @since  7.2.1
    476      * @param  int $quiz_id The ID of the quiz.
    477      * @return array The array of categories.
    478      */
    479     public static function get_quiz_categories( $quiz_id = 0 ) {
    480         global $wpdb;
    481         $categories = array();
    482         if ( 0 !== $quiz_id ) {
    483             $questions      = self::load_questions_by_pages( $quiz_id );
    484             $question_ids   = array_column( $questions, 'question_id' );
    485             $question_ids   = implode( ',', $question_ids );
    486             $question_terms = $wpdb->get_results( "SELECT `term_id` FROM `{$wpdb->prefix}mlw_question_terms` WHERE `question_id` IN ({$question_ids}) AND `taxonomy`='qsm_category'", ARRAY_A );
    487             $term_ids       = ! empty( $question_terms ) ? array_unique( array_column( $question_terms, 'term_id' ) ) : array();
    488             $cat_array      = self::get_question_categories_from_quiz_id( $quiz_id );
    489             $enabled        = get_option( 'qsm_multiple_category_enabled' );
    490             if ( $enabled && 'cancelled' !== $enabled && ! empty( $cat_array ) ) {
    491                 $term_ids = array_unique( array_merge( $term_ids, $cat_array ) );
    492             }
    493 
    494             $categories = self::get_question_categories_from_term_ids( $term_ids );
    495         }
    496         return $categories;
    497     }
    498 
    499     /**
    500      * Get categories from quiz id
    501      *
    502      * @since  7.3.3
    503      * @param  int $quiz_id The ID of the quiz.
    504      * @return array The array of categories.
    505      */
    506     public static function get_question_categories_from_quiz_id( $quiz_id ) {
    507         $cat_array = array();
    508         $questions = self::load_questions_by_pages( $quiz_id );
    509         foreach ( $questions as $single_question ) {
    510             if ( isset( $single_question['multicategories'] ) && is_array( $single_question['multicategories'] ) ) {
    511                 foreach ( $single_question['multicategories'] as $cat_id ) {
    512                     $cat_array[] = $cat_id;
    513                 }
    514             }
    515         }
    516         return $cat_array;
    517     }
    518 
    519     /**
    520      * Get categories from term ids
    521      *
    522      * @since  7.3.3
    523      * @param  int $term_ids Term IDs of the quiz.
    524      * @return array The array of categories.
    525      */
    526     public static function get_question_categories_from_term_ids( $term_ids ) {
    527         $categories = array();
    528         if ( ! empty( $term_ids ) ) {
    529             $categories_names = array();
    530             $categories_tree  = array();
    531             $terms            = get_terms(
    532                 array(
    533                     'taxonomy'   => 'qsm_category',
    534                     'include'    => $term_ids,
    535                     'hide_empty' => false,
    536                     'orderby'    => '',
    537                     'order'      => '',
    538                 )
    539             );
    540             if ( ! empty( $terms ) ) {
    541                 foreach ( $terms as $tax ) {
    542                     $categories_names[ $tax->term_id ] = $tax->name;
    543                     $taxs[ $tax->parent ][]            = $tax;
    544                 }
    545                 $categories_tree = self::create_terms_tree( $taxs, isset( $taxs[0] ) ? $taxs[0] : reset( $taxs ) );
    546             }
    547             $categories = array(
    548                 'list' => $categories_names,
    549                 'tree' => $categories_tree,
    550             );
    551         }
    552         return $categories;
    553     }
    554 
    555     /**
    556      * Get categories for a Question
    557      *
    558      * @since  7.2.1
    559      * @param  int $quiz_id The ID of the quiz.
    560      * @return array The array of categories.
    561      */
    562     public static function get_question_categories( $question_id = 0 ) {
    563         global $wpdb;
    564         $categories_tree  = array();
    565         $categories_names = array();
    566         if ( 0 !== $question_id ) {
    567             $question_terms = $wpdb->get_results( "SELECT `term_id` FROM `{$wpdb->prefix}mlw_question_terms` WHERE `question_id`='{$question_id}' AND `taxonomy`='qsm_category'", ARRAY_A );
    568             if ( ! empty( $question_terms ) ) {
    569                 $term_ids = array_unique( array_column( $question_terms, 'term_id' ) );
    570                 if ( ! empty( $term_ids ) ) {
    571                     $terms = get_terms(
    572                         array(
    573                             'taxonomy'   => 'qsm_category',
    574                             'include'    => array_unique( $term_ids ),
    575                             'hide_empty' => false,
    576                             'orderby'    => '',
    577                             'order'      => '',
    578                         )
    579                     );
    580                     if ( ! empty( $terms ) ) {
    581                         foreach ( $terms as $tax ) {
    582                             $categories_names[ $tax->term_id ] = $tax->name;
    583                             $taxs[ $tax->parent ][]            = $tax;
    584                         }
    585                         $categories_tree = self::create_terms_tree( $taxs, isset( $taxs[0] ) ? $taxs[0] : reset( $taxs ) );
    586 
    587                     }
    588                 }
    589             }
    590         }
    591         return array(
    592             'category_name' => $categories_names,
    593             'category_tree' => $categories_tree,
    594         );
    595     }
    596     /**
    597      * Create tree structure of terms.
    598      *
    599      * @since 7.2.1
    600      */
    601     public static function create_terms_tree( &$list, $parent ) {
    602         $taxTree = array();
    603         if ( is_array($parent) ) {
    604             foreach ( $parent as $ind => $val ) {
    605                 if ( isset( $list[ $val->term_id ] ) ) {
    606                     $val->children = self::create_terms_tree( $list, $list[ $val->term_id ] );
    607                 }
    608                 $taxTree[] = $val;
    609             }
    610         }
    611         return $taxTree;
    612     }
    613 
     13class QSM_Questions
     14{
     15
     16
     17    /**
     18     * Loads single question using question ID
     19     *
     20     * @since  5.2.0
     21     * @param  int $question_id The ID of the question.
     22     * @return array The data for the question.
     23     */
     24    public static function load_question($question_id)
     25    {
     26        global $wpdb;
     27        $question_id = intval($question_id);
     28        $question    = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$wpdb->prefix}mlw_questions WHERE question_id = %d LIMIT 1", $question_id), 'ARRAY_A');
     29        if (! is_null($question)) {
     30            $multicategories     = [];
     31            $multicategories_res = $wpdb->get_results("SELECT `term_id` FROM `{$wpdb->prefix}mlw_question_terms` WHERE `question_id`='{$question['question_id']}' AND `taxonomy`='qsm_category'", ARRAY_A);
     32            if (! empty($multicategories_res)) {
     33                foreach ($multicategories_res as $cat) {
     34                    $multicategories[] = $cat['term_id'];
     35                }
     36            }
     37            $question['multicategories'] = $multicategories;
     38            // Prepare answers.
     39            $answers = maybe_unserialize($question['answer_array']);
     40            if (! is_array($answers)) {
     41                $answers = [];
     42            }
     43            $question['answers'] = $answers;
     44
     45            $settings = maybe_unserialize($question['question_settings']);
     46            if (! is_array($settings)) {
     47                $settings = [ 'required' => 1 ];
     48            }
     49            $question['settings'] = $settings;
     50
     51            return apply_filters('qsm_load_question', $question, $question_id);
     52        }
     53        return [];
     54    }
     55
     56    /**
     57     *
     58     */
     59    public static function load_question_data($question_id, $question_data)
     60    {
     61        global $wpdb;
     62        return $wpdb->get_var("SELECT {$question_data} FROM {$wpdb->prefix}mlw_questions WHERE question_id = {$question_id} LIMIT 1");
     63    }
     64
     65    /**
     66     * Loads questions for a quiz using the new page system
     67     *
     68     * @since  5.2.0
     69     * @param  int $quiz_id The ID of the quiz.
     70     * @return array The array of questions.
     71     */
     72    public static function load_questions_by_pages($quiz_id, $caller = '')
     73    {
     74        // Prepares our variables.
     75        global $wpdb;
     76        global $mlwQuizMasterNext;
     77        $quiz_id      = intval($quiz_id);
     78        $question_ids = [];
     79        $questions    = [];
     80        $page_for_ids = [];
     81
     82        // Gets the pages for the quiz.
     83        $mlwQuizMasterNext->pluginHelper->prepare_quiz($quiz_id);
     84        $pages = $mlwQuizMasterNext->pluginHelper->get_quiz_setting('pages', [], $caller);
     85
     86        // Get all question IDs needed.
     87        if (! empty($pages)) {
     88            $total_pages = count($pages);
     89            for ($i = 0; $i < $total_pages; $i++) {
     90                foreach ($pages[ $i ] as $question) {
     91                    $question_id                  = intval($question);
     92                    $question_ids[]               = $question_id;
     93                    $page_for_ids[ $question_id ] = $i;
     94                }
     95            }
     96        }
     97
     98        // If we have any question IDs, get the questions.
     99        if (count($question_ids) > 0) {
     100            $question_sql = implode(', ', $question_ids);
     101
     102            // Get all questions.
     103            $question_array = $wpdb->get_results($wpdb->prepare("SELECT * FROM {$wpdb->prefix}mlw_questions WHERE question_id IN (%1s)", $question_sql), 'ARRAY_A');
     104            // Loop through questions and prepare serialized data.
     105            foreach ($question_array as $question) {
     106                $multicategories = self::get_question_categories($question['question_id']);
     107                // get_question_categories
     108
     109                $question['multicategories']       = isset($multicategories['category_tree']) && ! empty($multicategories['category_tree']) ? array_keys($multicategories['category_name']) : [];
     110                $question['multicategoriesobject'] = isset($multicategories['category_tree']) && ! empty($multicategories['category_tree']) ? $multicategories['category_tree'] : [];
     111                // Prepares settings.
     112                $settings = maybe_unserialize($question['question_settings']);
     113                if (! is_array($settings)) {
     114                    $settings = [ 'required' => 1 ];
     115                }
     116                $question['settings'] = $settings;
     117                // Prepare answers.
     118                $answers = maybe_unserialize($question['answer_array']);
     119                if (! is_array($answers)) {
     120                    $answers = [];
     121                }
     122                $question['answers'] = self::sanitize_answers($answers, $settings);
     123                // Get the page.
     124                $question_id      = intval($question['question_id']);
     125                $question['page'] = intval($page_for_ids[ $question_id ]);
     126
     127                $questions[ $question_id ] = $question;
     128            }
     129        } else {
     130            // If we do not have pages on this quiz yet, use older load_questions and add page to them.
     131            $questions = self::load_questions($quiz_id, $caller);
     132            foreach ($questions as $key => $question) {
     133                $questions[ $key ]['page'] = isset($question['page']) ? $question['page'] : 0;
     134            }
     135        }
     136        return apply_filters('qsm_load_questions_by_pages', $questions, $quiz_id);
     137    }
     138
     139    /**
     140     * Loads questions for a quiz
     141     *
     142     * @since  5.2.0
     143     * @param  int $quiz_id The ID of the quiz.
     144     * @return array The array of questions.
     145     */
     146    public static function load_questions($quiz_id, $caller = '')
     147    {
     148
     149        global $wpdb;
     150        $question_array = [];
     151
     152        // Get all questions.
     153        if (0 !== $quiz_id) {
     154            $quiz_id   = intval($quiz_id);
     155            $questions = $wpdb->get_results($wpdb->prepare("SELECT * FROM {$wpdb->prefix}mlw_questions WHERE quiz_id=%d AND deleted='0' ORDER BY question_order ASC", $quiz_id), 'ARRAY_A');
     156        } else {
     157            $questions = $wpdb->get_results("SELECT * FROM {$wpdb->prefix}mlw_questions WHERE deleted='0' ORDER BY question_order ASC", 'ARRAY_A');
     158        }
     159
     160        // Loop through questions and prepare serialized data.
     161        foreach ($questions as $question) {
     162            $multicategories     = [];
     163            $multicategories_res = $wpdb->get_results("SELECT `term_id` FROM `{$wpdb->prefix}mlw_question_terms` WHERE `question_id`='{$question['question_id']}' AND `taxonomy`='qsm_category'", ARRAY_A);
     164            if (! empty($multicategories_res)) {
     165                foreach ($multicategories_res as $cat) {
     166                    $multicategories[] = $cat['term_id'];
     167                }
     168            }
     169            $question['multicategories'] = $multicategories;
     170            // Prepare answers.
     171            $answers = maybe_unserialize($question['answer_array']);
     172            if (! is_array($answers)) {
     173                $answers = [];
     174            }
     175            $question['answers'] = $answers;
     176
     177            $settings = maybe_unserialize($question['question_settings']);
     178            if (! is_array($settings)) {
     179                $settings = [ 'required' => 1 ];
     180            }
     181            $question['settings'] = $settings;
     182
     183            $question_array[ $question['question_id'] ] = $question;
     184        }
     185        $question_array = ! empty($caller) ? $question_array : array_filter(
     186            $question_array,
     187            function ($question) {
     188                return ! isset($question['settings']['isPublished']) || 0 !== intval($question['settings']['isPublished']);
     189            }
     190        );
     191        return apply_filters('qsm_load_questions', $question_array, $quiz_id);
     192    }
     193
     194    /**
     195     * Creates a new question
     196     *
     197     * @since  5.2.0
     198     * @param  array $data     The question data.
     199     * @param  array $answers  The answers for the question.
     200     * @param  array $settings Any settings for the question.
     201     * @throws Exception Throws exception if wpdb query results in error.
     202     * @return int The ID of the question that was created.
     203     */
     204    public static function create_question($data, $answers = [], $settings = [])
     205    {
     206        return self::create_save_question($data, $answers, $settings);
     207    }
     208
     209    /**
     210     * Saves a question
     211     *
     212     * @since  5.2.0
     213     * @param  int   $question_id The ID of the question to be saved.
     214     * @param  array $data        The question data.
     215     * @param  array $answers     The answers for the question.
     216     * @param  array $settings    Any settings for the question.
     217     * @throws Exception Throws exception if wpdb query results in error.
     218     * @return int The ID of the question that was saved.
     219     */
     220    public static function save_question($question_id, $data, $answers = [], $settings = [])
     221    {
     222        $data['ID'] = intval($question_id);
     223        return self::create_save_question($data, $answers, $settings, false);
     224    }
     225
     226    /**
     227     * Deletes a question
     228     *
     229     * @since  5.2.0
     230     * @param  int $question_id The ID for the question.
     231     * @throws Exception Throws exception if wpdb query results in error.
     232     * @return bool True if successful
     233     */
     234    public static function delete_question($question_id)
     235    {
     236        global $wpdb;
     237
     238        $results = $wpdb->update(
     239            $wpdb->prefix . 'mlw_questions',
     240            [
     241                'deleted' => 1,
     242            ],
     243            [ 'question_id' => intval($question_id) ],
     244            [
     245                '%d',
     246            ],
     247            [ '%d' ]
     248        );
     249
     250        if (false === $results) {
     251            $msg = $wpdb->last_error . ' from ' . $wpdb->last_query;
     252            $mlwQuizMasterNext->log_manager->add('Error when deleting question', $msg, 0, 'error');
     253            throw new Exception(esc_html($msg));
     254        }
     255
     256        return true;
     257    }
     258
     259    /**
     260     * Creates or saves a question
     261     *
     262     * This is used internally. Use create_question or save_question instead.
     263     *
     264     * @since  5.2.0
     265     * @param  array $data        The question data.
     266     * @param  array $answers     The answers for the question.
     267     * @param  array $settings    Any settings for the question.
     268     * @param  bool  $is_creating True if question is being created, false if being saved.
     269     * @throws Exception Throws exception if wpdb query results in error.
     270     * @return int The ID of the question that was created/saved.
     271     */
     272    private static function create_save_question($data, $answers, $settings, $is_creating = true)
     273    {
     274        global $wpdb, $mlwQuizMasterNext;
     275
     276        // Prepare defaults and parse.
     277        $defaults = [
     278            'quiz_id'         => 0,
     279            'type'            => '0',
     280            'name'            => '',
     281            'answer_info'     => '',
     282            'comments'        => '1',
     283            'hint'            => '',
     284            'order'           => 1,
     285            'category'        => '',
     286            'multicategories' => '',
     287            'linked_question' => '',
     288        ];
     289        $data     = wp_parse_args($data, $defaults);
     290
     291        $defaults = [
     292            'required' => 1,
     293        ];
     294        $settings = wp_parse_args($settings, $defaults);
     295
     296        $sanitize_answers = self::sanitize_answers($answers, $settings);
     297        foreach ($sanitize_answers as $key => $answer) {
     298            $answers_array = [
     299                isset($answer[0]) ? htmlspecialchars($answer[0], ENT_QUOTES) : '',
     300                isset($answer[1]) ? floatval($answer[1]) : 0,
     301                isset($answer[2]) ? intval($answer[2]) : 0,
     302            ];
     303            if (isset($answer[3])) {
     304                array_push($answers_array, htmlspecialchars($answer[3], ENT_QUOTES));
     305            }
     306            $sanitize_answers[ $key ] = $answers_array;
     307        }
     308        $answers = apply_filters('qsm_answers_before_save', $sanitize_answers, $answers, $data, $settings);
     309
     310        $question_name             = htmlspecialchars($mlwQuizMasterNext->sanitize_html($data['name']), ENT_QUOTES);
     311        $trim_question_description = apply_filters('qsm_trim_question_description', true);
     312        if ($trim_question_description) {
     313            $question_name = trim(preg_replace('/\s+/', ' ', $question_name));
     314        }
     315        $linked_question = sanitize_text_field($data['linked_question']);
     316        $linked_questions_array = [];
     317
     318        if (( $is_creating && isset($data['is_linking']) && 1 <= $data['is_linking'] ) || ! $is_creating) {
     319            // Convert the existing linked_question into an array
     320            $linked_questions_array = array_filter(array_map('trim', explode(',', $linked_question)));
     321            // Add the new value if it's not already in the array
     322            if (isset($data['is_linking']) && ! in_array($data['is_linking'], $linked_questions_array, true)) {
     323                $linked_questions_array[] = $data['is_linking'];
     324            }
     325            $linked_questions_array = array_filter($linked_questions_array);
     326            // Join back into a comma-separated string
     327            $linked_question = implode(',', $linked_questions_array);
     328        } elseif (isset($data['is_linking']) && 0 == $data['is_linking']) {
     329            $linked_question = '';
     330        }
     331
     332        $values = [
     333            'quiz_id'              => intval($data['quiz_id']),
     334            'question_name'        => $question_name,
     335            'answer_array'         => maybe_serialize($answers),
     336            'question_answer_info' => $mlwQuizMasterNext->sanitize_html($data['answer_info']),
     337            'comments'             => sanitize_text_field($data['comments']),
     338            'hints'                => sanitize_text_field($data['hint']),
     339            'question_order'       => intval($data['order']),
     340            'question_type_new'    => sanitize_text_field($data['type']),
     341            'question_settings'    => maybe_serialize($settings),
     342            'category'             => sanitize_text_field($data['category']),
     343            'linked_question'      => $linked_question,
     344            'deleted'              => 0,
     345        ];
     346        $values = apply_filters('qsm_save_question_data', $values);
     347
     348        $types = [
     349            '%d',
     350            '%s',
     351            '%s',
     352            '%s',
     353            '%d',
     354            '%s',
     355            '%d',
     356            '%s',
     357            '%s',
     358            '%s',
     359            '%s',
     360            '%d',
     361        ];
     362
     363        if ($is_creating) {
     364            $results     = $wpdb->insert(
     365                $wpdb->prefix . 'mlw_questions',
     366                $values,
     367                $types
     368            );
     369            $question_id = $wpdb->insert_id;
     370        } else {
     371            $question_id = intval($data['ID']);
     372            $results     = $wpdb->update(
     373                $wpdb->prefix . 'mlw_questions',
     374                $values,
     375                [ 'question_id' => $question_id ],
     376                $types,
     377                [ '%d' ]
     378            );
     379        }
     380
     381        if (false === $results) {
     382            $msg = $wpdb->last_error . ' from ' . $wpdb->last_query;
     383            $mlwQuizMasterNext->log_manager->add('Error when creating/saving question', $msg, 0, 'error');
     384            throw new Exception(esc_html($msg));
     385        }
     386
     387        $base_question_id = $question_id;
     388        $quiz_questions_array = [];
     389        $quiz_questions_array[ intval($data['quiz_id']) ] = $question_id;
     390        $linked_questions_array[] = $question_id;
     391        if (isset($linked_question) && "" != $linked_question) {
     392            // preparing array for quiz question id
     393            $imploded_question_ids = implode(',', array_unique($linked_questions_array));
     394            if (! empty($linked_questions_array)) {
     395                $quiz_results = $wpdb->get_results("SELECT `quiz_id`, `question_id` FROM `{$wpdb->prefix}mlw_questions` WHERE `question_id` IN (" . $imploded_question_ids . ")");
     396                foreach ($quiz_results as $key => $value) {
     397                    $quiz_questions_array[ $value->quiz_id ] = $value->question_id;
     398                }
     399            }
     400            $values['linked_question'] = $imploded_question_ids;
     401        }
     402        $question_terms_table = $wpdb->prefix . 'mlw_question_terms';
     403        foreach ($quiz_questions_array as $quiz_id => $question_id_loop) {
     404            $values['quiz_id'] = intval($quiz_id);
     405            $wpdb->update(
     406                $wpdb->prefix . 'mlw_questions',
     407                $values,
     408                [ 'question_id' => intval($question_id_loop) ],
     409                $types,
     410                [ '%d' ]
     411            );
     412
     413            /**
     414             * Process Question Categories
     415             */
     416
     417            $wpdb->delete(
     418                $question_terms_table,
     419                [
     420                    'question_id' => $question_id_loop,
     421                    'taxonomy'    => 'qsm_category',
     422                ]
     423            );
     424            if (! empty($data['multicategories'])) {
     425                foreach ($data['multicategories'] as $term_id) {
     426                    $term_rel_data = [
     427                        'question_id' => $question_id_loop,
     428                        'quiz_id'     => intval($quiz_id),
     429                        'term_id'     => $term_id,
     430                        'taxonomy'    => 'qsm_category',
     431                    ];
     432                    // Check if the data already exists in the table
     433                    $data_exists = $wpdb->get_row($wpdb->prepare("SELECT * FROM $question_terms_table WHERE question_id = %s AND quiz_id = %s AND term_id = %s AND taxonomy = %s", $question_id_loop, intval($quiz_id), $term_id, 'qsm_category'));
     434                    if (! $data_exists) {
     435                        $wpdb->insert($question_terms_table, $term_rel_data);
     436                    }
     437                }
     438            }
     439
     440            /**
     441             * Hook after saving question
     442             */
     443
     444            if ($is_creating && $base_question_id == $question_id_loop) {
     445                do_action('qsm_question_added', $question_id_loop, $values);
     446            } else {
     447                do_action('qsm_question_updated', $question_id_loop, $values);
     448            }
     449            do_action('qsm_saved_question', $question_id_loop, $values);
     450        }
     451        return $base_question_id;
     452    }
     453
     454    /**
     455     * Creates or saves a question
     456     *
     457     * sanitizes answers
     458     *
     459     * @since  7.3.5
     460     * @param  array $answers The answers for the question.
     461     * @return array sanitized $answers The answers for the question.
     462     */
     463    public static function sanitize_answers($answers, $settings)
     464    {
     465        global $mlwQuizMasterNext;
     466        foreach ($answers as $key => $answer) {
     467            if (isset($answer[0])) {
     468                if (isset($settings['answerEditor']) && 'rich' == $settings['answerEditor']) {
     469                    $answer[0] = $mlwQuizMasterNext->sanitize_html($answer[0]);
     470                } else {
     471                    $answer[0] = $mlwQuizMasterNext->sanitize_html($answer[0], false);
     472                }
     473                $answers[ $key ] = $answer;
     474            }
     475        }
     476
     477        return $answers;
     478    }
     479
     480    /**
     481     * Get categories for a quiz
     482     *
     483     * @since  7.2.1
     484     * @param  int $quiz_id The ID of the quiz.
     485     * @return array The array of categories.
     486     */
     487    public static function get_quiz_categories($quiz_id = 0)
     488    {
     489        global $wpdb;
     490        $categories = [];
     491        if (0 !== $quiz_id) {
     492            $questions      = self::load_questions_by_pages($quiz_id);
     493            $question_ids   = array_column($questions, 'question_id');
     494            $question_ids   = implode(',', $question_ids);
     495            $question_terms = $wpdb->get_results("SELECT `term_id` FROM `{$wpdb->prefix}mlw_question_terms` WHERE `question_id` IN ({$question_ids}) AND `taxonomy`='qsm_category'", ARRAY_A);
     496            $term_ids       = ! empty($question_terms) ? array_unique(array_column($question_terms, 'term_id')) : [];
     497            $cat_array      = self::get_question_categories_from_quiz_id($quiz_id);
     498            $enabled        = get_option('qsm_multiple_category_enabled');
     499            if ($enabled && 'cancelled' !== $enabled && ! empty($cat_array)) {
     500                $term_ids = array_unique(array_merge($term_ids, $cat_array));
     501            }
     502
     503            $categories = self::get_question_categories_from_term_ids($term_ids);
     504        }
     505        return $categories;
     506    }
     507
     508    /**
     509     * Get categories from quiz id
     510     *
     511     * @since  7.3.3
     512     * @param  int $quiz_id The ID of the quiz.
     513     * @return array The array of categories.
     514     */
     515    public static function get_question_categories_from_quiz_id($quiz_id)
     516    {
     517        $cat_array = [];
     518        $questions = self::load_questions_by_pages($quiz_id);
     519        foreach ($questions as $single_question) {
     520            if (isset($single_question['multicategories']) && is_array($single_question['multicategories'])) {
     521                foreach ($single_question['multicategories'] as $cat_id) {
     522                    $cat_array[] = $cat_id;
     523                }
     524            }
     525        }
     526        return $cat_array;
     527    }
     528
     529    /**
     530     * Get categories from term ids
     531     *
     532     * @since  7.3.3
     533     * @param  int $term_ids Term IDs of the quiz.
     534     * @return array The array of categories.
     535     */
     536    public static function get_question_categories_from_term_ids($term_ids)
     537    {
     538        $categories = [];
     539        if (! empty($term_ids)) {
     540            $categories_names = [];
     541            $categories_tree  = [];
     542            $terms            = get_terms(
     543                [
     544                    'taxonomy'   => 'qsm_category',
     545                    'include'    => $term_ids,
     546                    'hide_empty' => false,
     547                    'orderby'    => '',
     548                    'order'      => '',
     549                ]
     550            );
     551            if (! empty($terms)) {
     552                foreach ($terms as $tax) {
     553                    $categories_names[ $tax->term_id ] = $tax->name;
     554                    $taxs[ $tax->parent ][]            = $tax;
     555                }
     556                $categories_tree = self::create_terms_tree($taxs, isset($taxs[0]) ? $taxs[0] : reset($taxs));
     557            }
     558            $categories = [
     559                'list' => $categories_names,
     560                'tree' => $categories_tree,
     561            ];
     562        }
     563        return $categories;
     564    }
     565
     566    /**
     567     * Get categories for a Question
     568     *
     569     * @since  7.2.1
     570     * @param  int $quiz_id The ID of the quiz.
     571     * @return array The array of categories.
     572     */
     573    public static function get_question_categories($question_id = 0)
     574    {
     575        global $wpdb;
     576        $categories_tree  = [];
     577        $categories_names = [];
     578        if (0 !== $question_id) {
     579            $question_terms = $wpdb->get_results("SELECT `term_id` FROM `{$wpdb->prefix}mlw_question_terms` WHERE `question_id`='{$question_id}' AND `taxonomy`='qsm_category'", ARRAY_A);
     580            if (! empty($question_terms)) {
     581                $term_ids = array_unique(array_column($question_terms, 'term_id'));
     582                if (! empty($term_ids)) {
     583                    $terms = get_terms(
     584                        [
     585                            'taxonomy'   => 'qsm_category',
     586                            'include'    => array_unique($term_ids),
     587                            'hide_empty' => false,
     588                            'orderby'    => '',
     589                            'order'      => '',
     590                        ]
     591                    );
     592                    if (! empty($terms)) {
     593                        foreach ($terms as $tax) {
     594                            $categories_names[ $tax->term_id ] = $tax->name;
     595                            $taxs[ $tax->parent ][]            = $tax;
     596                        }
     597                        $categories_tree = self::create_terms_tree($taxs, isset($taxs[0]) ? $taxs[0] : reset($taxs));
     598                    }
     599                }
     600            }
     601        }
     602        return [
     603            'category_name' => $categories_names,
     604            'category_tree' => $categories_tree,
     605        ];
     606    }
     607    /**
     608     * Create tree structure of terms.
     609     *
     610     * @since 7.2.1
     611     */
     612    public static function create_terms_tree(&$list, $parent)
     613    {
     614        $taxTree = [];
     615        if (is_array($parent)) {
     616            foreach ($parent as $ind => $val) {
     617                if (isset($list[ $val->term_id ])) {
     618                    $val->children = self::create_terms_tree($list, $list[ $val->term_id ]);
     619                }
     620                $taxTree[] = $val;
     621            }
     622        }
     623        return $taxTree;
     624    }
    614625}
  • quiz-master-next/trunk/readme.txt

    r3357783 r3372406  
    55Tested up to: 6.8
    66Requires PHP: 5.4
    7 Stable tag: 10.2.7
     7Stable tag: 10.2.8
    88License: GPLv2
    99License URI: http://www.gnu.org/licenses/gpl-2.0.html
     
    222222
    223223== Changelog ==
     224= 10.2.8 ( October 3, 2025 ) =
     225* Feature: Added option to shuffle questions in edit quiz page
     226* Bug: Resolved conflict between per-user limits and total quiz limits
     227
    224228= 10.2.7 ( September 8, 2025 ) =
    225229* Bug: Fixed an issue where the quiz page displayed blank if the "End Quiz" section was missing
Note: See TracChangeset for help on using the changeset viewer.