_quiz = $quiz; $this->_timenow = $timenow; $this->create_standard_rules($canignoretimelimits); } private function create_standard_rules($canignoretimelimits) { if ($this->_quiz->attempts > 0) { $this->_rules[] = new num_attempts_access_rule($this->_quiz, $this->_timenow); } $this->_rules[] = new open_close_date_access_rule($this->_quiz, $this->_timenow); if ($this->_quiz->timelimit && !$canignoretimelimits) { $this->_rules[] = new time_limit_access_rule($this->_quiz, $this->_timenow); } if ($this->_quiz->delay1 || $this->_quiz->delay2) { $this->_rules[] = new inter_attempt_delay_access_rule($this->_quiz, $this->_timenow); } if ($this->_quiz->subnet) { $this->_rules[] = new ipaddress_access_rule($this->_quiz, $this->_timenow); } if ($this->_quiz->password) { $this->_passwordrule = new password_access_rule($this->_quiz, $this->_timenow); $this->_rules[] = $this->_passwordrule; } if ($this->_quiz->popup) { $this->_securewindowrule = new securewindow_access_rule($this->_quiz, $this->_timenow); $this->_rules[] = $this->_securewindowrule; } } 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, each 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 $lastattempt information about the user's last completed attempt. * @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; } public function setup_secure_page() { /// This prevents the message window coming up. define('MESSAGE_WINDOW', true); echo "\n\n", '\n"; } public function show_attempt_timer_if_needed($attempt, $timenow) { $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); print_box_start('', 'quiz-timer-outer'); print_heading(get_string('timeleft', 'quiz'), '', 3); echo ''; print_box_end(); echo "\n\n", '\n"; } } /** * @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 object the securewindow_access_rule instance for this quiz, * or null if securewindow_required returns false. */ public function get_securewindow_object() { return $this->_securewindowrule; } /** * @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. */ public function do_password_check() { if (!is_null($this->_passwordrule)) { $this->_passwordrule->do_password_check(); } } /** * @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() { if ($this->_quiz->timelimit && $this->_quiz->attempts) { return get_string('confirmstartattempttimelimit','quiz', $this->_quiz->attempts); } else if ($this->_quiz->timelimit) { return get_string('confirmstarttimelimit','quiz'); } else if ($this->_quiz->attempts) { return get_string('confirmstartattemptlimit','quiz', $this->_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 */ public function make_review_link($linktext, $attempt) { global $CFG; /// If not even responses are to be shown in review then we don't allow any review if (!($this->_quiz->review & QUIZ_REVIEW_RESPONSES)) { return $linktext; } /// If the quiz is still open, are reviews allowed? if ((!$this->_quiz->timeclose || time() < $this->_quiz->timeclose) && !($this->_quiz->review & QUIZ_REVIEW_OPEN)) { /// If not, don't link. return $linktext; } /// If the quiz is closed, are reviews allowed? if (($this->_quiz->timeclose && time() > $this->_quiz->timeclose) && !($this->_quiz->review & QUIZ_REVIEW_CLOSED)) { /// If not, don't link. return $linktext; } /// If the attempt is still open, don't link. if (!$attempt->timefinish) { return $linktext; } /// It is OK to link. // TODO replace this with logic that matches review.php. if ($this->securewindow_required(false)) { return $this->get_securewindow_object()->make_review_link($linktext, $attempt->id); } else { return '' . $linktext . ''; } } } /** * 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 $_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($quiz, $timenow) { $this->_quiz = $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->_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 = 0; if ($numprevattempts == 1 && $this->_quiz->delay1) { $nextstarttime = $lastattempt->timefinish + $this->_quiz->delay1; } else if ($numprevattempts > 1 && $this->_quiz->delay2) { $nextstarttime = $lastattempt->timefinish + $this->_quiz->delay2; } 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; } public function is_finished($numprevattempts, $lastattempt) { $nextstarttime = 0; if ($numprevattempts == 1 && $this->_quiz->delay1) { $nextstarttime = $lastattempt->timefinish + $this->_quiz->delay1; } else if ($numprevattempts > 1 && $this->_quiz->delay2) { $nextstarttime = $lastattempt->timefinish + $this->_quiz->delay2; } 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 $return if true, return the HTML for the form (if required), instead of outputting * it at stopping * @return mixed return null, unless $return is true, and a form needs to be displayed. */ public function do_password_check($return = false) { global $CFG, $SESSION; /// 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)) { redirect($CFG->wwwroot . '/mod/quiz/view.php?q=' . $this->_quiz->id); } /// If they entered the right password, let them in. $enteredpassword = optional_param('quizpassword', '', PARAM_RAW); if (strcmp($this->_quiz->password, $enteredpassword) === 0) { $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 (!$return) { print_header('', '', '', 'quizpassword'); } if (trim(strip_tags($this->_quiz->intro))) { $formatoptions->noclean = true; $output .= print_box(format_text($this->_quiz->intro, FORMAT_MOODLE, $formatoptions), 'generalbox', 'intro', true); } $output .= print_box_start('generalbox', 'passwordbox', true); /// 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"; /// Finish page. $output .= print_box_end(true); /// return or display form. if ($return) { return $output; } else { echo $output; print_footer('empty'); 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 * 60)); } public function time_left($attempt, $timenow) { return $attempt->timestart + $this->_quiz->timelimit*60 - $timenow; } } /** * A rule implementing the ipaddress check against the ->submet setting. */ class securewindow_access_rule extends quiz_access_rule_base { private $windowoptions = "left=0, top=0, height='+window.screen.height+', width='+window.screen.width+', channelmode=yes, fullscreen=yes, scrollbars=yes, resizeable=no, directories=no, toolbar=no, titlebar=no, location=no, status=no, menubar=no"; /** * Output the start attempt button. * * @param string $buttontext the desired button caption. * @param string $cmid the quiz cmid. * @param string $strconfirmstartattempt optional message to diplay in a JavaScript altert * before the button submits. */ public function print_start_attempt_button($buttontext, $cmid, $strconfirmstartattempt) { global $CFG; $attempturl = $CFG->wwwroot . '/mod/quiz/attempt.php?id=' . $cmid; $window = 'quizpopup'; if (!empty($CFG->usesid) && !isset($_COOKIE[session_name()])) { $attempturl = sid_process_url($attempturl); } echo 'windowoptions');", '" />'; } /** * 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 $CFG; return link_to_popup_window($CFG->wwwroot . '/mod/quiz/review.php?q=' . $this->_quiz->id . '&attempt=' . $attemptid, 'quizpopup', $linktext, '', '', '', $windowoptions, true); } } ?>