Changeset 3372406
- Timestamp:
- 10/03/2025 02:06:59 PM (5 months ago)
- Location:
- quiz-master-next/trunk
- Files:
-
- 1 added
- 10 edited
-
assets/randomize.svg (added)
-
css/qsm-admin-question.css (modified) (1 diff)
-
js/qsm-admin.js (modified) (2 diffs)
-
js/qsm-quiz.js (modified) (2 diffs)
-
mlw_quizmaster2.php (modified) (3 diffs)
-
php/admin/options-page-questions-tab.php (modified) (1 diff)
-
php/classes/class-qmn-plugin-helper.php (modified) (2 diffs)
-
php/classes/class-qmn-quiz-manager.php (modified) (3 diffs)
-
php/classes/class-qsm-install.php (modified) (1 diff)
-
php/classes/class-qsm-questions.php (modified) (1 diff)
-
readme.txt (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
quiz-master-next/trunk/css/qsm-admin-question.css
r3317056 r3372406 89 89 color: #dc3232; 90 90 } 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; 93 123 } 94 124 .page-header .edit-page-button span { -
quiz-master-next/trunk/js/qsm-admin.js
r3336048 r3372406 2858 2858 }); 2859 2859 }, 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 }, 2860 2925 savePages: function () { 2861 2926 var pages = []; … … 3921 3986 }); 3922 3987 3988 $(document).on('click', '.qsm-admin-randomize-page-questions', function () { 3989 let pageId = $(this).data('page-id'); 3990 QSMQuestion.shuffleQuestions(pageId); 3991 }); 3992 3923 3993 $(document).on('click', '.qsm-admin-select-question-input', function () { 3924 3994 if (!$(this).prop('checked')) { -
quiz-master-next/trunk/js/qsm-quiz.js
r3357783 r3372406 2088 2088 jQuery(document).keydown(function(event) { 2089 2089 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 } 2091 2095 if (jQuery(event.target).is('input')) { 2092 2096 // Check if the parent div has the class 'qsm_contact_div' … … 2110 2114 } 2111 2115 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(); 2114 2118 } 2115 2119 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 3 3 * Plugin Name: Quiz And Survey Master 4 4 * Description: Easily and quickly add quizzes and surveys to your website. 5 * Version: 10.2. 75 * Version: 10.2.8 6 6 * Author: ExpressTech 7 7 * Author URI: https://quizandsurveymaster.com/ … … 44 44 * @since 4.0.0 45 45 */ 46 public $version = '10.2. 7';46 public $version = '10.2.8'; 47 47 48 48 /** … … 616 616 'warning_icon' => esc_url(QSM_PLUGIN_URL . 'assets/warning-message.png'), 617 617 'info_icon' => esc_url(QSM_PLUGIN_URL . 'assets/info-message.png'), 618 'question_shuffle' => __('Question shuffled successfully!', 'quiz-master-next'), 618 619 ); 619 620 $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 1417 1417 <div class="page-header"> 1418 1418 <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> 1421 1424 <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> 1422 1425 <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 1 1 <?php 2 if ( ! defined( 'ABSPATH' )) {3 exit;2 if (! defined('ABSPATH')) { 3 exit; 4 4 } 5 5 include_once ABSPATH . 'wp-admin/includes/plugin.php'; … … 12 12 * @since 4.0.0 13 13 */ 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 ); ?>. </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 ? "✕ " : ""; 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;'>• $2</span>", $html ); 1226 $html = preg_replace( '/<span class="qsm-text-wrong-option(.*?)">(.*?)<\/span>/', "<span style='color:red;display:block;margin-bottom:5px;'>✕ $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;'>✓ $2</span>", $html ); 1229 $html = preg_replace( '/<span class="qmn_user_correct_answer(.*?)">(.*?)<\/span>/', "<span style='color:green;display:block;margin-bottom:5px;'>✓ $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 } 14 class 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); ?>. </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 ? "✕ " : ""; 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;'>• $2</span>", $html); 1272 $html = preg_replace('/<span class="qsm-text-wrong-option(.*?)">(.*?)<\/span>/', "<span style='color:red;display:block;margin-bottom:5px;'>✕ $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;'>✓ $2</span>", $html); 1275 $html = preg_replace('/<span class="qmn_user_correct_answer(.*?)">(.*?)<\/span>/', "<span style='color:green;display:block;margin-bottom:5px;'>✓ $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 } 1319 1369 } -
quiz-master-next/trunk/php/classes/class-qmn-quiz-manager.php
r3357783 r3372406 353 353 354 354 $qmn_quiz_options = $has_proper_quiz['qmn_quiz_options']; 355 $qmn_quiz_options = apply_filters('qsm_quiz_option_before', $qmn_quiz_options); 355 356 $return_display = ''; 356 357 … … 1294 1295 } 1295 1296 $is_contact_fields_enabled = array_filter( 1296 $contact_fields,1297 is_array( $contact_fields ) ? $contact_fields : [], 1297 1298 function( $sub ) { 1298 1299 return isset( $sub['enable'] ) && 'true' === $sub['enable']; … … 1673 1674 } 1674 1675 } 1676 1677 $errors = apply_filters( 'qsm_validate_contact_field', $errors, $contact_form, $request ); 1675 1678 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>"; 1676 1679 } -
quiz-master-next/trunk/php/classes/class-qsm-install.php
r3341668 r3372406 180 180 $field_array = array( 181 181 'id' => 'randomness_order', 182 'label' => __( 'Randomiz e 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>', 183 183 'type' => 'radio', 184 184 'options' => array( 185 185 array( 186 'label' => __( ' Disabled', 'quiz-master-next' ),186 'label' => __( 'None', 'quiz-master-next' ), 187 187 'value' => 0, 188 188 ), 189 189 array( 190 'label' => __( ' Randomize question only', 'quiz-master-next' ),190 'label' => __( 'Questions', 'quiz-master-next' ), 191 191 'value' => 1, 192 192 ), 193 193 array( 194 'label' => __( ' Randomize answers only', 'quiz-master-next' ),194 'label' => __( 'Answers', 'quiz-master-next' ), 195 195 'value' => 3, 196 196 ), 197 197 array( 198 'label' => __( ' Randomize questions and their answers', 'quiz-master-next' ),198 'label' => __( 'Questions & Answers', 'quiz-master-next' ), 199 199 'value' => 2, 200 200 ), -
quiz-master-next/trunk/php/classes/class-qsm-questions.php
r3341668 r3372406 11 11 * @since 5.2.0 12 12 */ 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 13 class 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 } 614 625 } -
quiz-master-next/trunk/readme.txt
r3357783 r3372406 5 5 Tested up to: 6.8 6 6 Requires PHP: 5.4 7 Stable tag: 10.2. 77 Stable tag: 10.2.8 8 8 License: GPLv2 9 9 License URI: http://www.gnu.org/licenses/gpl-2.0.html … … 222 222 223 223 == 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 224 228 = 10.2.7 ( September 8, 2025 ) = 225 229 * 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.