Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * Entity model representing quiz settings for the seb plugin.
19
 *
20
 * @package    quizaccess_seb
21
 * @author     Andrew Madden <andrewmadden@catalyst-au.net>
22
 * @copyright  2019 Catalyst IT
23
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24
 */
25
 
26
namespace quizaccess_seb;
27
 
28
use CFPropertyList\CFArray;
29
use CFPropertyList\CFBoolean;
30
use CFPropertyList\CFDictionary;
31
use CFPropertyList\CFNumber;
32
use CFPropertyList\CFString;
33
use core\persistent;
34
use lang_string;
35
use moodle_exception;
36
use moodle_url;
37
 
38
defined('MOODLE_INTERNAL') || die();
39
 
40
/**
41
 * Entity model representing quiz settings for the seb plugin.
42
 *
43
 * @copyright  2020 Catalyst IT
44
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
45
 */
46
class seb_quiz_settings extends persistent {
47
 
48
    /** Table name for the persistent. */
49
    const TABLE = 'quizaccess_seb_quizsettings';
50
 
51
    /** @var property_list $plist The SEB config represented as a Property List object. */
52
    private $plist;
53
 
54
    /** @var string $config The SEB config represented as a string. */
55
    private $config;
56
 
57
    /** @var string $configkey The SEB config key represented as a string. */
58
    private $configkey;
59
 
60
 
61
    /**
62
     * Return the definition of the properties of this model.
63
     *
64
     * @return array
65
     */
66
    protected static function define_properties(): array {
67
        return [
68
            'quizid' => [
69
                'type' => PARAM_INT,
70
            ],
71
            'cmid' => [
72
                'type' => PARAM_INT,
73
            ],
74
            'templateid' => [
75
                'type' => PARAM_INT,
76
                'default' => 0,
77
            ],
78
            'requiresafeexambrowser' => [
79
                'type' => PARAM_INT,
80
                'default' => 0,
81
            ],
82
            'showsebtaskbar' => [
83
                'type' => PARAM_INT,
84
                'default' => 1,
85
                'null' => NULL_ALLOWED,
86
            ],
87
            'showwificontrol' => [
88
                'type' => PARAM_INT,
89
                'default' => 0,
90
                'null' => NULL_ALLOWED,
91
            ],
92
            'showreloadbutton' => [
93
                'type' => PARAM_INT,
94
                'default' => 1,
95
                'null' => NULL_ALLOWED,
96
            ],
97
            'showtime' => [
98
                'type' => PARAM_INT,
99
                'default' => 1,
100
                'null' => NULL_ALLOWED,
101
            ],
102
            'showkeyboardlayout' => [
103
                'type' => PARAM_INT,
104
                'default' => 1,
105
                'null' => NULL_ALLOWED,
106
            ],
107
            'allowuserquitseb' => [
108
                'type' => PARAM_INT,
109
                'default' => 1,
110
                'null' => NULL_ALLOWED,
111
            ],
112
            'quitpassword' => [
113
                'type' => PARAM_TEXT,
114
                'default' => '',
115
                'null' => NULL_ALLOWED,
116
            ],
117
            'linkquitseb' => [
118
                'type' => PARAM_URL,
119
                'default' => '',
120
                'null' => NULL_ALLOWED,
121
            ],
122
            'userconfirmquit' => [
123
                'type' => PARAM_INT,
124
                'default' => 1,
125
                'null' => NULL_ALLOWED,
126
            ],
127
            'enableaudiocontrol' => [
128
                'type' => PARAM_INT,
129
                'default' => 0,
130
                'null' => NULL_ALLOWED,
131
            ],
132
            'muteonstartup' => [
133
                'type' => PARAM_INT,
134
                'default' => 0,
135
                'null' => NULL_ALLOWED,
136
            ],
1441 ariadna 137
            'allowcapturecamera' => [
138
                'type' => PARAM_INT,
139
                'default' => 0,
140
                'null' => NULL_ALLOWED,
141
            ],
142
            'allowcapturemicrophone' => [
143
                'type' => PARAM_INT,
144
                'default' => 0,
145
                'null' => NULL_ALLOWED,
146
            ],
1 efrain 147
            'allowspellchecking' => [
148
                'type' => PARAM_INT,
149
                'default' => 0,
150
                'null' => NULL_ALLOWED,
151
            ],
152
            'allowreloadinexam' => [
153
                'type' => PARAM_INT,
154
                'default' => 1,
155
                'null' => NULL_ALLOWED,
156
            ],
157
            'activateurlfiltering' => [
158
                'type' => PARAM_INT,
159
                'default' => 0,
160
                'null' => NULL_ALLOWED,
161
            ],
162
            'filterembeddedcontent' => [
163
                'type' => PARAM_INT,
164
                'default' => 0,
165
                'null' => NULL_ALLOWED,
166
            ],
167
            'expressionsallowed' => [
168
                'type' => PARAM_TEXT,
169
                'default' => '',
170
                'null' => NULL_ALLOWED,
171
            ],
172
            'regexallowed' => [
173
                'type' => PARAM_TEXT,
174
                'default' => '',
175
                'null' => NULL_ALLOWED,
176
            ],
177
            'expressionsblocked' => [
178
                'type' => PARAM_TEXT,
179
                'default' => '',
180
                'null' => NULL_ALLOWED,
181
            ],
182
            'regexblocked' => [
183
                'type' => PARAM_TEXT,
184
                'default' => '',
185
                'null' => NULL_ALLOWED,
186
            ],
187
            'showsebdownloadlink' => [
188
                'type' => PARAM_INT,
189
                'default' => 1,
190
                'null' => NULL_ALLOWED,
191
            ],
192
            'allowedbrowserexamkeys' => [
193
                'type' => PARAM_TEXT,
194
                'default' => '',
195
                'null' => NULL_ALLOWED,
196
            ],
197
        ];
198
    }
199
 
200
    /**
201
     * Return an instance by quiz id.
202
     *
203
     * This method gets data from cache before doing any DB calls.
204
     *
205
     * @param int $quizid Quiz id.
206
     * @return false|\quizaccess_seb\seb_quiz_settings
207
     */
208
    public static function get_by_quiz_id(int $quizid) {
209
        if ($data = self::get_quiz_settings_cache()->get($quizid)) {
210
            return new static(0, $data);
211
        }
212
 
213
        return self::get_record(['quizid' => $quizid]);
214
    }
215
 
216
    /**
217
     * Return cached SEB config represented as a string by quiz ID.
218
     *
219
     * @param int $quizid Quiz id.
220
     * @return string|null
221
     */
222
    public static function get_config_by_quiz_id(int $quizid): ?string {
223
        $config = self::get_config_cache()->get($quizid);
224
 
225
        if ($config !== false) {
226
            return $config;
227
        }
228
 
229
        $config = null;
230
        if ($settings = self::get_by_quiz_id($quizid)) {
231
            $config = $settings->get_config();
232
            self::get_config_cache()->set($quizid, $config);
233
        }
234
 
235
        return $config;
236
    }
237
 
238
    /**
239
     * Return cached SEB config key by quiz ID.
240
     *
241
     * @param int $quizid Quiz id.
242
     * @return string|null
243
     */
244
    public static function get_config_key_by_quiz_id(int $quizid): ?string {
245
        $configkey = self::get_config_key_cache()->get($quizid);
246
 
247
        if ($configkey !== false) {
248
            return $configkey;
249
        }
250
 
251
        $configkey = null;
252
        if ($settings = self::get_by_quiz_id($quizid)) {
253
            $configkey = $settings->get_config_key();
254
            self::get_config_key_cache()->set($quizid, $configkey);
255
        }
256
 
257
        return $configkey;
258
    }
259
 
260
    /**
261
     * Return SEB config key cache instance.
262
     *
263
     * @return \cache_application
264
     */
265
    private static function get_config_key_cache(): \cache_application {
266
        return \cache::make('quizaccess_seb', 'configkey');
267
    }
268
 
269
    /**
270
     * Return SEB config cache instance.
271
     *
272
     * @return \cache_application
273
     */
274
    private static function get_config_cache(): \cache_application {
275
        return \cache::make('quizaccess_seb', 'config');
276
    }
277
 
278
    /**
279
     * Return quiz settings cache object,
280
     *
281
     * @return \cache_application
282
     */
283
    private static function get_quiz_settings_cache(): \cache_application {
284
        return \cache::make('quizaccess_seb', 'quizsettings');
285
    }
286
 
287
    /**
288
     * Adds the new record to the cache.
289
     */
290
    protected function after_create() {
291
        $this->after_save();
292
    }
293
 
294
    /**
295
     * Updates the cache record.
296
     *
297
     * @param bool $result
298
     */
299
    protected function after_update($result) {
300
        $this->after_save();
301
    }
302
 
303
    /**
304
     * Helper method to execute common stuff after create and update.
305
     */
306
    private function after_save() {
307
        self::get_quiz_settings_cache()->set($this->get('quizid'), $this->to_record());
308
        self::get_config_cache()->set($this->get('quizid'), $this->config);
309
        self::get_config_key_cache()->set($this->get('quizid'), $this->configkey);
310
    }
311
 
312
    /**
313
     * Removes unnecessary stuff from db.
314
     */
315
    protected function before_delete() {
316
        $key = $this->get('quizid');
317
        self::get_quiz_settings_cache()->delete($key);
318
        self::get_config_cache()->delete($key);
319
        self::get_config_key_cache()->delete($key);
320
    }
321
 
322
    /**
323
     * Validate the browser exam keys string.
324
     *
325
     * @param string $keys Newline separated browser exam keys.
326
     * @return true|lang_string If there is an error, an error string is returned.
327
     */
328
    protected function validate_allowedbrowserexamkeys($keys) {
329
        $keys = $this->split_keys($keys);
330
        foreach ($keys as $i => $key) {
331
            if (!preg_match('~^[a-f0-9]{64}$~', $key)) {
332
                return new lang_string('allowedbrowserkeyssyntax', 'quizaccess_seb');
333
            }
334
        }
335
        if (count($keys) != count(array_unique($keys))) {
336
            return new lang_string('allowedbrowserkeysdistinct', 'quizaccess_seb');
337
        }
338
        return true;
339
    }
340
 
341
    /**
342
     * Get the browser exam keys as a pre-split array instead of just as a string.
343
     *
344
     * @return array
345
     */
346
    protected function get_allowedbrowserexamkeys(): array {
347
        $keysstring = $this->raw_get('allowedbrowserexamkeys');
348
        $keysstring = empty($keysstring) ? '' : $keysstring;
349
        return $this->split_keys($keysstring);
350
    }
351
 
352
    /**
353
     * Hook to execute before an update.
354
     *
355
     * Please note that at this stage the data has already been validated and therefore
356
     * any new data being set will not be validated before it is sent to the database.
357
     */
358
    protected function before_update() {
359
        $this->before_save();
360
    }
361
 
362
    /**
363
     * Hook to execute before a create.
364
     *
365
     * Please note that at this stage the data has already been validated and therefore
366
     * any new data being set will not be validated before it is sent to the database.
367
     */
368
    protected function before_create() {
369
        $this->before_save();
370
    }
371
 
372
    /**
373
     * As there is no hook for before both create and update, this function is called by both hooks.
374
     */
375
    private function before_save() {
376
        // Set template to 0 if using anything different to template.
377
        if ($this->get('requiresafeexambrowser') != settings_provider::USE_SEB_TEMPLATE) {
378
            $this->set('templateid', 0);
379
        }
380
 
381
        // Process configs to make sure that all data is set correctly.
382
        $this->process_configs();
383
    }
384
 
385
    /**
386
     * Before validate hook.
387
     */
388
    protected function before_validate() {
389
        // Template can't be null.
390
        if (is_null($this->raw_get('templateid'))) {
391
            $this->set('templateid', 0);
392
        }
393
    }
394
 
395
    /**
396
     * Create or update the config string based on the current quiz settings.
397
     */
398
    private function process_configs() {
399
        switch ($this->get('requiresafeexambrowser')) {
400
            case settings_provider::USE_SEB_NO:
401
                $this->process_seb_config_no();
402
                break;
403
 
404
            case settings_provider::USE_SEB_CONFIG_MANUALLY:
405
                $this->process_seb_config_manually();
406
                break;
407
 
408
            case settings_provider::USE_SEB_TEMPLATE:
409
                $this->process_seb_template();
410
                break;
411
 
412
            case settings_provider::USE_SEB_UPLOAD_CONFIG:
413
                $this->process_seb_upload_config();
414
                break;
415
 
416
            default: // Also settings_provider::USE_SEB_CLIENT_CONFIG.
417
                $this->process_seb_client_config();
418
        }
419
 
420
        // Generate config key based on given SEB config.
421
        if (!empty($this->config)) {
422
            $this->configkey = config_key::generate($this->config)->get_hash();
423
        } else {
424
            $this->configkey = null;
425
        }
426
    }
427
 
428
    /**
429
     * Return SEB config key.
430
     *
431
     * @return string|null
432
     */
433
    public function get_config_key(): ?string {
434
        $this->process_configs();
435
 
436
        return $this->configkey;
437
    }
438
 
439
    /**
440
     * Return string representation of the config.
441
     *
442
     * @return string|null
443
     */
444
    public function get_config(): ?string {
445
        $this->process_configs();
446
 
447
        return $this->config;
448
    }
449
 
450
    /**
451
     * Case for USE_SEB_NO.
452
     */
453
    private function process_seb_config_no() {
454
        $this->config = null;
455
    }
456
 
457
    /**
458
     * Case for USE_SEB_CONFIG_MANUALLY. This creates a plist and applies all settings from the posted form, along with
459
     * some defaults.
460
     */
461
    private function process_seb_config_manually() {
462
        // If at any point a configuration file has been uploaded and parsed, clear the settings.
463
        $this->plist = new property_list();
464
 
465
        $this->process_bool_settings();
466
        $this->process_quit_password_settings();
467
        $this->process_quit_url_from_settings();
468
        $this->process_url_filters();
469
        $this->process_required_enforced_settings();
470
 
471
        // One of the requirements for USE_SEB_CONFIG_MANUALLY is setting examSessionClearCookiesOnStart to false.
472
        $this->plist->set_or_update_value('examSessionClearCookiesOnStart', new CFBoolean(false));
473
        $this->plist->set_or_update_value('allowPreferencesWindow', new CFBoolean(false));
474
        $this->config = $this->plist->to_xml();
475
    }
476
 
477
    /**
478
     * Case for USE_SEB_TEMPLATE. This creates a plist from the template uploaded, then applies the quit password
479
     * setting and some defaults.
480
     */
481
    private function process_seb_template() {
482
        $template = template::get_record(['id' => $this->get('templateid')]);
483
        $this->plist = new property_list($template->get('content'));
484
 
485
        $this->process_bool_setting('allowuserquitseb');
486
        $this->process_quit_password_settings();
487
        $this->process_quit_url_from_template_or_config();
488
        $this->process_required_enforced_settings();
489
 
490
        $this->config = $this->plist->to_xml();
491
    }
492
 
493
    /**
494
     * Case for USE_SEB_UPLOAD_CONFIG. This creates a plist from an uploaded configuration file, then applies the quiz
495
     * password settings and some defaults.
496
     */
497
    private function process_seb_upload_config() {
498
        $file = settings_provider::get_module_context_sebconfig_file($this->get('cmid'));
499
 
500
        // If there was no file, create an empty plist so the rest of this wont explode.
501
        if (empty($file)) {
502
            throw new moodle_exception('noconfigfilefound', 'quizaccess_seb', '', $this->get('cmid'));
503
        } else {
504
            $this->plist = new property_list($file->get_content());
505
        }
506
 
507
        $this->process_quit_url_from_template_or_config();
508
        $this->process_required_enforced_settings();
509
 
510
        $this->config = $this->plist->to_xml();
511
    }
512
 
513
    /**
514
     * Case for USE_SEB_CLIENT_CONFIG. This creates an empty plist to remove the config stored.
515
     */
516
    private function process_seb_client_config() {
517
        $this->config = null;
518
    }
519
 
520
    /**
521
     * Sets or updates some sensible default settings, these are the items 'startURL' and 'sendBrowserExamKey'.
522
     */
523
    private function process_required_enforced_settings() {
524
        global $CFG;
525
 
526
        $quizurl = new moodle_url($CFG->wwwroot . "/mod/quiz/view.php", ['id' => $this->get('cmid')]);
527
        $this->plist->set_or_update_value('startURL', new CFString($quizurl->out(true)));
528
        $this->plist->set_or_update_value('sendBrowserExamKey', new CFBoolean(true));
529
 
530
        // Use the modern WebView and JS API if the SEB version supports it.
531
        // Documentation: https://safeexambrowser.org/developer/seb-config-key.html .
532
        // "Set the key browserWindowWebView to the policy "Prefer Modern" (value 3)".
533
        $this->plist->set_or_update_value('browserWindowWebView', new CFNumber(3));
534
    }
535
 
536
    /**
537
     * Use the boolean map to add Moodle boolean setting to config PList.
538
     */
539
    private function process_bool_settings() {
540
        $settings = $this->to_record();
541
        $map = $this->get_bool_seb_setting_map();
542
        foreach ($settings as $setting => $value) {
543
            if (isset($map[$setting])) {
544
                $this->process_bool_setting($setting);
545
            }
546
        }
547
    }
548
 
549
    /**
550
     * Process provided single bool setting.
551
     *
552
     * @param string $name Setting name matching one from self::get_bool_seb_setting_map.
553
     */
554
    private function process_bool_setting(string $name) {
555
        $map = $this->get_bool_seb_setting_map();
556
 
557
        if (!isset($map[$name])) {
558
            throw new \coding_exception('Provided setting name can not be found in known bool settings');
559
        }
560
 
561
        $enabled = $this->raw_get($name) == 1 ? true : false;
562
        $this->plist->set_or_update_value($map[$name], new CFBoolean($enabled));
563
    }
564
 
565
    /**
566
     * Turn hashed quit password and quit link into PList strings and add to config PList.
567
     */
568
    private function process_quit_password_settings() {
569
        $settings = $this->to_record();
570
        if (!empty($settings->quitpassword) && is_string($settings->quitpassword)) {
571
            // Hash quit password.
572
            $hashedpassword = hash('SHA256', $settings->quitpassword);
573
            $this->plist->add_element_to_root('hashedQuitPassword', new CFString($hashedpassword));
574
        } else if (!is_null($this->plist->get_element_value('hashedQuitPassword'))) {
575
            $this->plist->delete_element('hashedQuitPassword');
576
        }
577
    }
578
 
579
    /**
580
     * Sets the quitURL if found in the seb_quiz_settings.
581
     */
582
    private function process_quit_url_from_settings() {
583
        $settings = $this->to_record();
584
        if (!empty($settings->linkquitseb) && is_string($settings->linkquitseb)) {
585
            $this->plist->set_or_update_value('quitURL', new CFString($settings->linkquitseb));
586
        }
587
    }
588
 
589
    /**
590
     * Sets the quiz_setting's linkquitseb if a quitURL value was found in a template or uploaded config.
591
     */
592
    private function process_quit_url_from_template_or_config() {
593
        // Does the plist (template or config file) have an existing quitURL?
594
        $quiturl = $this->plist->get_element_value('quitURL');
595
        if (!empty($quiturl)) {
596
            $this->set('linkquitseb', $quiturl);
597
        }
598
    }
599
 
600
    /**
601
     * Turn return separated strings for URL filters into a PList array and add to config PList.
602
     */
603
    private function process_url_filters() {
604
        $settings = $this->to_record();
605
        // Create rules to each expression provided and add to config.
606
        $urlfilterrules = [];
607
        // Get all rules separated by newlines and remove empty rules.
608
        $expallowed = array_filter(explode(PHP_EOL, $settings->expressionsallowed));
609
        $expblocked = array_filter(explode(PHP_EOL, $settings->expressionsblocked));
610
        $regallowed = array_filter(explode(PHP_EOL, $settings->regexallowed));
611
        $regblocked = array_filter(explode(PHP_EOL, $settings->regexblocked));
612
        foreach ($expallowed as $rulestring) {
613
            $urlfilterrules[] = $this->create_filter_rule($rulestring, true, false);
614
        }
615
        foreach ($expblocked as $rulestring) {
616
            $urlfilterrules[] = $this->create_filter_rule($rulestring, false, false);
617
        }
618
        foreach ($regallowed as $rulestring) {
619
            $urlfilterrules[] = $this->create_filter_rule($rulestring, true, true);
620
        }
621
        foreach ($regblocked as $rulestring) {
622
            $urlfilterrules[] = $this->create_filter_rule($rulestring, false, true);
623
        }
624
        $this->plist->add_element_to_root('URLFilterRules', new CFArray($urlfilterrules));
625
    }
626
 
627
    /**
628
     * Create a CFDictionary represeting a URL filter rule.
629
     *
630
     * @param string $rulestring The expression to filter with.
631
     * @param bool $allowed Allowed or blocked.
632
     * @param bool $isregex Regex or simple.
633
     * @return CFDictionary A PList dictionary.
634
     */
635
    private function create_filter_rule(string $rulestring, bool $allowed, bool $isregex): CFDictionary {
636
        $action = $allowed ? 1 : 0;
637
        return new CFDictionary([
638
                    'action' => new CFNumber($action),
639
                    'active' => new CFBoolean(true),
640
                    'expression' => new CFString(trim($rulestring)),
641
                    'regex' => new CFBoolean($isregex),
642
                    ]);
643
    }
644
 
645
    /**
646
     * Map the settings that are booleans to the Safe Exam Browser config keys.
647
     *
648
     * @return array Moodle setting as key, SEB setting as value.
649
     */
650
    private function get_bool_seb_setting_map(): array {
651
        return [
652
            'activateurlfiltering' => 'URLFilterEnable',
653
            'allowspellchecking' => 'allowSpellCheck',
654
            'allowreloadinexam' => 'browserWindowAllowReload',
655
            'allowuserquitseb' => 'allowQuit',
656
            'enableaudiocontrol' => 'audioControlEnabled',
1441 ariadna 657
            'allowcapturecamera' => 'browserMediaCaptureCamera',
658
            'allowcapturemicrophone' => 'browserMediaCaptureMicrophone',
1 efrain 659
            'filterembeddedcontent' => 'URLFilterEnableContentFilter',
660
            'muteonstartup' => 'audioMute',
661
            'showkeyboardlayout' => 'showInputLanguage',
662
            'showreloadbutton' => 'showReloadButton',
663
            'showsebtaskbar' => 'showTaskBar',
664
            'showtime' => 'showTime',
665
            'showwificontrol' => 'allowWlan',
666
            'userconfirmquit' => 'quitURLConfirm',
667
        ];
668
    }
669
 
670
    /**
671
     * This helper method takes list of browser exam keys in a string and splits it into an array of separate keys.
672
     *
673
     * @param string|null $keys the allowed keys.
674
     * @return array of string, the separate keys.
675
     */
676
    private function split_keys($keys): array {
677
        $keys = preg_split('~[ \t\n\r,;]+~', $keys ?? '', -1, PREG_SPLIT_NO_EMPTY);
678
        foreach ($keys as $i => $key) {
679
            $keys[$i] = strtolower($key);
680
        }
681
        return $keys;
682
    }
683
}