_quizobj = $quizobj; $this->_timenow = $timenow; $this->create_standard_rules($canignoretimelimits); } private function create_standard_rules($canignoretimelimits) { $quiz = $this->_quizobj->get_quiz(); if ($quiz->attempts > 0) { $this->_rules[] = new num_attempts_access_rule($this->_quizobj, $this->_timenow); } $this->_rules[] = new open_close_date_access_rule($this->_quizobj, $this->_timenow); if (!empty($quiz->timelimit) && !$canignoretimelimits) { $this->_rules[] = new time_limit_access_rule($this->_quizobj, $this->_timenow); } if (!empty($quiz->delay1) || !empty($quiz->delay2)) { $this->_rules[] = new inter_attempt_delay_access_rule($this->_quizobj, $this->_timenow); } if (!empty($quiz->subnet)) { $this->_rules[] = new ipaddress_access_rule($this->_quizobj, $this->_timenow); } if (!empty($quiz->password)) { $this->_passwordrule = new password_access_rule($this->_quizobj, $this->_timenow); $this->_rules[] = $this->_passwordrule; } if (!empty($quiz->popup)) { if ($quiz->popup == 1) { $this->_securewindowrule = new securewindow_access_rule($this->_quizobj, $this->_timenow); $this->_rules[] = $this->_securewindowrule; } elseif ($quiz->popup == 2) { $this->_safebrowserrule = new safebrowser_access_rule($this->_quizobj, $this->_timenow); $this->_rules[] = $this->_safebrowserrule; } } } private function accumulate_messages(&$messages, $new) { if (is_array($new)) { $messages = array_merge($messages, $new); } else if (is_string($new) && $new) { $messages[] = $new; } } /** * Print each message in an array, surrounded by <p>, </p> tags. * * @param array $messages the array of message strings. * @param boolean $return if true, return a string, instead of outputting. * * @return mixed, if $return is true, return the string that would have been output, otherwise * return null. */ public function print_messages($messages, $return=false) { $output = ''; foreach ($messages as $message) { $output .= '

' . $message . "

\n"; } if ($return) { return $output; } else { echo $output; } } /** * Provide a description of the rules that apply to this quiz, such * as is shown at the top of the quiz view page. Note that not all * rules consider themselves important enough to output a description. * * @return array an array of description messages which may be empty. It * would be sensible to output each one surrounded by <p> tags. */ public function describe_rules() { $result = array(); foreach ($this->_rules as $rule) { $this->accumulate_messages($result, $rule->description()); } return $result; } /** * Is it OK to let the current user start a new attempt now? If there are * any restrictions in force now, return an array of reasons why access * should be blocked. If access is OK, return false. * * @param integer $numattempts the number of previous attempts this user has made. * @param object|false $lastattempt information about the user's last completed attempt. * if there is not a previous attempt, the false is passed. * @return mixed An array of reason why access is not allowed, or an empty array * (== false) if access should be allowed. */ public function prevent_new_attempt($numprevattempts, $lastattempt) { $reasons = array(); foreach ($this->_rules as $rule) { $this->accumulate_messages($reasons, $rule->prevent_new_attempt($numprevattempts, $lastattempt)); } return $reasons; } /** * Is it OK to let the current user start a new attempt now? If there are * any restrictions in force now, return an array of reasons why access * should be blocked. If access is OK, return false. * * @return mixed An array of reason why access is not allowed, or an empty array * (== false) if access should be allowed. */ public function prevent_access() { $reasons = array(); foreach ($this->_rules as $rule) { $this->accumulate_messages($reasons, $rule->prevent_access()); } return $reasons; } /** * Do any of the rules mean that this student will no be allowed any further attempts at this * quiz. Used, for example, to change the label by the grade displayed on the view page from * 'your current score is' to 'your final score is'. * * @param integer $numattempts the number of previous attempts this user has made. * @param object $lastattempt information about the user's last completed attempt. * @return boolean true if there is no way the user will ever be allowed to attempt this quiz again. */ public function is_finished($numprevattempts, $lastattempt) { foreach ($this->_rules as $rule) { if ($rule->is_finished($numprevattempts, $lastattempt)) { return true; } } return false; } /** * Do the printheader call, etc. required for a secure page, including the necessary JS. * * @param string $title HTML title tag content, passed to printheader. * @param string $headtags extra stuff to go in the HTML head tag, passed to printheader. */ public function setup_secure_page($title, $headtags = null) { $this->_securewindowrule->setup_secure_page($title, $headtags); } public function show_attempt_timer_if_needed($attempt, $timenow) { global $PAGE; $timeleft = false; foreach ($this->_rules as $rule) { $ruletimeleft = $rule->time_left($attempt, $timenow); if ($ruletimeleft !== false && ($timeleft === false || $ruletimeleft < $timeleft)) { $timeleft = $ruletimeleft; } } if ($timeleft !== false) { /// Make sure the timer starts just above zero. If $timeleft was <= 0, then /// this will just have the effect of causing the quiz to be submitted immediately. $timerstartvalue = max($timeleft, 1); $PAGE->requires->js_init_call('M.mod_quiz.timer.init', array($timerstartvalue), false, quiz_get_js_module()); } } /** * @return bolean if this quiz should only be shown to students in a secure window. */ public function securewindow_required($canpreview) { return !$canpreview && !is_null($this->_securewindowrule); } /** * @return bolean if this quiz should only be shown to students with safe browser. */ public function safebrowser_required($canpreview) { return !$canpreview && !is_null($this->_safebrowserrule); } /** * Print a button to start a quiz attempt, with an appropriate javascript warning, * depending on the access restrictions. The link will pop up a 'secure' window, if * necessary. * * @param boolean $canpreview whether this user can preview. This affects whether they must * use a secure window. * @param string $buttontext the label to put on the button. * @param boolean $unfinished whether the button is to continue an existing attempt, * or start a new one. This affects whether a javascript alert is shown. */ public function print_start_attempt_button($canpreview, $buttontext, $unfinished) { global $OUTPUT; $url = $this->_quizobj->start_attempt_url(); $button = new single_button($url, $buttontext); $button->class .= ' quizstartbuttondiv'; if (!$unfinished) { $strconfirmstartattempt = $this->confirm_start_attempt_message(); if ($strconfirmstartattempt) { $button->add_confirm_action($strconfirmstartattempt); } } $warning = ''; if ($this->securewindow_required($canpreview)) { $button->class .= ' quizsecuremoderequired'; $button->add_action(new popup_action('click', $url, 'quizpopup', securewindow_access_rule::$popupoptions)); $warning = html_writer::tag('noscript', $OUTPUT->heading(get_string('noscript', 'quiz'))); } echo $OUTPUT->render($button) . $warning; } /** * Send the user back to the quiz view page. Normally this is just a redirect, but * If we were in a secure window, we close this window, and reload the view window we came from. * * @param boolean $canpreview This affects whether we have to worry about secure window stuff. */ public function back_to_view_page($canpreview, $message = '') { global $CFG, $OUTPUT, $PAGE; $url = $this->_quizobj->view_url(); if ($this->securewindow_required($canpreview)) { $PAGE->set_pagelayout('popup'); echo $OUTPUT->header(); echo $OUTPUT->box_start(); if ($message) { echo '

' . $message . '

' . get_string('windowclosing', 'quiz') . '

'; $delay = 5; } else { echo '

' . get_string('pleaseclose', 'quiz') . '

'; $delay = 0; } $PAGE->requires->js_function_call('M.mod_quiz.secure_window.close', array($url, $delay)); echo $OUTPUT->box_end(); echo $OUTPUT->footer(); die(); } else { redirect($url, $message); } } /** * Print a control to finish the review. Normally this is just a link, but if we are * in a secure window, it needs to be a button that does M.mod_quiz.secure_window.close. * * @param boolean $canpreview This affects whether we have to worry about secure window stuff. */ public function print_finish_review_link($canpreview, $return = false) { global $CFG; $output = ''; $url = $this->_quizobj->view_url(); $output .= '
'; if ($this->securewindow_required($canpreview)) { $url = addslashes_js(htmlspecialchars($url)); $output .= '\n"; } else { $output .= '' . get_string('finishreview', 'quiz') . "\n"; } $output .= "
\n"; if ($return) { return $output; } else { echo $output; } } /** * @return bolean if this quiz is password protected. */ public function password_required() { return !is_null($this->_passwordrule); } /** * Clear the flag in the session that says that the current user is allowed to do this quiz. */ public function clear_password_access() { if (!is_null($this->_passwordrule)) { $this->_passwordrule->clear_access_allowed(); } } /** * Actually ask the user for the password, if they have not already given it this session. * This function only returns is access is OK. * * @param boolean $canpreview used to enfore securewindow stuff. */ public function do_password_check($canpreview) { if (!is_null($this->_passwordrule)) { $this->_passwordrule->do_password_check($canpreview, $this); } } /** * @return string if the quiz policies merit it, return a warning string to be displayed * in a javascript alert on the start attempt button. */ public function confirm_start_attempt_message() { $quiz = $this->_quizobj->get_quiz(); if ($quiz->timelimit && $quiz->attempts) { return get_string('confirmstartattempttimelimit','quiz', $quiz->attempts); } else if ($quiz->timelimit) { return get_string('confirmstarttimelimit','quiz'); } else if ($quiz->attempts) { return get_string('confirmstartattemptlimit','quiz', $quiz->attempts); } return ''; } /** * Make some text into a link to review the quiz, if that is appropriate. * * @param string $linktext some text. * @param object $attempt the attempt object * @return string some HTML, the $linktext either unmodified or wrapped in a link to the review page. */ public function make_review_link($attempt, $canpreview, $reviewoptions) { global $CFG; /// If review of responses is not allowed, or the attempt is still open, don't link. if (!$attempt->timefinish) { return ''; } if (!$reviewoptions->responses) { $message = $this->cannot_review_message($reviewoptions, true); if ($message) { return '' . $message . ''; } else { return ''; } } $linktext = get_string('review', 'quiz'); /// It is OK to link, does it need to be in a secure window? if ($this->securewindow_required($canpreview)) { return $this->_securewindowrule->make_review_link($linktext, $attempt->id); } else { return '' . $linktext . ''; } } /** * If $reviewoptions->responses is false, meaning that students can't review this * attempt at the moment, return an appropriate string explaining why. * * @param object $reviewoptions as obtained from quiz_get_reviewoptions. * @param boolean $short if true, return a shorter string. * @return string an appropraite message. */ public function cannot_review_message($reviewoptions, $short = false) { $quiz = $this->_quizobj->get_quiz(); if ($short) { $langstrsuffix = 'short'; $dateformat = get_string('strftimedatetimeshort', 'langconfig'); } else { $langstrsuffix = ''; $dateformat = ''; } if ($reviewoptions->quizstate == QUIZ_STATE_IMMEDIATELY) { return ''; } else if ($reviewoptions->quizstate == QUIZ_STATE_OPEN && $quiz->timeclose && ($quiz->review & QUIZ_REVIEW_CLOSED & QUIZ_REVIEW_RESPONSES)) { return get_string('noreviewuntil' . $langstrsuffix, 'quiz', userdate($quiz->timeclose, $dateformat)); } else { return get_string('noreview' . $langstrsuffix, 'quiz'); } } } /** * A base class that defines the interface for the various quiz access rules. * Most of the methods are defined in a slightly unnatural way because we either * want to say that access is allowed, or explain the reason why it is block. * Therefore instead of is_access_allowed(...) we have prevent_access(...) that * return false if access is permitted, or a string explanation (which is treated * as true) if access should be blocked. Slighly unnatural, but acutally the easist * way to implement this. */ abstract class quiz_access_rule_base { protected $_quiz; protected $_quizobj; protected $_timenow; /** * Create an instance of this rule for a particular quiz. * @param object $quiz the quiz we will be controlling access to. */ public function __construct($quizobj, $timenow) { $this->_quizobj = $quizobj; $this->_quiz = $quizobj->get_quiz(); $this->_timenow = $timenow; } /** * Whether or not a user should be allowed to start a new attempt at this quiz now. * @param integer $numattempts the number of previous attempts this user has made. * @param object $lastattempt information about the user's last completed attempt. * @return string false if access should be allowed, a message explaining the reason if access should be prevented. */ public function prevent_new_attempt($numprevattempts, $lastattempt) { return false; } /** * Whether or not a user should be allowed to start a new attempt at this quiz now. * @return string false if access should be allowed, a message explaining the reason if access should be prevented. */ public function prevent_access() { return false; } /** * Information, such as might be shown on the quiz view page, relating to this restriction. * There is no obligation to return anything. If it is not appropriate to tell students * about this rule, then just return ''. * @return mixed a message, or array of messages, explaining the restriction * (may be '' if no message is appropriate). */ public function description() { return ''; } /** * If this rule can determine that this user will never be allowed another attempt at * this quiz, then return true. This is used so we can know whether to display a * final score on the view page. This will only be called if there is not a currently * active attempt for this user. * @param integer $numattempts the number of previous attempts this user has made. * @param object $lastattempt information about the user's last completed attempt. * @return boolean true if this rule means that this user will never be allowed another * attempt at this quiz. */ public function is_finished($numprevattempts, $lastattempt) { return false; } /** * If, becuase of this rule, the user has to finish their attempt by a certain time, * you should override this method to return the amount of time left in seconds. * @param object $attempt the current attempt * @param integer $timenow the time now. We don't use $this->_timenow, so we can * give the user a more accurate indication of how much time is left. * @return mixed false if there is no deadline, of the time left in seconds if there is one. */ public function time_left($attempt, $timenow) { return false; } } /** * A rule controlling the number of attempts allowed. */ class num_attempts_access_rule extends quiz_access_rule_base { public function description() { return get_string('attemptsallowedn', 'quiz', $this->_quiz->attempts); } public function prevent_new_attempt($numprevattempts, $lastattempt) { if ($numprevattempts >= $this->_quiz->attempts) { return get_string('nomoreattempts', 'quiz'); } return false; } public function is_finished($numprevattempts, $lastattempt) { return $numprevattempts >= $this->_quiz->attempts; } } /** * A rule enforcing open and close dates. */ class open_close_date_access_rule extends quiz_access_rule_base { public function description() { $result = array(); if ($this->_timenow < $this->_quiz->timeopen) { $result[] = get_string('quiznotavailable', 'quiz', userdate($this->_quiz->timeopen)); } else if ($this->_quiz->timeclose && $this->_timenow > $this->_quiz->timeclose) { $result[] = get_string("quizclosed", "quiz", userdate($this->_quiz->timeclose)); } else { if ($this->_quiz->timeopen) { $result[] = get_string('quizopenedon', 'quiz', userdate($this->_quiz->timeopen)); } if ($this->_quiz->timeclose) { $result[] = get_string('quizcloseson', 'quiz', userdate($this->_quiz->timeclose)); } } return $result; } public function prevent_access() { if ($this->_timenow < $this->_quiz->timeopen || ($this->_quiz->timeclose && $this->_timenow > $this->_quiz->timeclose)) { return get_string('notavailable', 'quiz'); } return false; } public function is_finished($numprevattempts, $lastattempt) { return $this->_quiz->timeclose && $this->_timenow > $this->_quiz->timeclose; } public function time_left($attempt, $timenow) { // If this is a teacher preview after the close date, do not show // the time. if ($attempt->preview && $timenow > $this->_quiz->timeclose) { return false; } // Otherwise, return to the time left until the close date, providing // that is less than QUIZ_SHOW_TIME_BEFORE_DEADLINE if ($this->_quiz->timeclose) { $timeleft = $this->_quiz->timeclose - $timenow; if ($timeleft < QUIZ_SHOW_TIME_BEFORE_DEADLINE) { return $timeleft; } } return false; } } /** * A rule imposing the delay between attemtps settings. */ class inter_attempt_delay_access_rule extends quiz_access_rule_base { public function prevent_new_attempt($numprevattempts, $lastattempt) { if ($this->_quiz->attempts > 0 && $numprevattempts >= $this->_quiz->attempts) { /// No more attempts allowed anyway. return false; } if ($this->_quiz->timeclose != 0 && $this->_timenow > $this->_quiz->timeclose) { /// No more attempts allowed anyway. return false; } $nextstarttime = $this->compute_next_start_time($numprevattempts, $lastattempt); if ($this->_timenow < $nextstarttime) { if ($this->_quiz->timeclose == 0 || $nextstarttime <= $this->_quiz->timeclose) { return get_string('youmustwait', 'quiz', userdate($nextstarttime)); } else { return get_string('youcannotwait', 'quiz'); } } return false; } /** * Compute the next time a student would be allowed to start an attempt, * according to this rule. * @param integer $numprevattempts number of previous attempts. * @param object $lastattempt information about the previous attempt. * @return number the time. */ protected function compute_next_start_time($numprevattempts, $lastattempt) { if ($numprevattempts == 0) { return 0; } $lastattemptfinish = $lastattempt->timefinish; if ($this->_quiz->timelimit > 0){ $lastattemptfinish = min($lastattemptfinish, $lastattempt->timestart + $this->_quiz->timelimit); } if ($numprevattempts == 1 && $this->_quiz->delay1) { return $lastattemptfinish + $this->_quiz->delay1; } else if ($numprevattempts > 1 && $this->_quiz->delay2) { return $lastattemptfinish + $this->_quiz->delay2; } return 0; } public function is_finished($numprevattempts, $lastattempt) { $nextstarttime = $this->compute_next_start_time($numprevattempts, $lastattempt); return $this->_timenow <= $nextstarttime && $this->_quiz->timeclose != 0 && $nextstarttime >= $this->_quiz->timeclose; } } /** * A rule implementing the ipaddress check against the ->submet setting. */ class ipaddress_access_rule extends quiz_access_rule_base { public function prevent_access() { if (address_in_subnet(getremoteaddr(), $this->_quiz->subnet)) { return false; } else { return get_string('subnetwrong', 'quiz'); } } } /** * A rule representing the password check. It does not actually implement the check, * that has to be done directly in attempt.php, but this facilitates telling users about it. */ class password_access_rule extends quiz_access_rule_base { public function description() { return get_string('requirepasswordmessage', 'quiz'); } /** * Clear the flag in the session that says that the current user is allowed to do this quiz. */ public function clear_access_allowed() { global $SESSION; if (!empty($SESSION->passwordcheckedquizzes[$this->_quiz->id])) { unset($SESSION->passwordcheckedquizzes[$this->_quiz->id]); } } /** * Actually ask the user for the password, if they have not already given it this session. * This function only returns is access is OK. * * @param boolean $canpreview used to enfore securewindow stuff. * @param object $accessmanager the accessmanager calling us. */ public function do_password_check($canpreview, $accessmanager) { global $CFG, $SESSION, $OUTPUT, $PAGE; /// We have already checked the password for this quiz this session, so don't ask again. if (!empty($SESSION->passwordcheckedquizzes[$this->_quiz->id])) { return; } /// If the user cancelled the password form, send them back to the view page. if (optional_param('cancelpassword', false, PARAM_BOOL)) { $accessmanager->back_to_view_page($canpreview); } /// If they entered the right password, let them in. $enteredpassword = optional_param('quizpassword', '', PARAM_RAW); $validpassword = false; if (strcmp($this->_quiz->password, $enteredpassword) === 0) { $validpassword = true; } else if (isset($this->_quiz->extrapasswords)) { // group overrides may have additional passwords foreach ($this->_quiz->extrapasswords as $password) { if (strcmp($password, $enteredpassword) === 0) { $validpassword = true; break; } } } if ($validpassword) { $SESSION->passwordcheckedquizzes[$this->_quiz->id] = true; return; } /// User entered the wrong password, or has not entered one yet, so display the form. $output = ''; /// Start the page and print the quiz intro, if any. if ($accessmanager->securewindow_required($canpreview)) { $accessmanager->setup_secure_page($this->_quizobj->get_course()->shortname . ': ' . format_string($this->_quizobj->get_quiz_name())); } else if ($accessmanager->safebrowser_required($canpreview)) { $PAGE->set_title($this->_quizobj->get_course()->shortname . ': '.format_string($this->_quizobj->get_quiz_name())); $PAGE->set_cacheable(false); echo $OUTPUT->header(); } else { $PAGE->set_title(format_string($this->_quizobj->get_quiz_name())); echo $OUTPUT->header(); } if (trim(strip_tags($this->_quiz->intro))) { $output .= $OUTPUT->box(format_module_intro('quiz', $this->_quiz, $this->_quizobj->get_cmid()), 'generalbox', 'intro'); } $output .= $OUTPUT->box_start('generalbox', 'passwordbox'); /// If they have previously tried and failed to enter a password, tell them it was wrong. if (!empty($enteredpassword)) { $output .= '

' . get_string('passworderror', 'quiz') . '

'; } /// Print the password entry form. $output .= '

' . get_string('requirepasswordmessage', 'quiz') . "

\n"; $output .= '
' . "\n"; $output .= "
\n"; $output .= '\n"; $output .= '' . "\n"; $output .= '' . "\n"; $output .= '' . "\n"; $output .= ''; $output .= '' . "\n"; $output .= "
\n"; $output .= "
\n"; /// Finish page. $output .= $OUTPUT->box_end(); /// return or display form. echo $output; echo $OUTPUT->footer(); exit; } } /** * A rule representing the time limit. It does not actually restrict access, but we use this * class to encapsulate some of the relevant code. */ class time_limit_access_rule extends quiz_access_rule_base { public function description() { return get_string('quiztimelimit', 'quiz', format_time($this->_quiz->timelimit)); } public function time_left($attempt, $timenow) { return $attempt->timestart + $this->_quiz->timelimit - $timenow; } } /** * A rule for ensuring that the quiz is opened in a popup, with some JavaScript * to prevent copying and pasting, etc. */ class securewindow_access_rule extends quiz_access_rule_base { /** * @var array options that should be used for opening the secure popup. */ public static $popupoptions = array( 'left' => 0, 'top' => 0, 'fullscreen' => true, 'scrollbars' => true, 'resizeable' => false, 'directories' => false, 'toolbar' => false, 'titlebar' => false, 'location' => false, 'status' => false, 'menubar' => false, ); /** * Make a link to the review page for an attempt. * * @param string $linktext the desired link text. * @param integer $attemptid the attempt id. * @return string HTML for the link. */ public function make_review_link($linktext, $attemptid) { global $OUTPUT; $url = $this->_quizobj->review_url($attemptid); $button = new single_button($url, $linktext); $button->add_action(new popup_action('click', $url, 'quizpopup', self::$popupoptions)); return $OUTPUT->render($button); } /** * Do the printheader call, etc. required for a secure page, including the necessary JS. * * @param string $title HTML title tag content, passed to printheader. * @param string $headtags extra stuff to go in the HTML head tag, passed to printheader. * $headtags has been deprectaed since Moodle 2.0 */ public function setup_secure_page($title, $headtags=null) { global $OUTPUT, $PAGE; $PAGE->set_popup_notification_allowed(false);//prevent message notifications $PAGE->set_title($title); $PAGE->set_cacheable(false); $PAGE->set_pagelayout('popup'); $PAGE->add_body_class('quiz-secure-window'); $PAGE->requires->js_init_call('M.mod_quiz.secure_window.init', null, false, quiz_get_js_module()); echo $OUTPUT->header(); } } /** * A rule representing the safe browser check. */ class safebrowser_access_rule extends quiz_access_rule_base { public function prevent_access() { if (!$this->_quizobj->is_preview_user() && !quiz_check_safe_browser()) { return get_string('safebrowsererror', 'quiz'); } else { return false; } } public function description() { return get_string("safebrowsernotice", "quiz"); } }