MDL-33140 quiz browser security: block copy/paste more
[moodle.git] / mod / quiz / module.js
1 // This file is part of Moodle - http://moodle.org/
2 //
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16 /**
17  * JavaScript library for the quiz module.
18  *
19  * @package    mod
20  * @subpackage quiz
21  * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 M.mod_quiz = M.mod_quiz || {};
27 M.mod_quiz.init_attempt_form = function(Y) {
28     M.core_question_engine.init_form(Y, '#responseform');
29     Y.on('submit', M.mod_quiz.timer.stop, '#responseform');
30 };
32 M.mod_quiz.init_review_form = function(Y) {
33     M.core_question_engine.init_form(Y, '.questionflagsaveform');
34     Y.on('submit', function(e) { e.halt(); }, '.questionflagsaveform');
35 };
37 M.mod_quiz.init_comment_popup = function(Y) {
38     // Add a close button to the window.
39     var closebutton = Y.Node.create('<input type="button" />');
40     closebutton.set('value', M.util.get_string('cancel', 'moodle'));
41     Y.one('#id_submitbutton').ancestor().append(closebutton);
42     Y.on('click', function() { window.close() }, closebutton);
43 }
45 // Code for updating the countdown timer that is used on timed quizzes.
46 M.mod_quiz.timer = {
47     // YUI object.
48     Y: null,
50     // Timestamp at which time runs out, according to the student's computer's clock.
51     endtime: 0,
53     // This records the id of the timeout that updates the clock periodically,
54     // so we can cancel.
55     timeoutid: null,
57     /**
58      * @param Y the YUI object
59      * @param timeleft, the time remaining, in seconds.
60      */
61     init: function(Y, timeleft) {
62         M.mod_quiz.timer.Y = Y;
63         M.mod_quiz.timer.endtime = new Date().getTime() + timeleft*1000;
64         M.mod_quiz.timer.update();
65         Y.one('#quiz-timer').setStyle('display', 'block');
66     },
68     /**
69      * Stop the timer, if it is running.
70      */
71     stop: function(e) {
72         if (M.mod_quiz.timer.timeoutid) {
73             clearTimeout(M.mod_quiz.timer.timeoutid);
74         }
75     },
77     /**
78      * Function to convert a number between 0 and 99 to a two-digit string.
79      */
80     two_digit: function(num) {
81         if (num < 10) {
82             return '0' + num;
83         } else {
84             return num;
85         }
86     },
88     // Function to update the clock with the current time left, and submit the quiz if necessary.
89     update: function() {
90         var Y = M.mod_quiz.timer.Y;
91         var secondsleft = Math.floor((M.mod_quiz.timer.endtime - new Date().getTime())/1000);
93         // If time has expired, Set the hidden form field that says time has expired.
94         if (secondsleft < 0) {
95             M.mod_quiz.timer.stop(null);
96             Y.one('#quiz-time-left').setContent(M.str.quiz.timesup);
97             var input = Y.one('input[name=timeup]');
98             input.set('value', 1);
99             var form = input.ancestor('form');
100             if (form.one('input[name=finishattempt]')) {
101                 form.one('input[name=finishattempt]').set('value', 0);
102             }
103             form.submit();
104             return;
105         }
107         // If time has nearly expired, change the colour.
108         if (secondsleft < 100) {
109             Y.one('#quiz-timer').removeClass('timeleft' + (secondsleft + 2))
110                     .removeClass('timeleft' + (secondsleft + 1))
111                     .addClass('timeleft' + secondsleft);
112         }
114         // Update the time display.
115         var hours = Math.floor(secondsleft/3600);
116         secondsleft -= hours*3600;
117         var minutes = Math.floor(secondsleft/60);
118         secondsleft -= minutes*60;
119         var seconds = secondsleft;
120         Y.one('#quiz-time-left').setContent(hours + ':' +
121                 M.mod_quiz.timer.two_digit(minutes) + ':' +
122                 M.mod_quiz.timer.two_digit(seconds));
124         // Arrange for this method to be called again soon.
125         M.mod_quiz.timer.timeoutid = setTimeout(M.mod_quiz.timer.update, 100);
126     }
127 };
129 M.mod_quiz.nav = M.mod_quiz.nav || {};
131 M.mod_quiz.nav.update_flag_state = function(attemptid, questionid, newstate) {
132     var Y = M.mod_quiz.nav.Y;
133     var navlink = Y.one('#quiznavbutton' + questionid);
134     navlink.removeClass('flagged');
135     if (newstate == 1) {
136         navlink.addClass('flagged');
137         navlink.one('.accesshide .flagstate').setContent(M.str.question.flagged);
138     } else {
139         navlink.one('.accesshide .flagstate').setContent('');
140     }
141 };
143 M.mod_quiz.nav.init = function(Y) {
144     M.mod_quiz.nav.Y = Y;
146     Y.all('#quiznojswarning').remove();
148     var form = Y.one('#responseform');
149     if (form) {
150         function find_enabled_submit() {
151             // This is rather inelegant, but the CSS3 selector
152             //     return form.one('input[type=submit]:enabled');
153             // does not work in IE7, 8 or 9 for me.
154             var enabledsubmit = null;
155             form.all('input[type=submit]').each(function(submit) {
156                 if (!enabledsubmit && !submit.get('disabled')) {
157                     enabledsubmit = submit;
158                 }
159             });
160             return enabledsubmit;
161         }
163         function nav_to_page(pageno) {
164             Y.one('#followingpage').set('value', pageno);
166             // Automatically submit the form. We do it this strange way because just
167             // calling form.submit() does not run the form's submit event handlers.
168             var submit = find_enabled_submit();
169             submit.set('name', '');
170             submit.getDOMNode().click();
171         };
173         Y.delegate('click', function(e) {
174             if (this.hasClass('thispage')) {
175                 return;
176             }
178             e.preventDefault();
180             var pageidmatch = this.get('href').match(/page=(\d+)/);
181             var pageno;
182             if (pageidmatch) {
183                 pageno = pageidmatch[1];
184             } else {
185                 pageno = 0;
186             }
188             var questionidmatch = this.get('href').match(/#q(\d+)/);
189             if (questionidmatch) {
190                 form.set('action', form.get('action') + '#q' + questionidmatch[1]);
191             }
193             nav_to_page(pageno);
194         }, document.body, '.qnbutton');
195     }
197     if (Y.one('a.endtestlink')) {
198         Y.on('click', function(e) {
199             e.preventDefault();
200             nav_to_page(-1);
201         }, 'a.endtestlink');
202     }
204     if (M.core_question_flags) {
205         M.core_question_flags.add_listener(M.mod_quiz.nav.update_flag_state);
206     }
207 };
209 M.mod_quiz.secure_window = {
210     init: function(Y) {
211         if (window.location.href.substring(0, 4) == 'file') {
212             window.location = 'about:blank';
213         }
214         Y.delegate('contextmenu', M.mod_quiz.secure_window.prevent, document, '*');
215         Y.delegate('mousedown',   M.mod_quiz.secure_window.prevent_mouse, document, '*');
216         Y.delegate('mouseup',     M.mod_quiz.secure_window.prevent_mouse, document, '*');
217         Y.delegate('dragstart',   M.mod_quiz.secure_window.prevent, document, '*');
218         Y.delegate('selectstart', M.mod_quiz.secure_window.prevent, document, '*');
219         Y.delegate('cut',         M.mod_quiz.secure_window.prevent, document, '*');
220         Y.delegate('copy',        M.mod_quiz.secure_window.prevent, document, '*');
221         Y.delegate('paste',       M.mod_quiz.secure_window.prevent, document, '*');
222         M.mod_quiz.secure_window.clear_status;
223         Y.on('beforeprint', function() {
224             Y.one(document.body).setStyle('display', 'none');
225         }, window);
226         Y.on('afterprint', function() {
227             Y.one(document.body).setStyle('display', 'block');
228         }, window);
229         Y.on('key', M.mod_quiz.secure_window.prevent, '*', 'press:67,86,88+ctrl');
230         Y.on('key', M.mod_quiz.secure_window.prevent, '*', 'up:67,86,88+ctrl');
231         Y.on('key', M.mod_quiz.secure_window.prevent, '*', 'down:67,86,88+ctrl');
232         Y.on('key', M.mod_quiz.secure_window.prevent, '*', 'press:67,86,88+meta');
233         Y.on('key', M.mod_quiz.secure_window.prevent, '*', 'up:67,86,88+meta');
234         Y.on('key', M.mod_quiz.secure_window.prevent, '*', 'down:67,86,88+meta');
235     },
237     clear_status: function() {
238         window.status = '';
239         setTimeout(M.mod_quiz.secure_window.clear_status, 10);
240     },
242     prevent: function(e) {
243         alert(M.str.quiz.functiondisabledbysecuremode);
244         e.halt();
245     },
247     prevent_mouse: function(e) {
248         if (e.button == 1 && /^(INPUT|TEXTAREA|BUTTON|SELECT|LABEL|A)$/i.test(e.target.get('tagName'))) {
249             // Left click on a button or similar. No worries.
250             return;
251         }
252         e.halt();
253     },
255     /**
256      * Event handler for the quiz start attempt button.
257      */
258     start_attempt_action: function(e, args) {
259         if (args.startattemptwarning == '') {
260             openpopup(e, args);
261         } else {
262             M.util.show_confirm_dialog(e, {
263                 message: args.startattemptwarning,
264                 callback: function() {
265                     openpopup(e, args);
266                 },
267                 continuelabel: M.util.get_string('startattempt', 'quiz')
268             });
269         }
270     },
272     init_close_button: function(Y, url) {
273         Y.on('click', function(e) {
274             M.mod_quiz.secure_window.close(url, 0)
275         }, '#secureclosebutton');
276     },
278     close: function(url, delay) {
279         setTimeout(function() {
280             if (window.opener) {
281                 window.opener.document.location.reload();
282                 window.close();
283             } else {
284                 window.location.href = url;
285             }
286         }, delay*1000);
287     }
288 };