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-quiz.php (1 issue)

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 Quiz Class
6
 *
7
 * All functionality pertaining to the quiz post type in Sensei.
8
 *
9
 * @package Assessment
10
 * @author Automattic
11
 *
12
 * @since 1.0.0
13
 */
14
 class Sensei_Quiz {
15
	public $token;
16
	public $meta_fields;
17
	public $file;
18
19
	/**
20
	 * Constructor.
21
	 * @since  1.0.0
22
	 *
23
	 * @param $file
24
	 */
25
	public function __construct ( $file = __FILE__ ) {
26
		$this->file = $file;
27
        $this->token = 'quiz';
28
		$this->meta_fields = array( 'quiz_passmark', 'quiz_lesson', 'quiz_type', 'quiz_grade_type', 'pass_required','enable_quiz_reset' );
29
		add_action( 'save_post', array( $this, 'update_author' ));
30
31
		// listen to the reset button click
32
		add_action( 'template_redirect', array( $this, 'reset_button_click_listener'  ) );
33
34
        // fire the complete quiz button submit for grading action
35
        add_action( 'sensei_single_quiz_content_inside_before', array( $this, 'user_quiz_submit_listener' ) );
36
37
		// fire the save user answers quiz button click responder
38
		add_action( 'sensei_single_quiz_content_inside_before', array( $this, 'user_save_quiz_answers_listener' ) );
39
40
        // fire the load global data function
41
        add_action( 'sensei_single_quiz_content_inside_before', array( $this, 'load_global_quiz_data' ), 80 );
42
43
        add_action( 'template_redirect', array ( $this, 'quiz_has_no_questions') );
44
45
46
    } // End __construct()
47
48
	/**
49
	* Update the quiz author when the lesson post type is save
50
	*
51
	* @param int $post_id
52
	* @return void
53
	*/
54
	public function update_author( $post_id ){
55
56
		// If this isn't a 'lesson' post, don't update it.
57
        // if this is a revision don't save it
58
	    if ( isset( $_POST['post_type'] ) && 'lesson' != $_POST['post_type']
59
            || wp_is_post_revision( $post_id ) ) {
60
61
                return;
62
63
        }
64
	    // get the lesson author id to be use late
65
	    $saved_post = get_post( $post_id );
66
	    $new_lesson_author_id =  $saved_post->post_author;
67
68
	    //get the lessons quiz
69
		$lesson_quizzes = Sensei()->lesson->lesson_quizzes( $post_id );
70
	    foreach ( (array) $lesson_quizzes as $quiz_item ) {
71
72
	    	if( ! $quiz_item ) {
73
	    		continue;
74
	    	}
75
76
		    // setup the quiz items new author value
77
			$my_post = array(
78
			      'ID'           => $quiz_item,
79
			      'post_author' =>  $new_lesson_author_id
80
			);
81
82
            // remove the action so that it doesn't fire again
83
            remove_action( 'save_post', array( $this, 'update_author' ));
84
85
			// Update the post into the database
86
		  	wp_update_post( $my_post );
87
	    }
88
89
	    return;
90
	}// end update_author
91
92
93
	/**
94
	 * Get the lesson this quiz belongs to
95
	 *
96
	 * @since 1.7.2
97
	 * @param int $quiz_id
98
	 * @return int @lesson_id
99
	 */
100
	public function get_lesson_id( $quiz_id ){
101
102
		if( empty( $quiz_id ) || ! intval( $quiz_id ) > 0 ){
103
			global $post;
104
			if( 'quiz' == get_post_type( $post ) ){
105
				$quiz_id = $post->ID;
106
			}else{
107
				return false;
108
			}
109
110
		}
111
112
		$quiz = get_post( $quiz_id );
113
		$lesson_id = $quiz->post_parent;
114
115
		return $lesson_id;
116
117
	} // end lesson
118
119
120
    /**
121
     * user_save_quiz_answers_listener
122
     *
123
     * This function hooks into the quiz page and accepts the answer form save post.
124
     * @since 1.7.3
125
     * @return bool $saved;
126
     */
127
    public function user_save_quiz_answers_listener(){
128
129 View Code Duplication
        if( ! isset( $_POST[ 'quiz_save' ])
130
            || !isset( $_POST[ 'sensei_question' ] )
131
            || empty( $_POST[ 'sensei_question' ] )
132
            ||  ! wp_verify_nonce( $_POST['woothemes_sensei_save_quiz_nonce'], 'woothemes_sensei_save_quiz_nonce'  ) > 1 ) {
133
            return;
134
        }
135
136
        global $post;
137
        $lesson_id = $this->get_lesson_id( $post->ID );
138
        $quiz_answers = $_POST[ 'sensei_question' ];
139
        // call the save function
140
        self::save_user_answers( $quiz_answers, $_FILES , $lesson_id  , get_current_user_id() );
141
142
        // remove the hook as it should only fire once per click
143
        remove_action( 'sensei_single_quiz_content_inside_before', 'user_save_quiz_answers_listener' );
144
145
    } // end user_save_quiz_answers_listener
146
147
	/**
148
	 * Save the user answers for the given lesson's quiz
149
	 *
150
	 * For this function you must supply all three parameters. If will return false one is left out.
151
	 *
152
	 * @since 1.7.4
153
	 * @access public
154
	 *
155
	 * @param array $quiz_answers
156
     * @param array $files from global $_FILES
157
	 * @param int $lesson_id
158
	 * @param int $user_id
159
	 *
160
	 * @return false or int $answers_saved
161
	 */
162
	public static function save_user_answers( $quiz_answers, $files = array(), $lesson_id , $user_id = 0 ){
163
164
        if( ! ( $user_id > 0 ) ){
165
            $user_id = get_current_user_id();
166
        }
167
168
        // make sure the parameters are valid before continuing
169 View Code Duplication
		if( empty( $lesson_id ) || empty( $user_id )
170
			|| 'lesson' != get_post_type( $lesson_id )
171
			||!get_userdata( $user_id )
172
			|| !is_array( $quiz_answers ) ){
173
174
			return false;
175
176
		}
177
178
179
        // start the lesson before saving the data in case the user has not started the lesson
180
        $activity_logged = Sensei_Utils::sensei_start_lesson( $lesson_id, $user_id );
181
182
		//prepare the answers
183
		$prepared_answers = self::prepare_form_submitted_answers( $quiz_answers , $files );
184
185
		// save the user data
186
        $answers_saved = Sensei_Utils::add_user_data( 'quiz_answers', $lesson_id, $prepared_answers, $user_id ) ;
187
188
		// were the answers saved correctly?
189
		if( intval( $answers_saved ) > 0){
190
191
            // save transient to make retrieval faster
192
            $transient_key = 'sensei_answers_'.$user_id.'_'.$lesson_id;
193
            set_transient( $transient_key, $prepared_answers, 10 * DAY_IN_SECONDS );
194
195
            // update the message showed to user
196
            Sensei()->frontend->messages = '<div class="sensei-message note">' . __( 'Quiz Saved Successfully.', 'woothemes-sensei' )  . '</div>';
197
        }
198
199
		return $answers_saved;
200
201
	}// end save_user_answers()
202
203
	/**
204
	 * Get the user answers for the given lesson's quiz.
205
     *
206
     * This function returns the data that is stored on the lesson as meta and is not compatible with
207
     * retrieving data for quiz answer before sensei 1.7.4
208
	 *
209
	 *
210
	 * @since 1.7.4
211
	 * @access public
212
	 *
213
	 * @param int $lesson_id
214
	 * @param int $user_id
215
	 *
216
	 * @return array $answers or false
217
	 */
218
	public function get_user_answers( $lesson_id, $user_id ){
219
220
		$answers = false;
221
222 View Code Duplication
		if ( ! intval( $lesson_id ) > 0 || 'lesson' != get_post_type( $lesson_id )
223
		|| ! intval( $user_id )  > 0 || !get_userdata( $user_id )  ) {
224
			return false;
225
		}
226
227
        // save some time and get the transient cached data
228
        $transient_key = 'sensei_answers_'.$user_id.'_'.$lesson_id;
229
        $transient_cached_answers = get_transient( $transient_key );
230
231
        // return the transient or get the values get the values from the comment meta
232
        if( !empty( $transient_cached_answers  ) && false != $transient_cached_answers ){
233
234
            $encoded_user_answers = $transient_cached_answers;
235
236
        }else{
237
238
            $encoded_user_answers = Sensei_Utils::get_user_data( 'quiz_answers', $lesson_id  , $user_id );
239
240
        } // end if transient check
241
242
		if( ! is_array( $encoded_user_answers ) ){
243
			return false;
244
		}
245
246
        //set the transient with the new valid data for faster retrieval in future
247
        set_transient( $transient_key,  $encoded_user_answers, 10 * DAY_IN_SECONDS);
248
249
		// decode an unserialize all answers
250
		foreach( $encoded_user_answers as $question_id => $encoded_answer ) {
251
			$decoded_answer = base64_decode( $encoded_answer );
252
			$answers[$question_id] = maybe_unserialize( $decoded_answer );
253
		}
254
255
		return $answers;
256
257
	}// end get_user_answers()
258
259
260
	/**
261
	 *
262
	 * This function runs on the init hook and checks if the reset quiz button was clicked.
263
	 *
264
	 * @since 1.7.2
265
	 * @hooked init
266
	 *
267
	 * @return void;
268
	 */
269
	public function reset_button_click_listener( ){
270
271
		if( ! isset( $_POST[ 'quiz_reset' ])
272
			||  ! wp_verify_nonce( $_POST['woothemes_sensei_reset_quiz_nonce'], 'woothemes_sensei_reset_quiz_nonce'  ) > 1 ) {
273
274
			return; // exit
275
		}
276
277
		global $post;
278
		$current_quiz_id = $post->ID;
279
		$lesson_id = $this->get_lesson_id( $current_quiz_id );
280
281
        // reset all user data
282
        $this->reset_user_lesson_data( $lesson_id, get_current_user_id() );
283
284
		//this function should only run once
285
		remove_action( 'template_redirect', array( $this, 'reset_button_click_listener'  ) );
286
287
	} // end reset_button_click_listener
288
289
	/**
290
	 * Complete/ submit  quiz hooked function
291
	 *
292
	 * This function listens to the complete button submit action and processes the users submitted answers
293
     * not that this function submits the given users quiz answers for grading.
294
	 *
295
	 * @since  1.7.4
296
	 * @access public
297
	 *
298
	 * @since
299
	 * @return void
300
	 */
301
	public function user_quiz_submit_listener() {
302
303
        // only respond to valid quiz completion submissions
304 View Code Duplication
        if( ! isset( $_POST[ 'quiz_complete' ])
305
            || !isset( $_POST[ 'sensei_question' ] )
306
            || empty( $_POST[ 'sensei_question' ] )
307
            ||  ! wp_verify_nonce( $_POST['woothemes_sensei_complete_quiz_nonce'], 'woothemes_sensei_complete_quiz_nonce'  ) > 1 ) {
308
            return;
309
        }
310
311
        global $post, $current_user;
312
        $lesson_id = $this->get_lesson_id( $post->ID );
313
        $quiz_answers = $_POST[ 'sensei_question' ];
314
315
        self::submit_answers_for_grading( $quiz_answers, $_FILES ,  $lesson_id  , $current_user->ID );
316
317
	} // End sensei_complete_quiz()
318
319
    /**
320
     * This function set's up the data need for the quiz page
321
     *
322
     * This function hooks into sensei_complete_quiz and load the global data for the
323
     * current quiz.
324
     *
325
     * @since 1.7.4
326
     * @access public
327
     *
328
     */
329
    public function load_global_quiz_data(){
330
331
        global  $post, $current_user;
332
        $this->data = new stdClass();
333
334
        // Default grade
335
        $grade = 0;
336
337
        // Get Quiz Questions
338
        $lesson_quiz_questions = Sensei()->lesson->lesson_quiz_questions( $post->ID );
339
340
        $quiz_lesson_id = absint( get_post_meta( $post->ID, '_quiz_lesson', true ) );
341
342
        // Get quiz grade type
343
        $quiz_grade_type = get_post_meta( $post->ID, '_quiz_grade_type', true );
344
345
        // Get quiz pass setting
346
        $pass_required = get_post_meta( $post->ID, '_pass_required', true );
347
348
        // Get quiz pass mark
349
        $quiz_passmark = abs( round( doubleval( get_post_meta( $post->ID, '_quiz_passmark', true ) ), 2 ) );
350
351
        // Get latest quiz answers and grades
352
        $lesson_id = Sensei()->quiz->get_lesson_id( $post->ID );
353
        $user_quizzes = Sensei()->quiz->get_user_answers( $lesson_id, get_current_user_id() );
354
        $user_lesson_status = Sensei_Utils::user_lesson_status( $quiz_lesson_id, $current_user->ID );
355
        $user_quiz_grade = 0;
356
        if( isset( $user_lesson_status->comment_ID ) ) {
357
            $user_quiz_grade = get_comment_meta( $user_lesson_status->comment_ID, 'grade', true );
358
        }
359
360
        if ( ! is_array($user_quizzes) ) { $user_quizzes = array(); }
361
362
        // Check again that the lesson is complete
363
        $user_lesson_end = Sensei_Utils::user_completed_lesson( $user_lesson_status );
364
        $user_lesson_complete = false;
365
        if ( $user_lesson_end ) {
366
            $user_lesson_complete = true;
367
        } // End If Statement
368
369
        $reset_allowed = get_post_meta( $post->ID, '_enable_quiz_reset', true );
370
        //backwards compatibility
371
        if( 'on' == $reset_allowed ) {
372
            $reset_allowed = 1;
373
        }
374
375
        // Build frontend data object for backwards compatibility
376
        // using this is no longer recommended
377
        $this->data->user_quiz_grade = $user_quiz_grade;// Sensei_Quiz::get_user_quiz_grade( $lesson_id, get_current_user_id() );
378
        $this->data->quiz_passmark = $quiz_passmark;
379
        $this->data->quiz_lesson = $quiz_lesson_id;
380
        $this->data->quiz_grade_type = $quiz_grade_type; // get_post_meta( $quiz_id, '_quiz_grade_type', true );
381
        $this->data->user_lesson_end = $user_lesson_end;
382
        $this->data->user_lesson_complete = $user_lesson_complete; //Sensei_Utils::user_completed_lesson( $lesson_id, get_current_user_id() );
383
        $this->data->lesson_quiz_questions = $lesson_quiz_questions;
384
        $this->data->reset_quiz_allowed = $reset_allowed; // Sensei_Quiz::is_reset_allowed( $lesson_id );
385
386
    } // end load_global_quiz_data
387
388
389
	/**
390
	 * This function converts the submitted array and makes it ready it for storage
391
	 *
392
	 * Creating a single array of all question types including file id's to be stored
393
	 * as comment meta by the calling function.
394
	 *
395
	 * @since 1.7.4
396
	 * @access public
397
	 *
398
	 * @param array $unprepared_answers
399
	 * @param $files
400
	 * @return array
401
	 */
402
	public static function prepare_form_submitted_answers( $unprepared_answers,  $files ){
403
404
405
		$prepared_answers = array();
406
407
		// validate incoming answers
408
		if( empty( $unprepared_answers  ) || ! is_array( $unprepared_answers ) ){
409
			return false;
410
		}
411
412
		// Loop through submitted quiz answers and save them appropriately
413
		foreach( $unprepared_answers as $question_id => $answer ) {
414
415
			//get the current questions question type
416
            $question_type = Sensei()->question->get_question_type( $question_id );
417
418
			// Sanitise answer
419
			if( 0 == get_magic_quotes_gpc() ) {
420
				$answer = wp_unslash( $answer );
421
			}
422
423
            // compress the answer for saving
424
			if( 'multi-line' == $question_type ) {
425
                $answer = esc_html( $answer );
426
            }elseif( 'file-upload' == $question_type  ){
427
                $file_key = 'file_upload_' . $question_id;
428
                if( isset( $files[ $file_key ] ) ) {
429
                        $attachment_id = Sensei_Utils::upload_file(  $files[ $file_key ] );
430
                        if( $attachment_id ) {
431
                            $answer = $attachment_id;
432
                        }
433
                    }
434
            } // end if
435
436
			$prepared_answers[ $question_id ] =  base64_encode( maybe_serialize( $answer ) );
437
438
		}// end for each $quiz_answers
439
440
		return $prepared_answers;
441
	} // prepare_form_submitted_answers
442
443
    /**
444
     * Reset user submitted questions
445
     *
446
     * This function resets the quiz data for a user that has been submitted fro grading already. It is different to
447
     * the save_user_answers as currently the saved and submitted answers are stored differently.
448
     *
449
     * @since 1.7.4
450
     * @access public
451
     *
452
     * @return bool $reset_success
453
     * @param int $user_id
454
     * @param int $lesson_id
455
     */
456
    public function reset_user_lesson_data( $lesson_id , $user_id = 0 ){
457
458
        //make sure the parameters are valid
459
        if( empty( $lesson_id ) || empty( $user_id )
460
            || 'lesson' != get_post_type( $lesson_id )
461
            || ! get_userdata( $user_id ) ){
462
            return false;
463
        }
464
465
466
467
        //get the users lesson status to make
468
        $user_lesson_status = Sensei_Utils::user_lesson_status( $lesson_id, $user_id );
469
        if( ! isset( $user_lesson_status->comment_ID ) ) {
470
            // this user is not taking this lesson so this process is not needed
471
            return false;
472
        }
473
474
        //get the lesson quiz and course
475
        $quiz_id = Sensei()->lesson->lesson_quizzes( $lesson_id );
476
        $course_id = Sensei()->lesson->get_course_id( $lesson_id );
477
478
        // reset the transients
479
        $answers_transient_key = 'sensei_answers_'.$user_id.'_'.$lesson_id;
480
        $grades_transient_key = 'quiz_grades_'.$user_id.'_'.$lesson_id;
481
        $answers_feedback_transient_key = 'sensei_answers_feedback_'.$user_id.'_'.$lesson_id;
482
        delete_transient( $answers_transient_key );
483
        delete_transient( $grades_transient_key );
484
        delete_transient( $answers_feedback_transient_key );
485
486
        // reset the quiz answers and feedback notes
487
        $deleted_answers = Sensei_Utils::delete_user_data( 'quiz_answers', $lesson_id, $user_id );
488
        $deleted_grades = Sensei_Utils::delete_user_data( 'quiz_grades', $lesson_id, $user_id );
489
        $deleted_user_feedback = Sensei_Utils::delete_user_data( 'quiz_answers_feedback', $lesson_id, $user_id );
490
491
        // Delete quiz answers, this auto deletes the corresponding meta data, such as the question/answer grade
492
        Sensei_Utils::sensei_delete_quiz_answers( $quiz_id, $user_id );
493
494
        Sensei_Utils::update_lesson_status( $user_id , $lesson_id, 'in-progress', array( 'questions_asked' => '', 'grade' => '' ) );
495
496
        // Update course completion
497
        Sensei_Utils::update_course_status( $user_id, $course_id );
498
499
        // Run any action on quiz/lesson reset (previously this didn't occur on resetting a quiz, see resetting a lesson in sensei_complete_lesson()
500
        do_action( 'sensei_user_lesson_reset', $user_id, $lesson_id );
501
        Sensei()->frontend->messages = '<div class="sensei-message note">' . __( 'Quiz Reset Successfully.', 'woothemes-sensei' ) . '</div>';
502
503
        return ( $deleted_answers && $deleted_grades ) ;
504
505
    } // end reset_user_lesson_data
506
507
     /**
508
      * Submit the users quiz answers for grading
509
      *
510
      * This function accepts users answers and stores it but also initiates the grading
511
      * if a quiz can be graded automatically it will, if not the answers can be graded by the teacher.
512
      *
513
      * @since 1.7.4
514
      * @access public
515
      *
516
      * @param array $quiz_answers
517
      * @param array $files from $_FILES
518
      * @param int $user_id
519
      * @param int $lesson_id
520
      *
521
      * @return bool $answers_submitted
522
      */
523
     public static function submit_answers_for_grading( $quiz_answers , $files = array() , $lesson_id , $user_id = 0 ){
524
525
         $answers_submitted = false;
526
527
         // get the user_id if none was passed in use the current logged in user
528
         if( ! intval( $user_id ) > 0 ) {
529
             $user_id = get_current_user_id();
530
         }
531
532
         // make sure the parameters are valid before continuing
533 View Code Duplication
         if( empty( $lesson_id ) || empty( $user_id )
534
             || 'lesson' != get_post_type( $lesson_id )
535
             ||!get_userdata( $user_id )
536
             || !is_array( $quiz_answers ) ){
537
538
             return false;
539
540
         }
541
542
         // Default grade
543
         $grade = 0;
544
545
         // Get Quiz ID
546
         $quiz_id = Sensei()->lesson->lesson_quizzes( $lesson_id );
547
548
         // Get quiz grade type
549
         $quiz_grade_type = get_post_meta( $quiz_id, '_quiz_grade_type', true );
550
551
         // Get quiz pass setting
552
         $pass_required = get_post_meta( $quiz_id, '_pass_required', true );
553
554
         // Get the minimum percentage need to pass this quiz
555
         $quiz_pass_percentage = abs( round( doubleval( get_post_meta( $quiz_id, '_quiz_passmark', true ) ), 2 ) );
556
557
         // Handle Quiz Questions asked
558
         // This is to ensure we save the questions that we've asked this user and that this can't be change unless
559
         // the quiz is reset by admin or user( user: only if the setting is enabled ).
560
         // get the questions asked when when the quiz questions were generated for the user : Sensei_Lesson::lesson_quiz_questions
561
         $user_lesson_status = Sensei_Utils::user_lesson_status( $lesson_id, $user_id );
562
         $questions_asked = get_comment_meta( $user_lesson_status->comment_ID, 'questions_asked', true );
563
         if( empty( $questions_asked ) ){
564
565
             $questions_asked = array_keys( $quiz_answers );
566
             $questions_asked_string = implode( ',', $questions_asked );
567
568
             // Save questions that were asked in this quiz
569
             update_comment_meta( $user_lesson_status->comment_ID, 'questions_asked', $questions_asked_string );
570
571
         }
572
573
         // Save Quiz Answers for grading, the save function also calls the sensei_start_lesson
574
         self::save_user_answers( $quiz_answers , $files , $lesson_id , $user_id );
575
576
         // Grade quiz
577
         $grade = Sensei_Grading::grade_quiz_auto( $quiz_id, $quiz_answers, 0 , $quiz_grade_type );
578
579
         // Get Lesson Grading Setting
580
         $lesson_metadata = array();
581
         $lesson_status = 'ungraded'; // Default when completing a quiz
582
583
         // At this point the answers have been submitted
584
         $answers_submitted = true;
585
586
         // if this condition is false the quiz should manually be graded by admin
587
         if ('auto' == $quiz_grade_type && ! is_wp_error( $grade )  ) {
588
589
             // Quiz has been automatically Graded
590 View Code Duplication
             if ( 'on' == $pass_required ) {
591
592
                 // Student has reached the pass mark and lesson is complete
593
                 if ( $quiz_pass_percentage <= $grade ) {
594
595
                     $lesson_status = 'passed';
596
597
                 } else {
598
599
                     $lesson_status = 'failed';
600
601
                 } // End If Statement
602
603
             } else {
604
605
                 // Student only has to partake the quiz
606
                 $lesson_status = 'graded';
607
608
             }
609
610
             $lesson_metadata['grade'] = $grade; // Technically already set as part of "WooThemes_Sensei_Utils::sensei_grade_quiz_auto()" above
611
612
         } // end if ! is_wp_error( $grade ...
613
614
         Sensei_Utils::update_lesson_status( $user_id, $lesson_id, $lesson_status, $lesson_metadata );
615
616
         if( 'passed' == $lesson_status || 'graded' == $lesson_status ){
617
618
             /**
619
              * Lesson end action hook
620
              *
621
              * This hook is fired after a lesson quiz has been graded and the lesson status is 'passed' OR 'graded'
622
              *
623
              * @param int $user_id
624
              * @param int $lesson_id
625
              */
626
             do_action( 'sensei_user_lesson_end', $user_id, $lesson_id );
627
628
         }
629
630
         /**
631
          * User quiz has been submitted
632
          *
633
          * Fires the end of the submit_answers_for_grading function. It will fire irrespective of the submission
634
          * results.
635
          *
636
          * @param int $user_id
637
          * @param int $quiz_id
638
          * @param string $grade
639
          * @param string $quiz_pass_percentage
640
          * @param string $quiz_grade_type
641
          */
642
         do_action( 'sensei_user_quiz_submitted', $user_id, $quiz_id, $grade, $quiz_pass_percentage, $quiz_grade_type );
643
644
         return $answers_submitted;
645
646
     }// end submit_answers_for_grading
647
648
     /**
649
      * Get the user question answer
650
      *
651
      * This function gets the the users saved answer on given quiz for the given question parameter
652
      * this function allows for a fallback to users still using the question saved data from before 1.7.4
653
      *
654
      * @since 1.7.4
655
      *
656
      * @param int  $lesson_id
657
      * @param int $question_id
658
      * @param int  $user_id ( optional )
659
      *
660
      * @return bool|null $answers_submitted
661
      */
662
     public function get_user_question_answer( $lesson_id, $question_id, $user_id = 0 ){
663
664
         // parameter validation
665 View Code Duplication
         if( empty( $lesson_id ) || empty( $question_id )
666
             || ! ( intval( $lesson_id  ) > 0 )
667
             || ! ( intval( $question_id  ) > 0 )
668
             || 'lesson' != get_post_type( $lesson_id )
669
             || 'question' != get_post_type( $question_id )) {
670
671
             return false;
672
         }
673
674
         if( ! ( intval( $user_id ) > 0 )   ){
675
             $user_id = get_current_user_id();
676
         }
677
678
         $users_answers = $this->get_user_answers( $lesson_id, $user_id );
679
680
         if( !$users_answers || empty( $users_answers )
681
         ||  ! is_array( $users_answers ) || ! isset( $users_answers[ $question_id ] ) ){
682
683
             //Fallback for pre 1.7.4 data
684
             $comment =  Sensei_Utils::sensei_check_for_activity( array( 'post_id' => $question_id, 'user_id' => $user_id, 'type' => 'sensei_user_answer' ), true );
685
686
             if( ! isset( $comment->comment_content ) ){
687
                 return NULL;
688
             }
689
690
             return maybe_unserialize( base64_decode( $comment->comment_content ) );
691
         }
692
693
         return $users_answers[ $question_id ];
694
695
     }// end get_user_question_answer
696
697
     /**
698
      * Saving the users quiz question grades
699
      *
700
      * This function save all the grades for all the question in a given quiz on the lesson
701
      * comment meta. It makes use of transients to save the grades for easier access at a later stage
702
      *
703
      * @since 1.7.4
704
      *
705
      * @param array $quiz_grades{
706
      *      @type int $question_id
707
      *      @type int $question_grade
708
      * }
709
      * @param $lesson_id
710
      * @param $user_id (Optional) will use the current user if not supplied
711
      *
712
      * @return bool
713
      */
714
     public function set_user_grades( $quiz_grades, $lesson_id, $user_id = 0 ){
715
716
         // get the user_id if none was passed in use the current logged in user
717
         if( ! intval( $user_id ) > 0 ) {
718
             $user_id = get_current_user_id();
719
         }
720
721
         // make sure the parameters are valid before continuing
722 View Code Duplication
         if( empty( $lesson_id ) || empty( $user_id )
723
             || 'lesson' != get_post_type( $lesson_id )
724
             ||!get_userdata( $user_id )
725
             || !is_array( $quiz_grades ) ){
726
727
             return false;
728
729
         }
730
731
         $success = false;
732
733
         // save that data for the user on the lesson comment meta
734
         $comment_meta_id = Sensei_Utils::add_user_data( 'quiz_grades', $lesson_id, $quiz_grades, $user_id   );
735
736
         // were the grades save successfully ?
737 View Code Duplication
         if( intval( $comment_meta_id ) > 0 ) {
738
739
             $success = true;
740
             // save transient
741
             $transient_key = 'quiz_grades_'. $user_id . '_' . $lesson_id;
742
             set_transient( $transient_key, $quiz_grades, 10 * DAY_IN_SECONDS );
743
         }
744
745
         return $success;
746
747
     }// end set_user_grades
748
749
     /**
750
      * Retrieve the users quiz question grades
751
      *
752
      * This function gets all the grades for all the questions in the given lesson quiz for a specific user.
753
      *
754
      * @since 1.7.4
755
      *
756
      * @param $lesson_id
757
      * @param $user_id (Optional) will use the current user if not supplied
758
      *
759
      * @return array $user_quiz_grades or false if none exists for this users
760
      */
761
     public function get_user_grades( $lesson_id, $user_id = 0 ){
762
763
         $user_grades = array();
764
765
         // get the user_id if none was passed in use the current logged in user
766
         if( ! intval( $user_id ) > 0 ) {
767
             $user_id = get_current_user_id();
768
         }
769
770 View Code Duplication
         if ( ! intval( $lesson_id ) > 0 || 'lesson' != get_post_type( $lesson_id )
771
             || ! intval( $user_id )  > 0 || !get_userdata( $user_id )  ) {
772
             return false;
773
         }
774
775
         // save some time and get the transient cached data
776
         $transient_key = 'quiz_grades_'. $user_id . '_' . $lesson_id;
777
         $user_grades = get_transient( $transient_key );
778
779
         // get the data if nothing was stored in the transient
780 View Code Duplication
         if( empty( $user_grades  ) || false != $user_grades ){
781
782
             $user_grades = Sensei_Utils::get_user_data( 'quiz_grades', $lesson_id, $user_id );
783
784
             //set the transient with the new valid data for faster retrieval in future
785
             set_transient( $transient_key,  $user_grades, 10 * DAY_IN_SECONDS );
786
787
         } // end if transient check
788
789
         // if there is no data for this user
790
         if( ! is_array( $user_grades ) ){
791
             return false;
792
         }
793
794
         return $user_grades;
795
796
     }// end  get_user_grades
797
798
     /**
799
      * Get the user question grade
800
      *
801
      * This function gets the grade on a quiz for the given question parameter
802
      * It does NOT do any grading. It simply retrieves the data that was stored during grading.
803
      * this function allows for a fallback to users still using the question saved data from before 1.7.4
804
      *
805
      * @since 1.7.4
806
      *
807
      * @param int  $lesson_id
808
      * @param int $question_id
809
      * @param int  $user_id ( optional )
810
      *
811
      * @return bool $question_grade
812
      */
813
     public function get_user_question_grade( $lesson_id, $question_id, $user_id = 0 ){
814
815
         // parameter validation
816 View Code Duplication
         if( empty( $lesson_id ) || empty( $question_id )
817
             || ! ( intval( $lesson_id  ) > 0 )
818
             || ! ( intval( $question_id  ) > 0 )
819
             || 'lesson' != get_post_type( $lesson_id )
820
             || 'question' != get_post_type( $question_id )) {
821
822
             return false;
823
         }
824
825
         $all_user_grades = self::get_user_grades( $lesson_id,$user_id );
826
827
         if( ! $all_user_grades || ! isset(  $all_user_grades[ $question_id ] ) ){
828
829
             //fallback to data pre 1.7.4
830
             $args = array(
831
                 'post_id' => $question_id,
832
                 'user_id' => $user_id,
833
                 'type'    => 'sensei_user_answer'
834
             );
835
836
             $question_activity = Sensei_Utils::sensei_check_for_activity( $args , true );
837
             $fall_back_grade = false;
838
             if( isset( $question_activity->comment_ID ) ){
839
                 $fall_back_grade = get_comment_meta(  $question_activity->comment_ID , 'user_grade', true );
840
             }
841
842
             return $fall_back_grade;
843
844
         } // end if $all_user_grades...
845
846
         return $all_user_grades[ $question_id ];
847
848
     }// end get_user_question_grade
849
850
     /**
851
      * Save the user's answers feedback
852
      *
853
      * For this function you must supply all three parameters. If will return false one is left out.
854
      * The data will be saved on the lesson ID supplied.
855
      *
856
      * @since 1.7.5
857
      * @access public
858
      *
859
      * @param array $answers_feedback{
860
      *  $type int $question_id
861
      *  $type string $question_feedback
862
      * }
863
      * @param int $lesson_id
864
      * @param int $user_id
865
      *
866
      * @return false or int $feedback_saved
867
      */
868
    public function save_user_answers_feedback( $answers_feedback, $lesson_id , $user_id = 0 ){
869
870
        // make sure the parameters are valid before continuing
871 View Code Duplication
        if( empty( $lesson_id ) || empty( $user_id )
872
            || 'lesson' != get_post_type( $lesson_id )
873
            ||!get_userdata( $user_id )
874
            || !is_array( $answers_feedback ) ){
875
876
            return false;
877
878
        }
879
880
881
        // check if the lesson is started before saving, if not start the lesson for the user
882
        if ( !( 0 < intval( Sensei_Utils::user_started_lesson( $lesson_id, $user_id) ) ) ) {
883
            Sensei_Utils::sensei_start_lesson( $lesson_id, $user_id );
884
        }
885
886
        // encode the feedback
887
        $encoded_answers_feedback =  array();
888
        foreach( $answers_feedback as $question_id => $feedback ){
889
            $encoded_answers_feedback[ $question_id ] = base64_encode( $feedback );
890
        }
891
892
        // save the user data
893
        $feedback_saved = Sensei_Utils::add_user_data( 'quiz_answers_feedback', $lesson_id , $encoded_answers_feedback, $user_id ) ;
894
895
        //Were the the question feedback save correctly?
896 View Code Duplication
        if( intval( $feedback_saved ) > 0){
897
898
            // save transient to make retrieval faster in future
899
             $transient_key = 'sensei_answers_feedback_'.$user_id.'_'.$lesson_id;
900
             set_transient( $transient_key, $encoded_answers_feedback, 10 * DAY_IN_SECONDS );
901
902
        }
903
904
        return $feedback_saved;
905
906
    } // end save_user_answers_feedback
907
908
     /**
909
      * Get the user's answers feedback.
910
      *
911
      * This function returns the feedback submitted by the teacher/admin
912
      * during grading. Grading occurs manually or automatically.
913
      *
914
      * @since 1.7.5
915
      * @access public
916
      *
917
      * @param int $lesson_id
918
      * @param int $user_id
919
      *
920
      * @return false | array $answers_feedback{
921
      *  $type int $question_id
922
      *  $type string $question_feedback
923
      * }
924
      */
925
     public function get_user_answers_feedback( $lesson_id , $user_id = 0 ){
926
927
         $answers_feedback = array();
928
929
         // get the user_id if none was passed in use the current logged in user
930
         if( ! intval( $user_id ) > 0 ) {
931
             $user_id = get_current_user_id();
932
         }
933
934 View Code Duplication
         if ( ! intval( $lesson_id ) > 0 || 'lesson' != get_post_type( $lesson_id )
935
             || ! intval( $user_id )  > 0 || !get_userdata( $user_id )  ) {
936
             return false;
937
         }
938
939
         // first check the transient to save a few split seconds
940
         $transient_key = 'sensei_answers_feedback_'.$user_id.'_'.$lesson_id;
941
         $encoded_feedback = get_transient( $transient_key );
942
943
         // get the data if nothing was stored in the transient
944 View Code Duplication
         if( empty( $encoded_feedback  ) || !$encoded_feedback ){
945
946
             $encoded_feedback = Sensei_Utils::get_user_data( 'quiz_answers_feedback', $lesson_id, $user_id );
947
948
             //set the transient with the new valid data for faster retrieval in future
949
             set_transient( $transient_key,  $encoded_feedback, 10 * DAY_IN_SECONDS);
950
951
         } // end if transient check
952
953
         // if there is no data for this user
954
         if( ! is_array( $encoded_feedback ) ){
955
             return false;
956
         }
957
958
         foreach( $encoded_feedback as $question_id => $feedback ){
959
960
             $answers_feedback[ $question_id ] = base64_decode( $feedback );
961
962
         }
963
964
         return $answers_feedback;
965
966
     } // end get_user_answers_feedback
967
968
     /**
969
      * Get the user's answer feedback for a specific question.
970
      *
971
      * This function gives you a single answer note/feedback string
972
      * for the user on the given question.
973
      *
974
      * @since 1.7.5
975
      * @access public
976
      *
977
      * @param int $lesson_id
978
      * @param int $question_id
979
      * @param int $user_id
980
      *
981
      * @return string $feedback or bool if false
982
      */
983
     public function get_user_question_feedback( $lesson_id, $question_id, $user_id = 0 ){
984
985
         $feedback = false;
986
987
         // parameter validation
988 View Code Duplication
         if( empty( $lesson_id ) || empty( $question_id )
989
             || ! ( intval( $lesson_id  ) > 0 )
990
             || ! ( intval( $question_id  ) > 0 )
991
             || 'lesson' != get_post_type( $lesson_id )
992
             || 'question' != get_post_type( $question_id )) {
993
994
             return false;
995
         }
996
997
         // get all the feedback for the user on the given lesson
998
         $all_feedback = $this->get_user_answers_feedback( $lesson_id, $user_id );
999
1000
         if( !$all_feedback || empty( $all_feedback )
1001
             || ! is_array( $all_feedback ) || ! isset( $all_feedback[ $question_id ] ) ){
1002
1003
             //fallback to data pre 1.7.4
1004
1005
             // setup the sensei data query
1006
             $args = array(
1007
                 'post_id' => $question_id,
1008
                 'user_id' => $user_id,
1009
                 'type'    => 'sensei_user_answer'
1010
             );
1011
             $question_activity = Sensei_Utils::sensei_check_for_activity( $args , true );
1012
1013
             // set the default to false and return that if no old data is available.
1014
             if( isset( $question_activity->comment_ID ) ){
1015
                 $feedback = base64_decode( get_comment_meta(  $question_activity->comment_ID , 'answer_note', true ) );
1016
             }
1017
1018
             // finally use the default question feedback
1019
             if( empty( $feedback ) ){
1020
                 $feedback = get_post_meta( $question_id, '_answer_feedback', true );
1021
             }
1022
1023
             return $feedback;
1024
1025
         }
1026
1027
         return $all_feedback[ $question_id ];
1028
1029
     } // end get_user_question_feedback
1030
1031
     /**
1032
      * Check if a quiz has no questions, and redirect back to lesson.
1033
      *
1034
      * Though a quiz is created for each lesson, it should not be visible
1035
      * unless it has questions.
1036
      *
1037
      * @since 1.9.0
1038
      * @access public
1039
      * @param none
1040
      * @return void
1041
      */
1042
1043
     public function quiz_has_no_questions() {
1044
1045
         if( ! is_singular( 'quiz' ) )  {
1046
             return;
1047
         }
1048
1049
         global $post;
1050
1051
         $lesson_id = $this->get_lesson_id($post->ID);
1052
1053
         $has_questions = get_post_meta( $lesson_id, '_quiz_has_questions', true );
1054
1055
         $lesson = get_post($lesson_id);
1056
1057
         if ( is_singular('quiz') && ! $has_questions && $_SERVER['REQUEST_URI'] != "/lesson/$lesson->post_name" ) {
1058
1059
             wp_redirect(get_permalink($lesson->ID), 301);
1060
             exit;
1061
1062
         }
1063
1064
     } // end quiz_has_no_questions
1065
1066
/**
1067
  * Deprecate the sensei_single_main_content on the single-quiz template.
1068
  *
1069
  * @deprecated since 1.9.0
1070
  */
1071
 public static function deprecate_quiz_sensei_single_main_content_hook(){
1072
1073
     sensei_do_deprecated_action('sensei_single_main_content', '1.9.0', 'sensei_single_quiz_content_inside_before or sensei_single_quiz_content_inside_after');
1074
1075
 }
1076
    /*
1077
     * Deprecate the sensei_quiz_single_title on the single-quiz template.
1078
     *
1079
     * @deprecated since 1.9.0
1080
     */
1081
     public static function deprecate_quiz_sensei_quiz_single_title_hook(){
1082
1083
         sensei_do_deprecated_action('sensei_quiz_single_title', '1.9.0', 'sensei_single_quiz_content_inside_before ');
1084
1085
     }
1086
1087
     /**
1088
      * Filter the single title and add the Quiz to it.
1089
      *
1090
      * @param string $title
1091
      * @param int $id title post id
1092
      * @return string $quiz_title
1093
      */
1094
     public static function single_quiz_title( $title, $post_id ){
1095
1096
         if( 'quiz' == get_post_type( $post_id ) ){
1097
1098
             $title_with_no_quizzes = $title;
1099
1100
             // if the title has quiz, remove it: legacy titles have the word quiz stored.
1101
             if( 1 < substr_count( strtoupper( $title_with_no_quizzes ), 'QUIZ' ) ){
1102
1103
                 // remove all possible appearances of quiz
1104
                 $title_with_no_quizzes = str_replace( 'quiz', '', $title  );
1105
                 $title_with_no_quizzes = str_replace( 'Quiz', '', $title_with_no_quizzes  );
1106
                 $title_with_no_quizzes = str_replace( 'QUIZ', '', $title_with_no_quizzes  );
1107
1108
             }
1109
1110
             $title = $title_with_no_quizzes .  ' ' . __( 'Quiz', 'woothemes-sensei' );
1111
         }
1112
1113
         /**
1114
          * hook document in class-woothemes-sensei-message.php
1115
          */
1116
         return apply_filters( 'sensei_single_title', $title, get_post_type( ) );
1117
1118
     }
1119
1120
     /**
1121
      * Initialize the quiz question loop on the single quiz template
1122
      *
1123
      * The function will create a global quiz loop varialbe.
1124
      *
1125
      * @since 1.9.0
1126
      *
1127
      */
1128
     public static function start_quiz_questions_loop(){
1129
1130
         global $sensei_question_loop;
1131
1132
         //intialize the questions loop object
1133
         $sensei_question_loop['current'] = -1;
1134
         $sensei_question_loop['total']   =  0;
1135
         $sensei_question_loop['questions'] = array();
1136
1137
1138
         $questions = Sensei()->lesson->lesson_quiz_questions( get_the_ID() );
1139
1140
         if( count( $questions  ) > 0  ){
1141
1142
             $sensei_question_loop['total']   =  count( $questions );
1143
             $sensei_question_loop['questions'] = $questions;
1144
             $sensei_question_loop['quiz_id'] = get_the_ID();
1145
1146
         }
1147
1148
     }// static function
1149
1150
     /**
1151
      * Initialize the quiz question loop on the single quiz template
1152
      *
1153
      * The function will create a global quiz loop varialbe.
1154
      *
1155
      * @since 1.9.0
1156
      *
1157
      */
1158
     public static function stop_quiz_questions_loop(){
1159
1160
         $sensei_question_loop['total']   =  0;
1161
         $sensei_question_loop['questions'] = array();
1162
         $sensei_question_loop['quiz_id'] = '';
1163
1164
     }
1165
1166
     /**
1167
      * Output the title for the single quiz page
1168
      *
1169
      * @since 1.9.0
1170
      */
1171
     public static function the_title(){
1172
         ?>
1173
         <header>
1174
1175
             <h1>
1176
1177
                 <?php
1178
                 /**
1179
                  * Filter documented in class-sensei-messages.php the_title
1180
                  */
1181
                 echo apply_filters( 'sensei_single_title', get_the_title( get_post() ), get_post_type( get_the_ID() ) );
1182
                 ?>
1183
1184
             </h1>
1185
1186
         </header>
1187
1188
         <?php
1189
     }//the_title
1190
1191
     /**
1192
      * Output the sensei quiz status message.
1193
      *
1194
      * @param $quiz_id
1195
      */
1196
    public static function  the_user_status_message( $quiz_id ){
1197
1198
        $lesson_id =  Sensei()->quiz->get_lesson_id( $quiz_id );
1199
        $status = Sensei_Utils::sensei_user_quiz_status_message( $lesson_id , get_current_user_id() );
1200
        echo '<div class="sensei-message ' . $status['box_class'] . '">' . $status['message'] . '</div>';
1201
1202
    }
1203
1204
     /**
1205
      * This functions runs the old sensei_quiz_action_buttons action
1206
      * for backwards compatiblity sake.
1207
      *
1208
      * @since 1.9.0
1209
      * @deprecated
1210
      */
1211
     public static function deprecate_sensei_quiz_action_buttons_hook(){
1212
1213
         sensei_do_deprecated_action( 'sensei_quiz_action_buttons', '1.9.0', 'sensei_single_quiz_questions_after');
1214
1215
     }
1216
1217
     /**
1218
      * The quiz action buttons needed to ouput quiz
1219
      * action such as reset complete and save.
1220
      *
1221
      * @since 1.3.0
1222
      */
1223
     public static function action_buttons() {
1224
1225
         global $post, $current_user;
1226
1227
         $lesson_id = (int) get_post_meta( $post->ID, '_quiz_lesson', true );
1228
         $lesson_course_id = (int) get_post_meta( $lesson_id, '_lesson_course', true );
1229
         $lesson_prerequisite = (int) get_post_meta( $lesson_id, '_lesson_prerequisite', true );
1230
         $show_actions = true;
1231
         $user_lesson_status = Sensei_Utils::user_lesson_status( $lesson_id, $current_user->ID );
1232
1233
         //setup quiz grade
1234
         $user_quiz_grade = '';
1235
         if( ! empty( $user_lesson_status  ) ){
1236
             $user_quiz_grade = get_comment_meta( $user_lesson_status->comment_ID, 'grade', true );
1237
         }
1238
1239
1240
         if( intval( $lesson_prerequisite ) > 0 ) {
1241
1242
             // If the user hasn't completed the prereq then hide the current actions
1243
             $show_actions = Sensei_Utils::user_completed_lesson( $lesson_prerequisite, $current_user->ID );
1244
1245
         }
1246
         if ( $show_actions && is_user_logged_in() && Sensei_Utils::user_started_course( $lesson_course_id, $current_user->ID ) ) {
1247
1248
             // Get Reset Settings
1249
             $reset_quiz_allowed = get_post_meta( $post->ID, '_enable_quiz_reset', true ); ?>
1250
1251
             <!-- Action Nonce's -->
1252
             <input type="hidden" name="woothemes_sensei_complete_quiz_nonce" id="woothemes_sensei_complete_quiz_nonce"
1253
                    value="<?php echo esc_attr(  wp_create_nonce( 'woothemes_sensei_complete_quiz_nonce' ) ); ?>" />
1254
             <input type="hidden" name="woothemes_sensei_reset_quiz_nonce" id="woothemes_sensei_reset_quiz_nonce"
1255
                    value="<?php echo esc_attr(  wp_create_nonce( 'woothemes_sensei_reset_quiz_nonce' ) ); ?>" />
1256
             <input type="hidden" name="woothemes_sensei_save_quiz_nonce" id="woothemes_sensei_save_quiz_nonce"
1257
                    value="<?php echo esc_attr(  wp_create_nonce( 'woothemes_sensei_save_quiz_nonce' ) ); ?>" />
1258
             <!--#end Action Nonce's -->
1259
1260
             <?php if ( '' == $user_quiz_grade) { ?>
1261
1262
                 <span><input type="submit" name="quiz_complete" class="quiz-submit complete" value="<?php  _e( 'Complete Quiz', 'woothemes-sensei' ); ?>"/></span>
1263
1264
                 <span><input type="submit" name="quiz_save" class="quiz-submit save" value="<?php _e( 'Save Quiz', 'woothemes-sensei' ); ?>"/></span>
1265
1266
             <?php } // End If Statement ?>
1267
1268
             <?php if ( isset( $reset_quiz_allowed ) && $reset_quiz_allowed ) { ?>
1269
1270
                 <span><input type="submit" name="quiz_reset" class="quiz-submit reset" value="<?php _e( 'Reset Quiz', 'woothemes-sensei' ); ?>"/></span>
1271
1272
             <?php } ?>
1273
1274
         <?php }
1275
1276
     } // End sensei_quiz_action_buttons()
1277
1278
     /**
1279
      * Fetch the quiz grade
1280
      *
1281
      * @since 1.9.0
1282
      *
1283
      * @param int $lesson_id
1284
      * @param int $user_id
1285
      *
1286
      * @return double $user_quiz_grade
1287
      */
1288
     public static function get_user_quiz_grade( $lesson_id, $user_id ){
1289
1290
         // get the quiz grade
1291
         $user_lesson_status = Sensei_Utils::user_lesson_status( $lesson_id, $user_id );
1292
         $user_quiz_grade = 0;
1293
         if( isset( $user_lesson_status->comment_ID ) ) {
1294
             $user_quiz_grade = get_comment_meta( $user_lesson_status->comment_ID, 'grade', true );
1295
         }
1296
1297
         return (double) $user_quiz_grade;
1298
1299
     }
1300
1301
     /**
1302
      * Check the quiz reset property for a given lesson's quiz.
1303
      *
1304
      * The data is stored on the quiz but going forward the quiz post
1305
      * type will be retired, hence the lesson_id is a require parameter.
1306
      *
1307
      * @since 1.9.0
1308
      *
1309
      * @param int $lesson_id
1310
      * @return bool
1311
      */
1312
     public static function is_reset_allowed( $lesson_id ){
1313
1314
         $quiz_id = Sensei()->lesson->lesson_quizzes( $lesson_id );
1315
1316
         $reset_allowed = get_post_meta( $quiz_id, '_enable_quiz_reset', true );
1317
         //backwards compatibility
1318
         if( 'on' == $reset_allowed ) {
1319
             $reset_allowed = 1;
1320
         }
1321
1322
         return (bool) $reset_allowed;
1323
1324
     }
1325
1326
 } // End Class WooThemes_Sensei_Quiz
1327
1328
1329
1330
/**
1331
 * Class WooThemes_Sensei_Quiz
1332
 * @ignore only for backward compatibility
1333
 * @since 1.9.0
1334
 */
1335
class WooThemes_Sensei_Quiz extends Sensei_Quiz{}
1336