Issues (896)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/class-sensei-question.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 14 and the first side effect is on line 2.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
3
4
/**
5
 * Sensei Question Class
6
 *
7
 * All functionality pertaining to the questions post type in Sensei.
8
 *
9
 * @package Assessment
10
 * @author Automattic
11
 *
12
 * @since 1.0.0
13
 */
14
class Sensei_Question {
15
	public $token;
16
	public $meta_fields;
17
18
	/**
19
	 * Constructor.
20
	 * @since  1.0.0
21
	 */
22
	public function __construct () {
23
        $this->token = 'question';
24
		$this->question_types = $this->question_types();
25
		$this->meta_fields = array( 'question_right_answer', 'question_wrong_answers' );
26
		if ( is_admin() ) {
27
			// Custom Write Panel Columns
28
			add_filter( 'manage_edit-question_columns', array( $this, 'add_column_headings' ), 10, 1 );
29
			add_action( 'manage_posts_custom_column', array( $this, 'add_column_data' ), 10, 2 );
30
			add_action( 'add_meta_boxes', array( $this, 'question_edit_panel_metabox' ), 10, 2 );
31
32
			// Quesitno list table filters
33
			add_action( 'restrict_manage_posts', array( $this, 'filter_options' ) );
34
			add_filter( 'request', array( $this, 'filter_actions' ) );
35
36
			add_action( 'save_post', array( $this, 'save_question' ), 10, 1 );
37
		} // End If Statement
38
	} // End __construct()
39
40
	public function question_types() {
41
		$types = array(
42
			'multiple-choice' => __( 'Multiple Choice', 'woothemes-sensei' ),
43
			'boolean' => __( 'True/False', 'woothemes-sensei' ),
44
			'gap-fill' => __( 'Gap Fill', 'woothemes-sensei' ),
45
			'single-line' => __( 'Single Line', 'woothemes-sensei' ),
46
			'multi-line' => __( 'Multi Line', 'woothemes-sensei' ),
47
			'file-upload' => __( 'File Upload', 'woothemes-sensei' ),
48
		);
49
50
		return apply_filters( 'sensei_question_types', $types );
51
	}
52
53
	/**
54
	 * Add column headings to the "lesson" post list screen.
55
	 * @access public
56
	 * @since  1.3.0
57
	 * @param  array $defaults
58
	 * @return array $new_columns
59
	 */
60 View Code Duplication
	public function add_column_headings ( $defaults ) {
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
61
		$new_columns['cb'] = '<input type="checkbox" />';
62
		$new_columns['title'] = _x( 'Question', 'column name', 'woothemes-sensei' );
63
		$new_columns['question-type'] = _x( 'Type', 'column name', 'woothemes-sensei' );
64
		$new_columns['question-category'] = _x( 'Categories', 'column name', 'woothemes-sensei' );
65
		if ( isset( $defaults['date'] ) ) {
66
			$new_columns['date'] = $defaults['date'];
67
		}
68
69
		return $new_columns;
70
	} // End add_column_headings()
71
72
	/**
73
	 * Add data for our newly-added custom columns.
74
	 * @access public
75
	 * @since  1.3.0
76
	 * @param  string $column_name
77
	 * @param  int $id
78
	 * @return void
79
	 */
80
	public function add_column_data ( $column_name, $id ) {
81
		global $wpdb, $post;
82
83
		switch ( $column_name ) {
84
85
			case 'id':
86
				echo $id;
87
			break;
88
89
			case 'question-type':
90
				$question_type = strip_tags( get_the_term_list( $id, 'question-type', '', ', ', '' ) );
91
				$output = '&mdash;';
92
				if( isset( $this->question_types[ $question_type ] ) ) {
93
					$output = $this->question_types[ $question_type ];
94
				}
95
				echo $output;
96
			break;
97
98
			case 'question-category':
99
				$output = strip_tags( get_the_term_list( $id, 'question-category', '', ', ', '' ) );
100
				if( ! $output ) {
101
					$output = '&mdash;';
102
				}
103
				echo $output;
104
			break;
105
106
			default:
107
			break;
108
109
		}
110
111
	} // End add_column_data()
112
113
	public function question_edit_panel_metabox( $post_type, $post ) {
114
		if( in_array( $post_type, array( 'question', 'multiple_question' ) ) ) {
115
116
			$metabox_title = __( 'Question', 'woothemes-sensei' );
117
118
			if( isset( $post->ID ) ) {
119
120
                $question_type = Sensei()->question->get_question_type( $post->ID );
121
122
				if( $question_type ) {
123
					$type = $this->question_types[ $question_type ];
124
					if( $type ) {
125
						$metabox_title = $type;
126
					}
127
				}
128
			}
129
			add_meta_box( 'question-edit-panel', $metabox_title, array( $this, 'question_edit_panel' ), 'question', 'normal', 'high' );
130
			add_meta_box( 'question-lessons-panel', __( 'Quizzes', 'woothemes-sensei' ), array( $this, 'question_lessons_panel' ), 'question', 'side', 'default' );
131
			add_meta_box( 'multiple-question-lessons-panel', __( 'Quizzes', 'woothemes-sensei' ), array( $this, 'question_lessons_panel' ), 'multiple_question', 'side', 'default' );
132
		}
133
	}
134
135
	public function question_edit_panel() {
136
		global  $post, $pagenow;
137
138
		add_action( 'admin_enqueue_scripts', array( Sensei()->lesson, 'enqueue_scripts' ) );
139
		add_action( 'admin_enqueue_scripts', array( Sensei()->lesson, 'enqueue_styles' ) );
140
141
		$html = '<div id="lesson-quiz" class="single-question"><div id="add-question-main">';
142
143
		if( 'post-new.php' == $pagenow ) {
144
145
			$html .= '<div id="add-question-actions">';
146
				$html .= Sensei()->lesson->quiz_panel_add( 'question' );
147
			$html .= '</div>';
148
149
		} else {
150
			$question_id = $post->ID;
151
152
			$question_type =  Sensei()->question->get_question_type( $post->ID );
153
154
			$html .= '<div id="add-question-metadata"><table class="widefat">';
155
				$html .= Sensei()->lesson->quiz_panel_question( $question_type, 0, $question_id, 'question' );
156
			$html .= '</table></div>';
157
		}
158
159
		$html .= '</div></div>';
160
161
		echo $html;
162
	}
163
164
	public function question_lessons_panel() {
165
		global $post;
166
167
		$no_lessons = sprintf( __( '%1$sThis question does not appear in any quizzes yet.%2$s', 'woothemes-sensei' ), '<em>', '</em>' );
168
169
		if( ! isset( $post->ID ) ) {
170
			echo $no_lessons;
171
			return;
172
		}
173
174
		// This retrieves those quizzes the question is directly connected to.
175
		$quizzes = get_post_meta( $post->ID, '_quiz_id', false );
176
177
		// Collate all 'multiple_question' quizzes the question is part of.
178
		$categories_of_question = wp_get_post_terms( $post->ID, 'question-category', array( 'fields' => 'ids' ) );
179
		if ( ! empty( $categories_of_question ) ) {
180
			foreach ( $categories_of_question as $term_id ) {
181
				$qargs = array(
182
					'fields'           => 'ids',
183
					'post_type'        => 'multiple_question',
184
					'posts_per_page'   => -1,
185
					'meta_query'       => array(
186
						array(
187
							'key'      => 'category',
188
							'value'    => $term_id,
189
						),
190
					),
191
					'post_status'      => 'any',
192
					'suppress_filters' => 0,
193
				);
194
				$cat_question_ids = get_posts( $qargs );
195
				foreach( $cat_question_ids as $cat_question_id ) {
196
					$cat_quizzes = get_post_meta( $cat_question_id, '_quiz_id', false );
197
					$quizzes = array_merge( $quizzes, $cat_quizzes );
198
				}
199
			}
200
			$quizzes = array_unique( array_filter( $quizzes ) );
201
		}
202
203
		if( 0 == count( $quizzes ) ) {
204
			echo $no_lessons;
205
			return;
206
		}
207
208
		$lessons = false;
209
210
		foreach( $quizzes as $quiz ) {
211
212
			$lesson_id = get_post_meta( $quiz, '_quiz_lesson', true );
213
214
			if( ! $lesson_id ) continue;
215
216
			$lessons[ $lesson_id ]['title'] = get_the_title( $lesson_id );
217
			$lessons[ $lesson_id ]['link'] = admin_url( 'post.php?post=' . $lesson_id . '&action=edit' );
218
		}
219
220
		if( ! $lessons ) {
221
			echo $no_lessons;
222
			return;
223
		}
224
225
		$html = '<ul>';
226
227
		foreach( $lessons as $id => $lesson ) {
228
			$html .= '<li><a href="' . esc_url( $lesson['link'] ) . '">' . esc_html( $lesson['title'] ) . '</a></li>';
229
		}
230
231
		$html .= '</ul>';
232
233
		echo $html;
234
235
	}
236
237
	public function save_question( $post_id = 0 ) {
238
239
		if( ! isset( $_POST['post_type']
240
            ) || 'question' != $_POST['post_type'] ) {
241
            return;
242
        }
243
244
245
246
        //setup the data for saving
247
		$data = $_POST ;
248
        $data['quiz_id'] = 0;
249
		$data['question_id'] = $post_id;
250
251
		if ( ! wp_is_post_revision( $post_id ) ){
252
253
			// Unhook function to prevent infinite loops
254
			remove_action( 'save_post', array( $this, 'save_question' ) );
255
256
			// Update question data
257
			$question_id = Sensei()->lesson->lesson_save_question( $data, 'question' );
258
259
			// Re-hook same function
260
			add_action( 'save_post', array( $this, 'save_question' ) );
261
		}
262
263
		return;
264
	}
265
266
	/**
267
	 * Add options to filter the questions list table
268
	 * @return void
269
	 */
270
	public function filter_options() {
271
		global $typenow;
272
273
		if( is_admin() && 'question' == $typenow ) {
274
275
			$output = '';
276
277
			// Question type
278
			$selected = isset( $_GET['question_type'] ) ? $_GET['question_type'] : '';
279
			$type_options = '<option value="">' . __( 'All types', 'woothemes-sensei' ) . '</option>';
280
			foreach( $this->question_types as $label => $type ) {
281
				$type_options .= '<option value="' . esc_attr( $label ) . '" ' . selected( $selected, $label, false ) . '>' . esc_html( $type ) . '</option>';
282
			}
283
284
			$output .= '<select name="question_type" id="dropdown_question_type">';
285
			$output .= $type_options;
286
			$output .= '</select>';
287
288
			// Question category
289
			$cats = get_terms( 'question-category', array( 'hide_empty' => false ) );
290
			if ( ! empty( $cats ) && ! is_wp_error( $cats ) ) {
291
				$selected = isset( $_GET['question_cat'] ) ? $_GET['question_cat'] : '';
292
				$cat_options = '<option value="">' . __( 'All categories', 'woothemes-sensei' ) . '</option>';
293
				foreach( $cats as $cat ) {
294
					$cat_options .= '<option value="' . esc_attr( $cat->slug ) . '" ' . selected( $selected, $cat->slug, false ) . '>' . esc_html( $cat->name ) . '</option>';
295
				}
296
297
				$output .= '<select name="question_cat" id="dropdown_question_cat">';
298
				$output .= $cat_options;
299
				$output .= '</select>';
300
			}
301
302
			echo $output;
303
		}
304
	}
305
306
	/**
307
	 * Filter questions list table
308
	 * @param  array $request Current request
309
	 * @return array          Modified request
310
	 */
311
	public function filter_actions( $request ) {
312
		global $typenow;
313
314
		if( is_admin() && 'question' == $typenow ) {
315
316
			// Question type
317
			$question_type = isset( $_GET['question_type'] ) ? $_GET['question_type'] : '';
318 View Code Duplication
			if( $question_type ) {
319
				$type_query = array(
320
					'taxonomy' => 'question-type',
321
					'terms' => $question_type,
322
					'field' => 'slug',
323
				);
324
				$request['tax_query'][] = $type_query;
325
			}
326
327
			// Question category
328
			$question_cat = isset( $_GET['question_cat'] ) ? $_GET['question_cat'] : '';
329 View Code Duplication
			if( $question_cat ) {
330
				$cat_query = array(
331
					'taxonomy' => 'question-category',
332
					'terms' => $question_cat,
333
					'field' => 'slug',
334
				);
335
				$request['tax_query'][] = $cat_query;
336
			}
337
		}
338
339
		return $request;
340
	}
341
342
    /**
343
     * Get the type of question by id
344
     *
345
     * This function uses the post terms to determine which question type
346
     * the passed question id belongs to.
347
     *
348
     * @since 1.7.4
349
     *
350
     * @param int $question_id
351
     *
352
     * @return string $question_type | bool
353
     */
354
    public function get_question_type( $question_id ){
355
356 View Code Duplication
        if( empty( $question_id ) || ! intval( $question_id ) > 0
357
            || 'question' != get_post_type( $question_id )   ){
358
            return false;
359
        }
360
361
        $question_type = 'multiple-choice';
362
        $question_types = wp_get_post_terms( $question_id, 'question-type' );
363
        foreach( $question_types as $type ) {
364
            $question_type = $type->slug;
365
        }
366
367
        return $question_type;
368
369
    }// end get_question_type
370
371
	/**
372
	 * Given a question ID, return the grade that can be achieved.
373
	 * 
374
	 * @since 1.9
375
	 *
376
	 * @param int $question_id
377
	 *
378
	 * @return int $question_grade | bool
379
	 */
380
	public function get_question_grade( $question_id ) {
381
382 View Code Duplication
		if ( empty( $question_id ) || ! intval( $question_id ) > 0
383
			|| 'question' != get_post_type( $question_id ) ) {
384
			return false;
385
		}
386
387
		$question_grade_raw = get_post_meta( $question_id, '_question_grade', true );
388
		// If not set then default to 1...
389
		if ( false === $question_grade_raw || $question_grade_raw == '' ) {
390
			$question_grade = 1;
391
		}
392
		// ...but allow a grade of 0 for non-marked questions
393
		else {
394
			$question_grade = intval( $question_grade_raw );
395
		}
396
		return $question_grade;
397
398
	} // end get_question_grade
399
400
401
    /**
402
     * This function simply loads the question type template
403
     *
404
     * @since 1.9.0
405
     * @param $question_type
406
     */
407
    public static function load_question_template( $question_type ){
408
409
        Sensei_Templates::get_template  ( 'single-quiz/question_type-' . $question_type . '.php' );
410
    }
411
412
    /**
413
     * Echo the sensei question title.
414
     *
415
     * @uses WooThemes_Sensei_Question::get_the_question_title
416
     *
417
     * @since 1.9.0
418
     * @param $question_id
419
     */
420
    public static function the_question_title( $question_id ){
421
422
        echo self::get_the_question_title( $question_id );
423
424
    }// end the_question_title
425
426
    /**
427
     * Generate the question title with it's grade.
428
     *
429
     * @since 1.9.0
430
     *
431
     * @param $question_id
432
     * @return string
433
     */
434
    public static function get_the_question_title( $question_id ){
435
436
        /**
437
         * Filter the sensei question title
438
         *
439
         * @since 1.3.0
440
         * @param $question_title
441
         */
442
        $title = apply_filters( 'sensei_question_title', get_the_title( $question_id ) );
443
444
        /**
445
         * hook document in class-woothemes-sensei-message.php the_title()
446
         */
447
        $title = apply_filters( 'sensei_single_title', $title, 'question');
448
449
        $title_html  = '<span class="question question-title">';
450
        $title_html .= $title;
451
        $title_html .= '<span class="grade"><?php sensi_the_question_grade()?></span>';
452
        $title_html .='</span>';
453
454
        return $title_html;
455
    }
456
457
    /**
458
     * Tech the question description
459
     *
460
     * @param $question_id
461
     * @return string
462
     */
463
    public static function get_the_question_description( $question_id ){
464
465
        $question = get_post( $question_id );
466
467
        /**
468
         * Already documented within WordPress Core
469
         */
470
        return apply_filters( 'the_content', $question->post_content );
471
472
    }
473
474
    /**
475
     * Output the question description
476
     *
477
     * @since 1.9.0
478
     * @param $question_id
479
     */
480
    public static function the_question_description( $question_id  ){
481
482
        echo self::get_the_question_description( $question_id );
483
484
    }
485
486
    /**
487
     * Get the questions media markup
488
     *
489
     * @since 1.9.0
490
     * @param $question_id
491
     * @return string
492
     */
493
    public static function get_the_question_media( $question_id ){
494
495
        $question_media = get_post_meta( $question_id, '_question_media', true );
496
        $question_media_link = '';
497
        if( 0 < intval( $question_media ) ) {
498
            $mimetype = get_post_mime_type( $question_media );
499
            if( $mimetype ) {
500
                $mimetype_array = explode( '/', $mimetype);
501
                if( isset( $mimetype_array[0] ) && $mimetype_array[0] ) {
502
                    $question_media_type = $mimetype_array[0];
503
                    $question_media_url = wp_get_attachment_url( $question_media );
504
                    $attachment = get_post( $question_media );
505
                    $question_media_title = $attachment->post_title;
506
                    $question_media_description = $attachment->post_content;
507
                    switch( $question_media_type ) {
508
                        case 'image':
509
                            $image_size = apply_filters( 'sensei_question_image_size', 'medium', $question_id );
510
                            $attachment_src = wp_get_attachment_image_src( $question_media, $image_size );
511
                            $question_media_link = '<a class="' . esc_attr( $question_media_type ) . '" title="' . esc_attr( $question_media_title ) . '" href="' . esc_url( $question_media_url ) . '" target="_blank"><img src="' . $attachment_src[0] . '" width="' . $attachment_src[1] . '" height="' . $attachment_src[2] . '" /></a>';
512
                            break;
513
514
                        case 'audio':
515
                            $question_media_link = wp_audio_shortcode( array( 'src' => $question_media_url ) );
516
                            break;
517
518
                        case 'video':
519
                            $question_media_link = wp_video_shortcode( array( 'src' => $question_media_url ) );
520
                            break;
521
522
                        default:
523
                            $question_media_filename = basename( $question_media_url );
524
                            $question_media_link = '<a class="' . esc_attr( $question_media_type ) . '" title="' . esc_attr( $question_media_title ) . '" href="' . esc_url( $question_media_url ) . '" target="_blank">' . $question_media_filename . '</a>';
525
                            break;
526
                    }
527
                }
528
            }
529
        }
530
531
        $output = '';
532
        if( $question_media_link ) {
533
534
                $output .= '<div class="question_media_display">';
535
                $output .=      $question_media_link;
536
                $output .= '<dl>';
537
538
                if( $question_media_title ) {
539
540
                   $output .= '<dt>'. $question_media_title. '</dt>';
541
542
                 }
543
544
                if( $question_media_description ) {
545
546
                    $output .= '<dd>' . $question_media_description . '</dd>';
547
548
                }
549
550
                $output .= '</dl>';
551
                $output .= '</div>';
552
553
554
         }
555
556
        return $output;
557
558
    } // end get_the_question_media
559
560
561
    /**
562
     * Output the question media
563
     *
564
     * @since 1.9.0
565
     * @param string $question_id
566
     */
567
    public static function the_question_media( $question_id ){
568
569
        echo self::get_the_question_media( $question_id );
570
571
    }
572
573
    /**
574
     * Output a special field for the question needed for question submission.
575
     *
576
     * @since 1.9.0
577
     *
578
     * @param $question_id
579
     */
580
    public static function the_question_hidden_fields( $question_id ){
581
        ?>
582
583
            <input type="hidden" name="question_id_<?php $question_id;?>" value="<?php $question_id;?>" />
584
            <input type="hidden" name="questions_asked[]" value="<?php esc_attr_e( $question_id ); ?>" />
585
586
        <?php
587
    }
588
589
    /**
590
     * This function can only be run withing the single quiz question loop
591
     *
592
     * @since 1.9.0
593
     * @param $question_id
594
     */
595
    public static function answer_feedback_notes( $question_id ){
596
597
        //IDS
598
        $quiz_id = get_the_ID();
599
        $lesson_id = Sensei()->quiz->get_lesson_id( $quiz_id );
600
601
        // Data to check before showing feedback
602
        $user_lesson_status = Sensei_Utils::user_lesson_status( $lesson_id, get_current_user_id() );
603
        $user_quiz_grade = Sensei_Quiz::get_user_quiz_grade( $lesson_id, get_current_user_id() );
604
        $not_empty_user_quiz_grade = !empty( $user_quiz_grade );
605
        $reset_quiz_allowed = Sensei_Quiz::is_reset_allowed( $lesson_id );
606
        $lesson_completed = Sensei_Utils::user_completed_lesson( $lesson_id );
607
        $quiz_grade_type = get_post_meta( $quiz_id , '_quiz_grade_type', true );
608
609
        if( ( $lesson_completed  && $not_empty_user_quiz_grade  )
610
            ||  ( $lesson_completed && ! $reset_quiz_allowed && 'auto' == $quiz_grade_type )
611
            || ( 'auto' == $quiz_grade_type && ! $reset_quiz_allowed && $not_empty_user_quiz_grade ) ) {
612
613
            $answer_notes = Sensei()->quiz->get_user_question_feedback( $lesson_id, $question_id, get_current_user_id() );
614
615
            if( $answer_notes ) { ?>
616
617
                <div class="sensei-message info info-special answer-feedback">
618
619
                    <?php
620
621
                        /**
622
                         * Filter the answer feedback
623
                         * Since 1.9.0
624
                         *
625
                         * @param string $answer_notes
626
                         * @param string $question_id
627
                         * @param string $lesson_id
628
                         */
629
                        echo apply_filters( 'sensei_question_answer_notes', $answer_notes, $question_id, $lesson_id );
630
631
                    ?>
632
633
                </div>
634
635
            <?php }
636
637
        }// end if we can show answer feedback
638
639
    }// end answer_feedback_notes
640
641
    /**
642
     * This function has to be run inside the quiz question loop on the single quiz page.
643
     *
644
     *
645
     * @since 1.9.0
646
     * @param string $question_id
647
     */
648
    public static function the_answer_result_indication( $question_id ){
649
650
        global $post,  $current_user, $sensei_question_loop;
651
652
        // Post Data
653
        $quiz_id = $sensei_question_loop['quiz_id'];
654
        $lesson_id = Sensei()->quiz->get_lesson_id( $quiz_id );
655
        $question_item = $sensei_question_loop['current_question'];
656
657
        // Setup variable needed to determine if the message should show and what it should show
658
        $user_quiz_grade = Sensei_Quiz::get_user_quiz_grade( $lesson_id, get_current_user_id() );
659
        $lesson_complete = Sensei_Utils::user_completed_lesson( $lesson_id, get_current_user_id() );
660
        $reset_quiz_allowed = Sensei_Quiz::is_reset_allowed( $lesson_id );
661
        $quiz_grade_type = get_post_meta( $quiz_id, '_quiz_grade_type', true );
662
663
        // retrieve the question total grade
664
        $question_grade = Sensei()->question->get_question_grade( $question_id );
665
666
        // retrieve grade the user achieved
667
        $user_question_grade = Sensei()->quiz->get_user_question_grade( $lesson_id, $question_id, get_current_user_id() );
668
669
        // Question ID
670
        $question_id = $question_item->ID;
671
672
        // conditions to check
673
        $completed_with_valid_grade = $lesson_complete && $user_quiz_grade != '' ;
674
        $completed_with_valid_grade_and_reset_not_allowed = $lesson_complete &&  $user_quiz_grade != '' && ! $reset_quiz_allowed ;
675
        $grade_type_auto_a_valid_grade_and_reset_not_allowed =  'auto' == $quiz_grade_type && ! $reset_quiz_allowed && $user_quiz_grade != '' ;
676
677
        if (  $completed_with_valid_grade
678
            || $completed_with_valid_grade_and_reset_not_allowed
679
            || $grade_type_auto_a_valid_grade_and_reset_not_allowed  ) {
680
681
            $user_correct = false;
682
            $answer_message = __( 'Incorrect', 'woothemes-sensei' );
683
            $answer_message_class = 'user_wrong';
684
            // For zero grade mark as 'correct' but add no classes
685
            if ( 0 == $question_grade ) {
686
687
                $user_correct = true;
688
                $answer_message = '';
689
                $answer_message_class = '';
690
691
            } else if( $user_question_grade > 0 ) {
692
693
                $user_correct = true;
694
                $answer_message = sprintf( __( 'Grade: %d', 'woothemes-sensei' ), $user_question_grade );
695
                $answer_message_class = 'user_right';
696
697
            }
698
699
            // attach the correct answer if the question is auto gradable and user got it wrong
700
            if( !$reset_quiz_allowed && !$user_correct ){
701
702
                $answer_message .=  ' - '. __('Right Answer:','woothemes-sensei') . ' ' . self::get_correct_answer( $question_item->ID );
703
704
            }
705
706
            // answer feedback
707
            $answer_notes = Sensei()->quiz->get_user_question_feedback( $lesson_id, $question_id, $current_user->ID );
708
            if( $answer_notes ) {
709
                $answer_message_class .= ' has_notes';
710
            }
711
            ?>
712
713
            <div class="answer_message <?php esc_attr_e( $answer_message_class ); ?>">
714
715
                <span><?php echo $answer_message; ?></span>
716
717
            </div>
718
719
            <?php
720
721
        } // end if user can see all the goodies
722
723
    }// end the_answer_result_indication
724
725
    /**
726
     * Generate the question template data and return it as an array.
727
     *
728
     * @since 1.9.0
729
     *
730
     * @param string $question_id
731
     * @param $quiz_id
732
     * @return array $question_data
733
     */
734
    public static function get_template_data( $question_id, $quiz_id ){
735
736
        $lesson_id = Sensei()->quiz->get_lesson_id( $quiz_id  );
737
738
        $reset_allowed = get_post_meta( $quiz_id, '_enable_quiz_reset', true );
739
        //backwards compatibility
740
        if( 'on' == $reset_allowed ) {
741
            $reset_allowed = 1;
742
        }
743
744
        // Check again that the lesson is complete
745
        $user_lesson_end = Sensei_Utils::user_completed_lesson( Sensei()->quiz->get_lesson_id( $quiz_id), get_current_user_id() );
746
        $user_lesson_complete = false;
747
        if ( $user_lesson_end ) {
748
            $user_lesson_complete = true;
749
        }
750
751
        //setup the question data
752
        $data[ 'ID' ]                     = $question_id;
753
        $data[ 'title' ]                  = get_the_title( $question_id );
754
        $data[ 'content' ]                = get_post( $question_id )->post_content;
755
        $data[ 'quiz_id' ]                = $quiz_id;
756
        $data[ 'lesson_id' ]              = Sensei()->quiz->get_lesson_id( $quiz_id );
757
        $data[ 'type' ]                   = Sensei()->question->get_question_type( $question_id );
758
        $data[ 'question_grade' ]         = Sensei()->question->get_question_grade(  $question_id  );
759
        $data[ 'user_question_grade' ]    = Sensei()->quiz->get_user_question_grade( $lesson_id,  $question_id , get_current_user_id());
760
        $data[ 'question_right_answer' ]  = get_post_meta( $question_id , '_question_right_answer', true );
761
        $data[ 'question_wrong_answers' ] = get_post_meta( $question_id , '_question_wrong_answers', true );
762
        $data[ 'user_answer_entry' ]      = Sensei()->quiz->get_user_question_answer( $lesson_id,  $question_id , get_current_user_id() );
763
        $data[ 'lesson_completed' ]       = Sensei_Utils::user_completed_course( $lesson_id, get_current_user_id( ) );
764
        $data[ 'quiz_grade_type' ]        = get_post_meta( $quiz_id , '_quiz_grade_type', true );
765
        $data[ 'reset_quiz_allowed' ]     = $reset_allowed;
766
        $data[ 'lesson_complete' ]        = $user_lesson_complete;
767
768
        /**
769
         * Filter the question template data. This filter fires  in
770
         * the get_template_data function
771
         *
772
         * @hooked self::boolean_load_question_data
773
         *
774
         * @since 1.9.0
775
         *
776
         * @param array $data
777
         * @param string $question_id
778
         * @param string $quiz_id
779
         */
780
        return apply_filters( 'sensei_get_question_template_data', $data, $question_id, $quiz_id );
781
782
    }
783
784
    /**
785
     * Load multiple choice question data on the sensei_get_question_template_data
786
     * filter.
787
     *
788
     * @since 1.9.0
789
     *
790
     * @param $question_data
791
     * @param $question_id
792
     * @param $quiz_id
793
     *
794
     * @return array()
795
     */
796
    public static function file_upload_load_question_data ( $question_data, $question_id, $quiz_id ){
797
798
799
        if( 'file-upload' == Sensei()->question->get_question_type( $question_id ) ) {
800
801
            // Get uploaded file
802
            $attachment_id = $question_data[ 'user_answer_entry' ];
803
            $answer_media_url = $answer_media_filename = '';
804
805
806
            $question_helptext = '';
807
            if( isset( $question_data['question_wrong_answers'][0] ) ) {
808
809
                $question_helptext =  $question_data['question_wrong_answers'][0];
810
811
            }
812
813
814
            if( 0 < intval( $attachment_id ) ) {
815
816
                $answer_media_url = wp_get_attachment_url( $attachment_id );
817
                $answer_media_filename = basename( $answer_media_url );
818
819
            }
820
821
822
            // Get max upload file size, formatted for display
823
            // Code copied from wp-admin/includes/media.php:1515
824
            $upload_size_unit = $max_upload_size = wp_max_upload_size();
825
            $sizes = array( 'KB', 'MB', 'GB' );
826
            for ( $u = -1; $upload_size_unit > 1024 && $u < count( $sizes ) - 1; $u++ ) {
827
                $upload_size_unit /= 1024;
828
            }
829
            if ( $u < 0 ) {
830
831
                $upload_size_unit = 0;
832
                $u = 0;
833
834
            } else {
835
836
                $upload_size_unit = (int) $upload_size_unit;
837
838
            }
839
            $max_upload_size = sprintf( __( 'Maximum upload file size: %d%s' ), esc_html( $upload_size_unit ), esc_html( $sizes[ $u ] ) );
840
841
            // Assemble all the data needed by the file upload template
842
            $question_data[ 'answer_media_url' ]      = $answer_media_url;
843
            $question_data[ 'answer_media_filename' ] = $answer_media_filename;
844
            $question_data[ 'max_upload_size' ]       = $max_upload_size;
845
846
            $question_data[ 'question_helptext' ]     = $question_helptext;
847
848
        }// end if is file upload type
849
850
        return $question_data;
851
852
    }// end file_upload_load_question_data
853
854
    /**
855
     * Load multiple choice question data on the sensei_get_question_template_data
856
     * filter.
857
     *
858
     * @since 1.9.0
859
     *
860
     * @param $question_data
861
     * @param $question_id
862
     * @param $quiz_id
863
     *
864
     * @return array()
865
     */
866
    public static function multiple_choice_load_question_data( $question_data, $question_id, $quiz_id ){
867
868
        if( 'multiple-choice' == Sensei()->question->get_question_type( $question_id ) ) {
869
870
871
            $answer_type = 'radio';
872
            if ( is_array( $question_data[ 'question_right_answer' ] ) && ( 1 < count( $question_data[ 'question_right_answer' ] ) ) ) {
873
874
                $answer_type = 'checkbox';
875
876
            }
877
878
            // Merge right and wrong answers
879
            if ( is_array( $question_data[ 'question_right_answer' ] ) ) {
880
881
                $merged_options = array_merge( $question_data[ 'question_wrong_answers' ], $question_data[ 'question_right_answer' ] );
882
883
            }  else {
884
885
                array_push( $question_data[ 'question_wrong_answers' ], $question_data[ 'question_right_answer' ] );
886
                $merged_options = $question_data[ 'question_wrong_answers' ];
887
888
            }
889
890
            // Setup answer options array.
891
            $question_answers_options = array();
892
            $count = 0;
893
894
            foreach( $merged_options as $answer ) {
895
896
                $count++;
897
                $question_option = array();
898
899
                if( ( $question_data[ 'lesson_completed' ] && $question_data[ 'user_quiz_grade' ] != '' )
900
                    || ( $question_data[ 'lesson_completed' ] && ! $question_data[ 'reset_quiz_allowed' ] && $question_data[ 'user_quiz_grade' ] != '' )
901
                    || ( 'auto' == $question_data[ 'quiz_grade_type' ] && ! $question_data[ 'reset_quiz_allowed' ]  && ! empty( $question_data[ 'user_quiz_grade' ] ) ) ) {
902
903
                    $user_correct = false;
904
905
906
                    // For zero grade mark as 'correct' but add no classes
907
                    if ( 0 == $question_data[ 'question_grade' ] ) {
908
909
                        $user_correct = true;
910
911
                    }  else if( $question_data[ 'user_question_grade' ] > 0 ) {
912
913
                        $user_correct = true;
914
915
                    }
916
917
                }
918
919
                // setup the option specific classes
920
                $answer_class = '';
921
                if( isset( $user_correct ) && 0 < $question_data[ 'question_grade' ] ) {
922
                    if ( is_array( $question_data['question_right_answer'] ) && in_array($answer, $question_data['question_right_answer']) ) {
923
924
                        $answer_class .= ' right_answer';
925
926
                    }  elseif( !is_array($question_data['question_right_answer']) && $question_data['question_right_answer'] == $answer ) {
927
928
                        $answer_class .= ' right_answer';
929
930
                    } elseif( ( is_array( $question_data['user_answer_entry']  ) && in_array($answer, $question_data['user_answer_entry'] ) )
931
                        ||  ( !  $question_data['user_answer_entry'] &&  $question_data['user_answer_entry'] == $answer ) ) {
932
933
                        $answer_class = 'user_wrong';
934
                        if( $user_correct ) {
935
936
                            $answer_class = 'user_right';
937
938
                        }
939
940
                    }
941
942
                }
943
944
                // determine if the current option must be checked
945
                $checked = '';
946
                if ( isset( $question_data['user_answer_entry'] ) && 0 < count( $question_data['user_answer_entry'] ) ) {
947
                    if ( is_array( $question_data['user_answer_entry'] ) && in_array( $answer, $question_data['user_answer_entry'] ) ) {
948
949
                        $checked = 'checked="checked"';
950
951
                    } elseif ( !is_array( $question_data['user_answer_entry'] ) ) {
952
953
                        $checked = checked( $answer, $question_data['user_answer_entry'] , false );
954
955
                    }
956
957
                } // End If Statement
958
959
                //Load the answer option data
960
                $question_option[ 'ID' ]          = Sensei()->lesson->get_answer_id( $answer );
961
                $question_option[ 'answer' ]      = $answer;
962
                $question_option[ 'option_class'] = $answer_class;
963
                $question_option[ 'checked']      = $checked;
964
                $question_option[ 'count' ]       = $count;
965
                $question_option[ 'type' ] = $answer_type;
966
967
                // add the speci  fic option to the list of options for this question
968
                $question_answers_options[$question_option[ 'ID' ]] = $question_option;
969
970
            } // end for each option
971
972
973
            // Shuffle the array depending on the settings
974
            $answer_options_sorted = array();
975
            $random_order = get_post_meta( $question_data['ID'], '_random_order', true );
976
            if(  $random_order && $random_order == 'yes' ) {
977
978
                $answer_options_sorted = $question_answers_options;
979
                shuffle( $answer_options_sorted );
980
981
            } else {
982
983
                $answer_order = array();
984
                $answer_order_string = get_post_meta( $question_data['ID'], '_answer_order', true );
985
                if( $answer_order_string ) {
986
987
                    $answer_order = array_filter( explode( ',', $answer_order_string ) );
988 View Code Duplication
                    if( count( $answer_order ) > 0 ) {
989
990
                        foreach( $answer_order as $answer_id ) {
991
992
                            if( isset( $question_answers_options[ $answer_id ] ) ) {
993
994
                                $answer_options_sorted[ $answer_id ] = $question_answers_options[ $answer_id ];
995
                                unset( $question_answers_options[ $answer_id ] );
996
997
                            }
998
999
                        }
1000
1001
                        if( count( $question_answers_options ) > 0 ) {
1002
                            foreach( $question_answers_options as $id => $answer ) {
1003
1004
                                $answer_options_sorted[ $id ] = $answer;
1005
1006
                            }
1007
                        }
1008
1009
                    }else{
1010
1011
                        $answer_options_sorted = $question_answers_options;
1012
1013
                    }
1014
1015
                }else{
1016
1017
                    $answer_options_sorted = $question_answers_options;
1018
1019
                } // end if $answer_order_string
1020
1021
            } // end if random order
1022
1023
1024
            // assemble and setup the data for the templates data array
1025
            $question_data[ 'answer_options' ]    =  $answer_options_sorted;
1026
1027
        }
1028
1029
        return $question_data;
1030
1031
    }//  end multiple_choice_load_question_data
1032
1033
    /**
1034
     * Load the gap fill question data on the sensei_get_question_template_data
1035
     * filter.
1036
     *
1037
     * @since 1.9.0
1038
     *
1039
     * @param $question_data
1040
     * @param $question_id
1041
     * @param $quiz_id
1042
     *
1043
     * @return array()
1044
     */
1045
    public static function gap_fill_load_question_data( $question_data, $question_id, $quiz_id ){
1046
1047
        if( 'gap-fill' == Sensei()->question->get_question_type( $question_id ) ) {
1048
1049
            $gapfill_array = explode( '||', $question_data[ 'question_right_answer' ] );
1050
            $question_data[ 'gapfill_pre' ]  = isset( $gapfill_array[0] ) ? $gapfill_array[0] : '';
1051
            $question_data[ 'gapfill_gap' ]  = isset( $gapfill_array[1] ) ? $gapfill_array[1] : '';
1052
            $question_data[ 'gapfill_post' ] = isset( $gapfill_array[2] ) ? $gapfill_array[2] : '';
1053
1054
        }
1055
1056
        return $question_data;
1057
1058
    }//  end gap_fill_load_question_data
1059
1060
1061
    /**
1062
     * Get the correct answer for a question
1063
     *
1064
     * @param $question_id
1065
     * @return string $correct_answer or empty
1066
     */
1067
    public static function get_correct_answer( $question_id ){
1068
1069
        $right_answer = get_post_meta( $question_id, '_question_right_answer', true );
1070
        $type = Sensei()->question->get_question_type( $question_id );
1071
        $type_name = __( 'Multiple Choice', 'woothemes-sensei' );
1072
        $grade_type = 'manual-grade';
1073
1074
        if ('boolean'== $type ) {
1075
1076
            $right_answer = ucfirst($right_answer);
1077
1078
        }elseif( 'multiple-choice' == $type ) {
1079
1080
            $right_answer = (array) $right_answer;
1081
            $right_answer = implode( ', ', $right_answer );
1082
1083
        }elseif( 'gap-fill' == $type ) {
1084
1085
            $right_answer_array = explode( '||', $right_answer );
1086
            if ( isset( $right_answer_array[0] ) ) { $gapfill_pre = $right_answer_array[0]; } else { $gapfill_pre = ''; }
1087
            if ( isset( $right_answer_array[1] ) ) { $gapfill_gap = $right_answer_array[1]; } else { $gapfill_gap = ''; }
1088
            if ( isset( $right_answer_array[2] ) ) { $gapfill_post = $right_answer_array[2]; } else { $gapfill_post = ''; }
1089
1090
            $right_answer = $gapfill_pre . ' <span class="highlight">' . $gapfill_gap . '</span> ' . $gapfill_post;
1091
1092
        }else{
1093
1094
            // for non auto gradable question types no answer should be returned.
1095
            $right_answer = '';
1096
1097
        }
1098
1099
        return $right_answer;
1100
1101
    } // get_correct_answer
1102
1103
} // End Class
1104
1105
/**
1106
 * Class WooThemes_Sensei_Question
1107
 * @ignore only for backward compatibility
1108
 * @since 1.9.0
1109
 */
1110
class WooThemes_Sensei_Question extends Sensei_Question{}
1111