ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç PK\cq,db/services.phpnu[. /** * List web services and external functions for plugin. * * @package quizaccess_seb * @author Andrew Madden * @copyright 2021 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $functions = [ 'quizaccess_seb_validate_quiz_keys' => [ 'classname' => 'quizaccess_seb\external\validate_quiz_keys', 'methodname' => 'execute', 'description' => 'Validate a Safe Exam Browser config key or a browser exam key.', 'type' => 'read', 'ajax' => true, ], ]; PK\YFdb/install.phpnu[. /** * Install script for plugin. * * @package quizaccess_seb * @author Andrew Madden * @copyright 2019 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/quiz/accessrule/seb/lib.php'); /** * Custom code to be run on installing the plugin. */ function xmldb_quizaccess_seb_install() { global $DB; // Reconfigure all existing quizzes to use a new quizaccess_seb. $params = ['browsersecurity' => 'safebrowser']; $total = $DB->count_records('quiz', $params); if ($total > 0) { $rs = $DB->get_recordset('quiz', $params); $i = 0; $pbar = new progress_bar('updatequizrecords', 500, true); foreach ($rs as $quiz) { if (!$DB->record_exists('quizaccess_seb_quizsettings', ['quizid' => $quiz->id])) { $cm = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course); $sebsettings = new stdClass(); $sebsettings->quizid = $quiz->id; $sebsettings->cmid = $cm->id; $sebsettings->templateid = 0; $sebsettings->requiresafeexambrowser = \quizaccess_seb\settings_provider::USE_SEB_CLIENT_CONFIG; $sebsettings->showsebtaskbar = null; $sebsettings->showwificontrol = null; $sebsettings->showreloadbutton = null; $sebsettings->showtime = null; $sebsettings->showkeyboardlayout = null; $sebsettings->allowuserquitseb = null; $sebsettings->quitpassword = null; $sebsettings->linkquitseb = null; $sebsettings->userconfirmquit = null; $sebsettings->enableaudiocontrol = null; $sebsettings->muteonstartup = null; $sebsettings->allowcapturecamera = null; $sebsettings->allowcapturemicrophone = null; $sebsettings->allowspellchecking = null; $sebsettings->allowreloadinexam = null; $sebsettings->activateurlfiltering = null; $sebsettings->filterembeddedcontent = null; $sebsettings->expressionsallowed = null; $sebsettings->regexallowed = null; $sebsettings->expressionsblocked = null; $sebsettings->regexblocked = null; $sebsettings->allowedbrowserexamkeys = null; $sebsettings->showsebdownloadlink = 1; $sebsettings->usermodified = get_admin()->id; $sebsettings->timecreated = time(); $sebsettings->timemodified = time(); $DB->insert_record('quizaccess_seb_quizsettings', $sebsettings); $quiz->browsersecurity = '-'; $DB->update_record('quiz', $quiz); } $i++; $pbar->update($i, $total, "Reconfiguring existing quizzes to use a new SEB plugin - $i/$total."); } $rs->close(); } return true; } PK\X'"'" db/access.phpnu[. /** * Define capabilities for plugin. * * @package quizaccess_seb * @author Andrew Madden * @copyright 2019 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = [ 'quizaccess/seb:managetemplates' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_SYSTEM, 'archetypes' => [ 'manager' => CAP_ALLOW, ] ], 'quizaccess/seb:bypassseb' => [ 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW ] ], 'quizaccess/seb:manage_seb_requiresafeexambrowser' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW ] ], // Ability to select "Yes – Configure manually" as an option for "Require the use of Safe Exam Browser". 'quizaccess/seb:manage_seb_configuremanually' => [ 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, ], 'clonepermissionsfrom' => 'quizaccess/seb:manage_seb_requiresafeexambrowser', ], // Ability to select "Yes – Use SEB client config" as an option for "Require the use of Safe Exam Browser". 'quizaccess/seb:manage_seb_usesebclientconfig' => [ 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, ], 'clonepermissionsfrom' => 'quizaccess/seb:manage_seb_requiresafeexambrowser', ], 'quizaccess/seb:manage_seb_templateid' => [ 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW ] ], 'quizaccess/seb:manage_filemanager_sebconfigfile' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW ] ], 'quizaccess/seb:manage_seb_showsebdownloadlink' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW ] ], 'quizaccess/seb:manage_seb_allowedbrowserexamkeys' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW ] ], 'quizaccess/seb:manage_seb_linkquitseb' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW ] ], 'quizaccess/seb:manage_seb_userconfirmquit' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW ] ], 'quizaccess/seb:manage_seb_allowuserquitseb' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW ] ], 'quizaccess/seb:manage_seb_quitpassword' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW ] ], 'quizaccess/seb:manage_seb_allowreloadinexam' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW ] ], 'quizaccess/seb:manage_seb_showsebtaskbar' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW ] ], 'quizaccess/seb:manage_seb_showreloadbutton' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW ] ], 'quizaccess/seb:manage_seb_showtime' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW ] ], 'quizaccess/seb:manage_seb_showkeyboardlayout' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW ] ], 'quizaccess/seb:manage_seb_showwificontrol' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW ] ], 'quizaccess/seb:manage_seb_enableaudiocontrol' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW ] ], 'quizaccess/seb:manage_seb_muteonstartup' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW ] ], 'quizaccess/seb:manage_seb_allowcapturecamera' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, ], ], 'quizaccess/seb:manage_seb_allowcapturemicrophone' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, ], ], 'quizaccess/seb:manage_seb_allowspellchecking' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW ] ], 'quizaccess/seb:manage_seb_activateurlfiltering' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW ] ], 'quizaccess/seb:manage_seb_filterembeddedcontent' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW ] ], 'quizaccess/seb:manage_seb_expressionsallowed' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW ] ], 'quizaccess/seb:manage_seb_regexallowed' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW ] ], 'quizaccess/seb:manage_seb_expressionsblocked' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW ] ], 'quizaccess/seb:manage_seb_regexblocked' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW ] ], ]; PK\Yg' ' db/upgrade.phpnu[. /** * Upgrade script for plugin. * * @package quizaccess_seb * @author Andrew Madden * @copyright 2019 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/quiz/accessrule/seb/lib.php'); /** * Function to upgrade quizaccess_seb plugin. * * @param int $oldversion The version we are upgrading from. * @return bool Result. */ function xmldb_quizaccess_seb_upgrade($oldversion) { global $DB; $dbman = $DB->get_manager(); // Automatically generated Moodle v4.2.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.3.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.4.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.5.0 release upgrade line. // Put any upgrade step following this. if ($oldversion < 2024121801) { // Define field allowcapturecamera to be added to quizaccess_seb_quizsettings. $table = new xmldb_table('quizaccess_seb_quizsettings'); $field = new xmldb_field('allowcapturecamera', XMLDB_TYPE_INTEGER, '1', null, null, null, null, 'muteonstartup'); // Conditionally launch add field allowcapturecamera. if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } // Define field allowcapturemicrophone to be added to quizaccess_seb_quizsettings. $field = new xmldb_field('allowcapturemicrophone', XMLDB_TYPE_INTEGER, '1', null, null, null, null, 'allowcapturecamera'); // Conditionally launch add field allowcapturemicrophone. if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } // Seb savepoint reached. upgrade_plugin_savepoint(true, 2024121801, 'quizaccess', 'seb'); } // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. return true; } PK\yәdb/install.xmlnu[
PK\wo P db/caches.phpnu[. /** * Plugin cache definitions. * * @package quizaccess_seb * @author Dmitrii Metelkin * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $definitions = [ 'quizsettings' => [ 'mode' => cache_store::MODE_APPLICATION, 'simplekeys' => true, 'simpledata' => true, 'staticacceleration' => true, ], 'config' => [ 'mode' => cache_store::MODE_APPLICATION, 'simplekeys' => true, 'simpledata' => true, 'staticacceleration' => true, ], 'configkey' => [ 'mode' => cache_store::MODE_APPLICATION, 'simplekeys' => true, 'simpledata' => true, 'staticacceleration' => true, ], ]; PK\MJJlang/en/quizaccess_seb.phpnu[. /** * Strings for the quizaccess_seb plugin. * * @package quizaccess_seb * @author Luca Bösch * @author Andrew Madden * @author Dmitrii Metelkin * @copyright 2019 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $string['addtemplate'] = 'Add new template'; $string['allowedbrowserkeysdistinct'] = 'The keys must all be different.'; $string['allowedbrowserkeyssyntax'] = 'A key should be a 64-character hex string.'; $string['cachedef_config'] = 'SEB config cache'; $string['cachedef_configkey'] = 'SEB config key cache'; $string['cachedef_quizsettings'] = 'SEB quiz settings cache'; $string['cantdelete'] = 'The template can\'t be deleted as it has been used for one or more quizzes.'; $string['cantedit'] = 'The template can\'t be edited as it has been used for one or more quizzes.'; $string['checkingaccess'] = 'Checking access to Safe Exam Browser...'; $string['clientrequiresseb'] = 'This quiz has been configured to use the Safe Exam Browser with client configuration.'; $string['confirmtemplateremovalquestion'] = 'Are you sure you want to remove this template?'; $string['confirmtemplateremovaltitle'] = 'Confirm template removal?'; $string['conflictingsettings'] = 'You don\'t have permission to update existing Safe Exam Browser settings.'; $string['content'] = 'Template'; $string['description'] = 'Description'; $string['disabledsettings'] = 'Disabled settings.'; $string['disabledsettings_help'] = 'Safe Exam Browser quiz settings can\'t be changed if the quiz has been attempted. To change a setting, all quiz attempts must first be deleted.'; $string['downloadsebconfig'] = 'Download SEB config file'; $string['duplicatetemplate'] = 'A template with the same name already exists.'; $string['edittemplate'] = 'Edit template'; $string['enabled'] = 'Enabled'; $string['error:ws:nokeyprovided'] = 'At least one Safe Exam Browser key must be provided.'; $string['error:ws:quiznotexists'] = 'Quiz not found matching course module ID: {$a}'; $string['event:accessprevented'] = "Quiz access was prevented"; $string['event:templatecreated'] = 'SEB template was created'; $string['event:templatedeleted'] = 'SEB template was deleted'; $string['event:templatedisabled'] = 'SEB template was disabled'; $string['event:templateenabled'] = 'SEB template was enabled'; $string['event:templateupdated'] = 'SEB template was updated'; $string['exitsebbutton'] = 'Exit Safe Exam Browser'; $string['filemanager_sebconfigfile'] = 'Upload Safe Exam Browser config file'; $string['filemanager_sebconfigfile_help'] = 'Please upload your own Safe Exam Browser config file for this quiz.'; $string['filenotpresent'] = 'Please upload a SEB config file.'; $string['fileparsefailed'] = 'The uploaded file could not be saved as a SEB config file.'; $string['httplinkbutton'] = 'Download configuration'; $string['invalid_browser_key'] = "Invalid SEB browser key"; $string['invalid_config_key'] = "Invalid SEB config key"; $string['invalidkeys'] = 'The Safe Exam Browser keys could not be validated. Check that you\'re using Safe Exam Browser with the correct configuration file.'; $string['invalidtemplate'] = "Invalid SEB config template"; $string['manage_templates'] = 'Safe Exam Browser templates'; $string['managetemplates'] = 'Manage templates'; $string['missingrequiredsettings'] = 'Config settings are missing some required values.'; $string['name'] = 'Name'; $string['newtemplate'] = 'New template'; $string['noconfigfilefound'] = 'No uploaded SEB config file could be found for quiz with cmid: {$a}'; $string['noconfigfound'] = 'No SEB config could be found for quiz with cmid: {$a}'; $string['not_seb'] = 'No Safe Exam Browser is being used.'; $string['notemplate'] = 'No template'; $string['passwordnotset'] = 'Current settings require quizzes using the Safe Exam Browser to have a quiz password set.'; $string['pluginname'] = 'Safe Exam Browser access rules'; $string['privacy:metadata:quizaccess_seb_quizsettings'] = 'Safe Exam Browser settings for a quiz. This includes the ID of the last user to create or modify the settings.'; $string['privacy:metadata:quizaccess_seb_quizsettings:quizid'] = 'ID of the quiz the settings exist for.'; $string['privacy:metadata:quizaccess_seb_quizsettings:timecreated'] = 'Unix time that the settings were created.'; $string['privacy:metadata:quizaccess_seb_quizsettings:timemodified'] = 'Unix time that the settings were last modified.'; $string['privacy:metadata:quizaccess_seb_quizsettings:usermodified'] = 'ID of user who last created or modified the settings.'; $string['privacy:metadata:quizaccess_seb_template'] = 'Safe Exam Browser template settings. This includes the ID of the last user to create or modify the template.'; $string['privacy:metadata:quizaccess_seb_template:timecreated'] = 'Unix time that the template was created.'; $string['privacy:metadata:quizaccess_seb_template:timemodified'] = 'Unix time that the template was last modified.'; $string['privacy:metadata:quizaccess_seb_template:usermodified'] = 'ID of user who last created or modified the template.'; $string['quizsettings'] = 'Quiz settings'; $string['restoredfrom'] = '{$a->name} (restored via cmid {$a->cmid})'; $string['seb'] = 'Safe Exam Browser'; $string['seb:bypassseb'] = 'Bypass the requirement to view quiz in Safe Exam Browser.'; $string['seb:manage_filemanager_sebconfigfile'] = 'Change SEB quiz setting: Select SEB config file'; $string['seb:manage_seb_activateurlfiltering'] = 'Change SEB quiz setting: Activate URL filtering'; $string['seb:manage_seb_allowcapturecamera'] = 'Change SEB quiz setting: Allow browser access to camera'; $string['seb:manage_seb_allowcapturemicrophone'] = 'Change SEB quiz setting: Allow browser access to microphone'; $string['seb:manage_seb_allowedbrowserexamkeys'] = 'Change SEB quiz setting: Allowed browser exam keys'; $string['seb:manage_seb_allowreloadinexam'] = 'Change SEB quiz setting: Allow reload'; $string['seb:manage_seb_allowspellchecking'] = 'Change SEB quiz setting: Enable spell checking'; $string['seb:manage_seb_allowuserquitseb'] = 'Change SEB quiz setting: Allow quit'; $string['seb:manage_seb_configuremanually'] = 'Change SEB quiz setting: Select manual configuration'; $string['seb:manage_seb_enableaudiocontrol'] = 'Change SEB quiz setting: Enable audio control'; $string['seb:manage_seb_expressionsallowed'] = 'Change SEB quiz setting: Simple expressions allowed'; $string['seb:manage_seb_expressionsblocked'] = 'Change SEB quiz setting: Simple expressions blocked'; $string['seb:manage_seb_filterembeddedcontent'] = 'Change SEB quiz setting: Filter embedded content'; $string['seb:manage_seb_linkquitseb'] = 'Change SEB quiz setting: Quit link'; $string['seb:manage_seb_muteonstartup'] = 'Change SEB quiz setting: Mute on startup'; $string['seb:manage_seb_quitpassword'] = 'Change SEB quiz setting: Quit password'; $string['seb:manage_seb_regexallowed'] = 'Change SEB quiz setting: Regex expressions allowed'; $string['seb:manage_seb_regexblocked'] = 'Change SEB quiz setting: Regex expressions blocked'; $string['seb:manage_seb_requiresafeexambrowser'] = 'Change SEB quiz setting: Require Safe Exam Browser'; $string['seb:manage_seb_showkeyboardlayout'] = 'Change SEB quiz setting: Show keyboard layout'; $string['seb:manage_seb_showreloadbutton'] = 'Change SEB quiz setting: Show reload button'; $string['seb:manage_seb_showsebtaskbar'] = 'Change SEB quiz setting: Show task bar'; $string['seb:manage_seb_showtime'] = 'Change SEB quiz setting: Show time'; $string['seb:manage_seb_showwificontrol'] = 'Change SEB quiz setting: Show Wi-Fi control'; $string['seb:manage_seb_showsebdownloadlink'] = 'Change SEB quiz setting: Show download link'; $string['seb:manage_seb_templateid'] = 'Change SEB quiz setting: Select SEB template'; $string['seb:manage_seb_userconfirmquit'] = 'Change SEB quiz setting: Confirm on quit'; $string['seb:manage_seb_usesebclientconfig'] = 'Change SEB quiz setting: Use SEB client configuration'; $string['seb:managetemplates'] = 'Manage SEB configuration templates'; $string['seb_activateurlfiltering'] = 'Enable URL filtering'; $string['seb_activateurlfiltering_help'] = 'If enabled, URLs will be filtered when loading web pages. The filter set has to be defined below.'; $string['seb_allowcapturecamera'] = 'Allow browser access to camera'; $string['seb_allowcapturecamera_help'] = 'If enabled, users will be asked to grant access to the camera for SEB before the exam session is started. In macOS 12/iOS 15 or later, disabling this setting prevents web applications from accessing the camera i.e. video recording via WebRTC will not be possible.'; $string['seb_allowcapturemicrophone'] = 'Allow browser access to microphone'; $string['seb_allowcapturemicrophone_help'] = 'If enabled, users will be asked to grant access to the microphone for SEB before the exam session is started. In macOS 12/iOS 15 or later, disabling this setting prevents web applications from accessing the microphone i.e. audio recording via WebRTC will not be possible.'; $string['seb_allowedbrowserexamkeys'] = 'Allowed browser exam keys'; $string['seb_allowedbrowserexamkeys_help'] = 'In this field you can enter the allowed browser exam keys for versions of Safe Exam Browser that are permitted to access this quiz. If no keys are entered, then browser exam keys are not checked.'; $string['seb_allowreloadinexam'] = 'Enable reload in exam'; $string['seb_allowreloadinexam_help'] = 'If enabled, page reload is allowed (reload button in SEB task bar, browser tool bar, iOS side slider menu, keyboard shortcut F5/cmd+R). Note that offline caching may break if a user tries to reload a page without an internet connection.'; $string['seb_allowspellchecking'] = 'Enable spell checking'; $string['seb_allowspellchecking_help'] = 'If enabled, spell checking in the SEB browser is allowed.'; $string['seb_allowuserquitseb'] = 'Enable quitting of SEB'; $string['seb_allowuserquitseb_help'] = 'If enabled, users can quit SEB with the "Quit" button in the SEB task bar or by pressing the keys Ctrl-Q or by clicking the main browser window close button.'; $string['seb_enableaudiocontrol'] = 'Enable audio controls'; $string['seb_enableaudiocontrol_help'] = 'If enabled, the audio control icon is shown in the SEB task bar.'; $string['seb_expressionsallowed'] = 'Expressions allowed'; $string['seb_expressionsallowed_help'] = 'A text field which contains the allowed filtering expressions for the allowed URLs. Use of the wildcard char \'\*\' is possible. Examples for expressions: \'example.com\' or \'example.com/stuff/\*\'. \'example.com\' matches \'example.com\', \'www.example.com\' and \'www.mail.example.com\'. \'example.com/stuff/\*\' matches all requests to any subdomain of \'example.com\' that have \'stuff\' as the first segment of the path.'; $string['seb_expressionsblocked'] = 'Expressions blocked'; $string['seb_expressionsblocked_help'] = 'A text field which contains the filtering expressions for the blocked URLs. Use of the wildcard char \'\*\' is possible. Examples for expressions: \'example.com\' or \'example.com/stuff/\*\'. \'example.com\' matches \'example.com\', \'www.example.com\' and \'www.mail.example.com\'. \'example.com/stuff/\*\' matches all requests to any subdomain of \'example.com\' that have \'stuff\' as the first segment of the path.'; $string['seb_filterembeddedcontent'] = 'Filter also embedded content'; $string['seb_filterembeddedcontent_help'] = 'If enabled, embedded resources will also be filtered using the filter set.'; $string['seb_help'] = 'Setup quiz to use the Safe Exam Browser.'; $string['seb_linkquitseb'] = 'Show Exit Safe Exam Browser button, configured with this quit link'; $string['seb_linkquitseb_help'] = 'In this field you can enter the link to quit SEB. It is used on an "Exit Safe Exam Browser" button on the page that appears after the exam is submitted. When clicking the button or the link placed wherever you want to put it, it is possible to quit SEB without having to enter a quit password. For an HTTPS site, the quit link has to start with https://. If no link is entered, then the "Exit Safe Exam Browser" button does not appear and there is no link set to quit SEB.'; $string['seb_managetemplates'] = 'Manage Safe Exam Browser templates'; $string['seb_muteonstartup'] = 'Mute on startup'; $string['seb_muteonstartup_help'] = 'If enabled, audio is initially muted when starting SEB.'; $string['seb_quitpassword'] = 'Quit password'; $string['seb_quitpassword_help'] = 'This password is prompted when users try to quit SEB with the "Quit" button, Ctrl-Q or the close button in the main browser window. If no quit password is set, then SEB just prompts "Are you sure you want to quit SEB?".'; $string['seb_regexallowed'] = 'Regex allowed'; $string['seb_regexallowed_help'] = 'A text field which contains the filtering expressions for allowed URLs in a regular expression (Regex) format.'; $string['seb_regexblocked'] = 'Regex blocked'; $string['seb_regexblocked_help'] = 'A text field which contains the filtering expressions for blocked URLs in a regular expression (Regex) format.'; $string['seb_requiresafeexambrowser'] = 'Require the use of Safe Exam Browser'; $string['seb_requiresafeexambrowser_help'] = 'If enabled, students can only attempt the quiz using the Safe Exam Browser. The available options are: * No
Safe Exam Browser is not required to attempt the quiz. * Yes – Use an existing template
A template for the configuration of Safe Exam Browser can be used. Templates are managed in the site administration. Your manual settings overwrite the settings in the template. * Yes – Configure manually
No template for the configuration of Safe Exam Browser will be used. You can configure Safe Exam Browser manually. * Yes – Upload my own config
You can upload your own Safe Exam Browser configuration file. All manual settings and the use of templates will be disabled. * Yes – Use SEB client config
No configurations of Safe Exam Browser are on the Moodle side. The quiz can be attempted with any configuration of Safe Exam Browser.'; $string['seb_showkeyboardlayout'] = 'Show keyboard layout'; $string['seb_showkeyboardlayout_help'] = 'If enabled, the current keyboard layout is shown in the SEB task bar. It allows you to switch to other keyboard layouts, which have been enabled in the operating system.'; $string['seb_showreloadbutton'] = 'Show reload button'; $string['seb_showreloadbutton_help'] = 'If enabled, a reload button is displayed in the SEB task bar, allowing the current web page to be reloaded.'; $string['seb_showsebtaskbar'] = 'Show SEB task bar'; $string['seb_showsebtaskbar_help'] = 'If enabled, a task bar appears at the bottom of the SEB browser window. The task bar is required to display items such as Wi-Fi control, reload button, time and keyboard layout.'; $string['seb_showtime'] = 'Show time'; $string['seb_showtime_help'] = 'If enabled, the current time is displayed in the SEB task bar.'; $string['seb_showwificontrol'] = 'Show Wi-Fi control'; $string['seb_showwificontrol_help'] = 'If enabled, a Wi-Fi control button appears in the SEB task bar. The button allows users to reconnect to Wi-Fi networks which have previously been connected to.'; $string['seb_showsebdownloadlink'] = 'Show Safe Exam Browser download button'; $string['seb_showsebdownloadlink_help'] = 'If enabled, a button for Safe Exam Browser download will be shown on the quiz start page.'; $string['seb_templateid'] = 'Safe Exam Browser config template'; $string['seb_templateid_help'] = 'The settings in the selected config template will be used for the configuration of the Safe Exam Browser while attempting the quiz. You may overwrite the settings in the template with your manual settings.'; $string['seb_use_client'] = 'Yes – Use SEB client config'; $string['seb_use_manually'] = 'Yes – Configure manually'; $string['seb_use_template'] = 'Yes – Use an existing template'; $string['seb_use_upload'] = 'Yes – Upload my own config'; $string['seb_userconfirmquit'] = 'Ask user to confirm quitting'; $string['seb_userconfirmquit_help'] = 'If enabled, users have to confirm quitting of SEB when a quit link is detected.'; $string['sebdownloadbutton'] = 'Download Safe Exam Browser'; $string['sebkeysvalidationfailed'] = 'Error validating SEB keys'; $string['seblinkbutton'] = 'Launch Safe Exam Browser'; $string['sebrequired'] = "This quiz has been configured so that students may only attempt it using the Safe Exam Browser."; $string['setting:autoreconfigureseb'] = 'Auto-configure SEB'; $string['setting:autoreconfigureseb_desc'] = 'If enabled, users who navigate to the quiz using the Safe Exam Browser will be automatically forced to reconfigure their Safe Exam Browser.'; $string['setting:displayblocksbeforestart'] = 'Display blocks before starting quiz'; $string['setting:displayblocksbeforestart_desc'] = 'If enabled, blocks will be displayed before a user attempts the quiz.'; $string['setting:displayblockswhenfinished'] = 'Display blocks after finishing quiz'; $string['setting:displayblockswhenfinished_desc'] = 'If enabled, blocks will be displayed after a user has finished their quiz attempt.'; $string['setting:downloadlink'] = 'Safe Exam Browser download link'; $string['setting:downloadlink_desc'] = 'URL for downloading the Safe Exam Browser application.'; $string['setting:quizpasswordrequired'] = 'Quiz password required'; $string['setting:quizpasswordrequired_desc'] = 'If enabled, all quizzes that require the Safe Exam Browser must have a quiz password set.'; $string['setting:showhttplink'] = 'Show http:// link'; $string['setting:showseblink'] = 'Show seb:// link'; $string['setting:showseblinks'] = 'Show Safe Exam Browser config links'; $string['setting:showseblinks_desc'] = 'Whether to show links for a user to access the Safe Exam Browser configuration file when access to the quiz is prevented. Note that seb:// links may not work in every browser.'; $string['setting:supportedversions'] = 'Please note that the following minimum versions of the Safe Exam Browser client are required to use the config key feature: macOS - 2.1.5pre2, Windows - 3.0, iOS - 2.1.14.'; $string['settingsfrozen'] = 'Due to there being at least one quiz attempt, the Safe Exam Browser settings can no longer be updated.'; $string['unknown_reason'] = "Unknown reason"; $string['used'] = 'In use'; PK\k9backup/moodle2/restore_quizaccess_seb_subplugin.class.phpnu[. /** * Restore instructions for the seb (Safe Exam Browser) quiz access subplugin. * * @package quizaccess_seb * @category backup * @author Andrew Madden * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use quizaccess_seb\seb_quiz_settings; use quizaccess_seb\settings_provider; use quizaccess_seb\template; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/quiz/backup/moodle2/restore_mod_quiz_access_subplugin.class.php'); /** * Restore instructions for the seb (Safe Exam Browser) quiz access subplugin. * * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_quizaccess_seb_subplugin extends restore_mod_quiz_access_subplugin { /** * Provides path structure required to restore data for seb quiz access plugin. * * @return array */ protected function define_quiz_subplugin_structure() { $paths = []; // Quiz settings. $path = $this->get_pathfor('/quizaccess_seb_quizsettings'); // Subplugin root path. $paths[] = new restore_path_element('quizaccess_seb_quizsettings', $path); // Template settings. $path = $this->get_pathfor('/quizaccess_seb_quizsettings/quizaccess_seb_template'); $paths[] = new restore_path_element('quizaccess_seb_template', $path); return $paths; } /** * Process the restored data for the quizaccess_seb_quizsettings table. * * @param stdClass $data Data for quizaccess_seb_quizsettings retrieved from backup xml. */ public function process_quizaccess_seb_quizsettings($data) { global $DB, $USER; // Process quizsettings. $data = (object) $data; $data->quizid = $this->get_new_parentid('quiz'); // Update quizid with new reference. $data->cmid = $this->task->get_moduleid(); unset($data->id); $data->timecreated = $data->timemodified = time(); $data->usermodified = $USER->id; // Do not use template if it is no longer enabled. if ($data->requiresafeexambrowser == settings_provider::USE_SEB_TEMPLATE && !$DB->record_exists(template::TABLE, ['id' => $data->templateid, 'enabled' => '1'])) { $data->templateid = 0; $data->requiresafeexambrowser = settings_provider::USE_SEB_NO; } $DB->insert_record(seb_quiz_settings::TABLE, $data); // Process attached files. $this->add_related_files('quizaccess_seb', 'filemanager_sebconfigfile', null); } /** * Process the restored data for the quizaccess_seb_template table. * * @param stdClass $data Data for quizaccess_seb_template retrieved from backup xml. */ public function process_quizaccess_seb_template($data) { global $DB; $data = (object) $data; $quizid = $this->get_new_parentid('quiz'); $template = null; if ($this->task->is_samesite()) { $template = \quizaccess_seb\template::get_record(['id' => $data->id]); } else { // In a different site, try to find existing template with the same name and content. $candidates = \quizaccess_seb\template::get_records(['name' => $data->name]); foreach ($candidates as $candidate) { if ($candidate->get('content') == $data->content) { $template = $candidate; break; } } } if (empty($template)) { unset($data->id); $template = new \quizaccess_seb\template(0, $data); $template->save(); } // Update the restored quiz settings to use restored template. // Check if template is enabled before using it. if ($template->get('enabled')) { $DB->set_field(seb_quiz_settings::TABLE, 'templateid', $template->get('id'), ['quizid' => $quizid]); } } } PK\%Y'8backup/moodle2/backup_quizaccess_seb_subplugin.class.phpnu[. /** * Backup instructions for the seb (Safe Exam Browser) quiz access subplugin. * * @package quizaccess_seb * @category backup * @author Andrew Madden * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/quiz/backup/moodle2/backup_mod_quiz_access_subplugin.class.php'); /** * Backup instructions for the seb (Safe Exam Browser) quiz access subplugin. * * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_quizaccess_seb_subplugin extends backup_mod_quiz_access_subplugin { /** * Stores the data related to the Safe Exam Browser quiz settings and management for a particular quiz. * * @return backup_subplugin_element */ protected function define_quiz_subplugin_structure() { parent::define_quiz_subplugin_structure(); $quizid = backup::VAR_ACTIVITYID; $subplugin = $this->get_subplugin_element(); $subpluginwrapper = new backup_nested_element($this->get_recommended_name()); $template = new \quizaccess_seb\template(); $blanktemplatearray = (array) $template->to_record(); unset($blanktemplatearray['usermodified']); unset($blanktemplatearray['timemodified']); $templatekeys = array_keys($blanktemplatearray); $subplugintemplatesettings = new backup_nested_element('quizaccess_seb_template', null, $templatekeys); // Get quiz settings keys to save. $settings = new \quizaccess_seb\seb_quiz_settings(); $blanksettingsarray = (array) $settings->to_record(); unset($blanksettingsarray['id']); // We don't need to save reference to settings record in current instance. // We don't need to save the data about who last modified the settings as they will be overwritten on restore. Also // means we don't have to think about user data for the backup. unset($blanksettingsarray['usermodified']); unset($blanksettingsarray['timemodified']); $settingskeys = array_keys($blanksettingsarray); // Save the settings. $subpluginquizsettings = new backup_nested_element('quizaccess_seb_quizsettings', null, $settingskeys); // Connect XML elements into the tree. $subplugin->add_child($subpluginwrapper); $subpluginwrapper->add_child($subpluginquizsettings); $subpluginquizsettings->add_child($subplugintemplatesettings); // Set source to populate the settings data by referencing the ID of quiz being backed up. $subpluginquizsettings->set_source_table(quizaccess_seb\seb_quiz_settings::TABLE, ['quizid' => $quizid]); $subpluginquizsettings->annotate_files('quizaccess_seb', 'filemanager_sebconfigfile', null); $params = ['id' => '../templateid']; $subplugintemplatesettings->set_source_table(\quizaccess_seb\template::TABLE, $params); return $subplugin; } } PK\4 config.phpnu[. /** * Serves an encrypted/unencrypted string as a file for download. * * @package quizaccess_seb * @author Andrew Madden * @copyright 2019 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../../../../config.php'); $cmid = required_param('cmid', PARAM_RAW); $config = \quizaccess_seb\helper::get_seb_config_content($cmid); \quizaccess_seb\helper::send_seb_config_file($config); PK\*#{M M lib.phpnu[. /** * Main library of plugin. * * @package quizaccess_seb * @author Andrew Madden * @author Dmitrii Metelkin * @copyright 2019 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Serve the files. * * @param stdClass $course the course object * @param stdClass $cm the course module object * @param \context $context the context * @param string $filearea the name of the file area * @param array $args extra arguments (itemid, path) * @param bool $forcedownload whether or not force download * @param array $options additional options affecting the file serving * @return bool false if the file not found, just send the file otherwise and do not return anything */ function quizaccess_seb_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options= []) { if ($context->contextlevel != CONTEXT_MODULE) { return false; } if ($filearea !== 'filemanager_sebconfigfile') { return false; } require_login($course, true, $cm); if (!has_capability('quizaccess/seb:manage_filemanager_sebconfigfile', $context)) { return false; } $itemid = array_shift($args); $filename = array_pop($args); if (!$args) { $filepath = '/'; } else { $filepath = '/' .implode('/', $args) . '/'; } $fs = get_file_storage(); $file = $fs->get_file($context->id, 'quizaccess_seb', $filearea, $itemid, $filepath, $filename); if (!$file) { return false; } send_stored_file($file, 0, 0, $forcedownload, $options); } PK\y+#gg version.phpnu[. /** * Version information for the quizaccess_seb plugin. * * @package quizaccess_seb * @author Andrew Madden * @copyright 2019 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; $plugin->requires = 2025040800; $plugin->component = 'quizaccess_seb'; $plugin->maturity = MATURITY_STABLE; PK\{i'classes/external/validate_quiz_keys.phpnu[. namespace quizaccess_seb\external; defined('MOODLE_INTERNAL') || die(); global $CFG; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_single_structure; use core_external\external_value; use invalid_parameter_exception; use mod_quiz\quiz_settings; use quizaccess_seb\event\access_prevented; use quizaccess_seb\seb_access_manager; /** * Validate browser exam key and config key. * * @package quizaccess_seb * @author Andrew Madden * @copyright 2021 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class validate_quiz_keys extends external_api { /** * External function parameters. * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters([ 'cmid' => new external_value(PARAM_INT, 'Course module ID', VALUE_REQUIRED, null, NULL_NOT_ALLOWED), 'url' => new external_value(PARAM_URL, 'Page URL to check', VALUE_REQUIRED, null, NULL_NOT_ALLOWED), 'configkey' => new external_value(PARAM_ALPHANUMEXT, 'SEB config key', VALUE_DEFAULT, null), 'browserexamkey' => new external_value(PARAM_ALPHANUMEXT, 'SEB browser exam key', VALUE_DEFAULT, null), ]); } /** * Validate a SEB config key or browser exam key. * * @param string $cmid Course module ID. * @param string $url URL of the page on which the SEB JS API generated the keys. * @param string|null $configkey A SEB config key hash. Includes URL in the hash. * @param string|null $browserexamkey A SEB browser exam key hash. Includes the URL in the hash. * @return array */ public static function execute(string $cmid, string $url, ?string $configkey = null, ?string $browserexamkey = null): array { list( 'cmid' => $cmid, 'url' => $url, 'configkey' => $configkey, 'browserexamkey' => $browserexamkey ) = self::validate_parameters(self::execute_parameters(), [ 'cmid' => $cmid, 'url' => $url, 'configkey' => $configkey, 'browserexamkey' => $browserexamkey, ]); self::validate_context(\context_module::instance($cmid)); // At least one SEB key must be provided. if (empty($configkey) && empty($browserexamkey)) { throw new invalid_parameter_exception(get_string('error:ws:nokeyprovided', 'quizaccess_seb')); } // Check quiz exists corresponding to cmid. if (($quizid = self::get_quiz_id($cmid)) === 0) { throw new invalid_parameter_exception(get_string('error:ws:quiznotexists', 'quizaccess_seb', $cmid)); } $result = ['configkey' => true, 'browserexamkey' => true]; $accessmanager = new seb_access_manager(quiz_settings::create($quizid)); // Check if there is a valid config key. if (!$accessmanager->validate_config_key($configkey, $url)) { access_prevented::create_strict($accessmanager, get_string('invalid_config_key', 'quizaccess_seb'), $configkey, $browserexamkey)->trigger(); $result['configkey'] = false; } // Check if there is a valid browser exam key. if (!$accessmanager->validate_browser_exam_key($browserexamkey, $url)) { access_prevented::create_strict($accessmanager, get_string('invalid_browser_key', 'quizaccess_seb'), $configkey, $browserexamkey)->trigger(); $result['browserexamkey'] = false; } if ($result['configkey'] && $result['browserexamkey']) { // Set the state of the access for this Moodle session. $accessmanager->set_session_access(true); } return $result; } /** * External function returns. * * @return external_single_structure */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'configkey' => new external_value(PARAM_BOOL, 'Is a provided config key valid?', VALUE_REQUIRED, 0, NULL_NOT_ALLOWED), 'browserexamkey' => new external_value(PARAM_BOOL, 'Is a provided browser exam key valid?', VALUE_REQUIRED, 0, NULL_NOT_ALLOWED) ]); } /** * Check if there is a valid quiz corresponding to a course module it. * * @param string $cmid Course module ID. * @return int Returns quiz id if cmid matches valid quiz, or 0 if there is no match. */ private static function get_quiz_id(string $cmid): int { $quizid = 0; $coursemodule = get_coursemodule_from_id('quiz', $cmid); if (!empty($coursemodule)) { $quizid = $coursemodule->instance; } return $quizid; } } PK\|4..classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for quizaccess_seb. * * @package quizaccess_seb * @author Andrew Madden * @copyright 2019 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace quizaccess_seb\privacy; use context; use core_privacy\local\metadata\collection; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\approved_userlist; use core_privacy\local\request\contextlist; use core_privacy\local\request\transform; use core_privacy\local\request\userlist; use core_privacy\local\request\writer; use quizaccess_seb\seb_quiz_settings; use quizaccess_seb\template; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem implementation for quizaccess_seb. * * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\plugin\provider { /** * Retrieve the user metadata stored by plugin. * * @param collection $collection Collection of metadata. * @return collection Collection of metadata. */ public static function get_metadata(collection $collection): collection { $collection->add_database_table( 'quizaccess_seb_quizsettings', [ 'quizid' => 'privacy:metadata:quizaccess_seb_quizsettings:quizid', 'usermodified' => 'privacy:metadata:quizaccess_seb_quizsettings:usermodified', 'timecreated' => 'privacy:metadata:quizaccess_seb_quizsettings:timecreated', 'timemodified' => 'privacy:metadata:quizaccess_seb_quizsettings:timemodified', ], 'privacy:metadata:quizaccess_seb_quizsettings' ); $collection->add_database_table( 'quizaccess_seb_template', [ 'usermodified' => 'privacy:metadata:quizaccess_seb_template:usermodified', 'timecreated' => 'privacy:metadata:quizaccess_seb_template:timecreated', 'timemodified' => 'privacy:metadata:quizaccess_seb_template:timemodified', ], 'privacy:metadata:quizaccess_seb_template' ); return $collection; } /** * Get the list of contexts that contain user information for the specified user. * * @param int $userid The user to search. * @return contextlist A list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid): contextlist { $contextlist = new contextlist(); // The data is associated at the module context level, so retrieve the quiz context id. $sql = "SELECT c.id FROM {quizaccess_seb_quizsettings} qs JOIN {course_modules} cm ON cm.instance = qs.quizid JOIN {modules} m ON cm.module = m.id AND m.name = :modulename JOIN {context} c ON c.instanceid = cm.id AND c.contextlevel = :context WHERE qs.usermodified = :userid GROUP BY c.id"; $params = [ 'context' => CONTEXT_MODULE, 'modulename' => 'quiz', 'userid' => $userid ]; $contextlist->add_from_sql($sql, $params); $sql = "SELECT c.id FROM {quizaccess_seb_template} tem JOIN {quizaccess_seb_quizsettings} qs ON qs.templateid = tem.id JOIN {course_modules} cm ON cm.instance = qs.quizid JOIN {modules} m ON cm.module = m.id AND m.name = :modulename JOIN {context} c ON c.instanceid = cm.id AND c.contextlevel = :context WHERE qs.usermodified = :userid GROUP BY c.id"; $contextlist->add_from_sql($sql, $params); return $contextlist; } /** * Export all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist) { global $DB; // Get all cmids that correspond to the contexts for a user. $cmids = []; foreach ($contextlist->get_contexts() as $context) { if ($context->contextlevel === CONTEXT_MODULE) { $cmids[] = $context->instanceid; } } // Do nothing if no matching quiz settings are found for the user. if (empty($cmids)) { return; } list($insql, $params) = $DB->get_in_or_equal($cmids, SQL_PARAMS_NAMED); $params['modulename'] = 'quiz'; // SEB quiz settings. $sql = "SELECT qs.id as id, qs.quizid as quizid, qs.usermodified as usermodified, qs.timecreated as timecreated, qs.timemodified as timemodified FROM {quizaccess_seb_quizsettings} qs JOIN {course_modules} cm ON cm.instance = qs.quizid JOIN {modules} m ON cm.module = m.id AND m.name = :modulename WHERE cm.id {$insql}"; $quizsettingslist = $DB->get_records_sql($sql, $params); $index = 0; foreach ($quizsettingslist as $quizsettings) { // Data export is organised in: {Context}/{Plugin Name}/{Table name}/{index}/data.json. $index++; $subcontext = [ get_string('pluginname', 'quizaccess_seb'), seb_quiz_settings::TABLE, $index ]; $data = (object) [ 'quizid' => $quizsettings->quizid, 'usermodified' => $quizsettings->usermodified, 'timecreated' => transform::datetime($quizsettings->timecreated), 'timemodified' => transform::datetime($quizsettings->timemodified) ]; writer::with_context($context)->export_data($subcontext, $data); } // SEB template settings. $sql = "SELECT tem.id as id, qs.quizid as quizid, tem.usermodified as usermodified, tem.timecreated as timecreated, tem.timemodified as timemodified FROM {quizaccess_seb_template} tem JOIN {quizaccess_seb_quizsettings} qs ON qs.templateid = tem.id JOIN {course_modules} cm ON cm.instance = qs.quizid JOIN {modules} m ON cm.module = m.id AND m.name = :modulename WHERE cm.id {$insql}"; $templatesettingslist = $DB->get_records_sql($sql, $params); $index = 0; foreach ($templatesettingslist as $templatesetting) { // Data export is organised in: {Context}/{Plugin Name}/{Table name}/{index}/data.json. $index++; $subcontext = [ get_string('pluginname', 'quizaccess_seb'), template::TABLE, $index ]; $data = (object) [ 'templateid' => $templatesetting->id, 'quizid' => $templatesetting->quizid, 'usermodified' => $templatesetting->usermodified, 'timecreated' => transform::datetime($templatesetting->timecreated), 'timemodified' => transform::datetime($templatesetting->timemodified) ]; writer::with_context($context)->export_data($subcontext, $data); } } /** * Delete all data for all users in the specified context. * * @param context $context The specific context to delete data for. */ public static function delete_data_for_all_users_in_context(\context $context) { global $DB; // Sanity check that context is at the module context level, then get the quizid. if ($context->contextlevel !== CONTEXT_MODULE) { return; } $cmid = $context->instanceid; $quizid = $DB->get_field('course_modules', 'instance', ['id' => $cmid]); $params['quizid'] = $quizid; $select = "id IN (SELECT templateid FROM {quizaccess_seb_quizsettings} qs WHERE qs.quizid = :quizid)"; $DB->set_field_select('quizaccess_seb_quizsettings', 'usermodified', 0, "quizid = :quizid", $params); $DB->set_field_select('quizaccess_seb_template', 'usermodified', 0, $select, $params); } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { global $DB; // If the user has data, then only the User context should be present so get the first context. $contexts = $contextlist->get_contexts(); if (count($contexts) == 0) { return; } $params['usermodified'] = $contextlist->get_user()->id; $DB->set_field_select('quizaccess_seb_quizsettings', 'usermodified', 0, "usermodified = :usermodified", $params); $DB->set_field_select('quizaccess_seb_template', 'usermodified', 0, "usermodified = :usermodified", $params); } /** * Get the list of users who have data within a context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { $context = $userlist->get_context(); if (!$context instanceof \context_module) { return; } // The data is associated at the quiz module context level, so retrieve the user's context id. $sql = "SELECT qs.usermodified AS userid FROM {quizaccess_seb_quizsettings} qs JOIN {course_modules} cm ON cm.instance = qs.quizid JOIN {modules} m ON cm.module = m.id AND m.name = ? WHERE cm.id = ?"; $params = ['quiz', $context->instanceid]; $userlist->add_from_sql('userid', $sql, $params); } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { global $DB; $context = $userlist->get_context(); // Sanity check that context is at the Module context level. if ($context->contextlevel !== CONTEXT_MODULE) { return; } $userids = $userlist->get_userids(); list($insql, $inparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); $DB->set_field_select('quizaccess_seb_quizsettings', 'usermodified', 0, "usermodified {$insql}", $inparams); $DB->set_field_select('quizaccess_seb_template', 'usermodified', 0, "usermodified {$insql}", $inparams); } } PK\;u@,,classes/local/form/template.phpnu[. /** * Form for manipulating with the template records. * * @package quizaccess_seb * @author Dmitrii Metelkin * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace quizaccess_seb\local\form; defined('MOODLE_INTERNAL') || die(); /** * Form for manipulating with the template records. * * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class template extends \core\form\persistent { /** @var string Persistent class name. */ protected static $persistentclass = 'quizaccess_seb\\template'; /** * Form definition. */ protected function definition() { $mform = $this->_form; $mform->addElement('text', 'name', get_string('name', 'quizaccess_seb')); $mform->addRule('name', get_string('required'), 'required', null, 'client'); $mform->setType('name', PARAM_TEXT); $mform->addElement('textarea', 'description', get_string('description', 'quizaccess_seb')); $mform->setType('description', PARAM_TEXT); if ($this->get_persistent()->get('id')) { $mform->addElement('textarea', 'content', get_string('content', 'quizaccess_seb'), ['rows' => 20, 'cols' => 60]); $mform->addRule('content', get_string('required'), 'required'); } else { $mform->addElement('filepicker', 'content', get_string('content', 'quizaccess_seb')); $mform->addRule('content', get_string('required'), 'required'); } $mform->addElement('selectyesno', 'enabled', get_string('enabled', 'quizaccess_seb')); $mform->setType('enabled', PARAM_INT); if ($this->get_persistent()->get('id')) { $mform->hardFreezeAllVisibleExcept(['enabled']); } $this->add_action_buttons(); } /** * Filter out the foreign fields of the persistent. * * @param \stdClass $data The data to filter the fields out of. * @return \stdClass. */ protected function filter_data_for_persistent($data) { // Uploading a new template file. if (empty($this->get_persistent()->get('id'))) { $files = $this->get_draft_files('content'); if ($files) { $file = reset($files); $data->content = $file->get_content(); } else { // No file found. Remove content data and let persistent to return an error. unset($data->content); } } return parent::filter_data_for_persistent($data); } /** * Extra validation. * * @param \stdClass $data Data to validate. * @param array $files Array of files. * @param array $errors Currently reported errors. * @return array of additional errors, or overridden errors. */ protected function extra_validation($data, $files, array &$errors) { $newerrors = []; // Check name. if (empty($data->name)) { $newerrors['name'] = get_string('namerequired', 'quizaccess_seb'); } return $newerrors; } } PK\M%%classes/local/table/template_list.phpnu[. /** * Templates table. * * @package quizaccess_seb * @author Dmitrii Metelkin * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace quizaccess_seb\local\table; use quizaccess_seb\helper; use quizaccess_seb\template; use quizaccess_seb\template_controller; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/tablelib.php'); /** * Templates table. * * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class template_list extends \flexible_table { /** * @var int Autogenerated id. */ private static $autoid = 0; /** * Constructor * * @param string|null $id to be used by the table, autogenerated if null. */ public function __construct($id = null) { global $PAGE; $id = (is_null($id) ? self::$autoid++ : $id); parent::__construct('quizaccess_seb' . $id); $this->define_baseurl($PAGE->url); $this->set_attribute('class', 'generaltable admintable'); // Column definition. $this->define_columns([ 'name', 'description', 'enabled', 'used', 'actions', ]); $this->define_headers([ get_string('name', 'quizaccess_seb'), get_string('description', 'quizaccess_seb'), get_string('enabled', 'quizaccess_seb'), get_string('used', 'quizaccess_seb'), get_string('actions'), ]); $this->setup(); } /** * Display name column. * * @param \quizaccess_seb\template $data Template for this row. * @return string */ protected function col_name(template $data): string { return \html_writer::link( new \moodle_url(template_controller::get_base_url(), [ 'id' => $data->get('id'), 'action' => template_controller::ACTION_EDIT, ]), $data->get('name') ); } /** * Display description column. * * @param \quizaccess_seb\template $data Template for this row. * @return string */ protected function col_description(template $data): string { return $data->get('description'); } /** * Display enabled column. * * @param \quizaccess_seb\template $data Template for this row. * @return string */ protected function col_enabled(template $data): string { return empty($data->get('enabled')) ? get_string('no') : get_string('yes'); } /** * Display if a template is being used. * * @param \quizaccess_seb\template $data Template for this row. * @return string */ protected function col_used(template $data): string { return $data->can_delete() ? get_string('no') : get_string('yes'); } /** * Display actions column. * * @param \quizaccess_seb\template $data Template for this row. * @return string */ protected function col_actions(template $data): string { $actions = []; $actions[] = helper::format_icon_link( new \moodle_url(template_controller::get_base_url(), [ 'id' => $data->get('id'), 'action' => template_controller::ACTION_EDIT, ]), 't/edit', get_string('edit') ); $actions[] = helper::format_icon_link( new \moodle_url(template_controller::get_base_url(), [ 'id' => $data->get('id'), 'action' => template_controller::ACTION_DELETE, 'sesskey' => sesskey(), ]), 't/delete', get_string('delete'), null, [ 'data-action' => 'delete', 'data-id' => $data->get('id'), ] ); return implode(' ', $actions); } /** * Sets the data of the table. * * @param \quizaccess_seb\template[] $records An array with records. */ public function display(array $records) { foreach ($records as $record) { $this->add_data_keyed($this->format_row($record)); } $this->finish_output(); } } PK\aD+(+(classes/template_controller.phpnu[. /** * Class for manipulating with the template records. * * @package quizaccess_seb * @author Dmitrii Metelkin * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace quizaccess_seb; use core\notification; use quizaccess_seb\local\table\template_list; defined('MOODLE_INTERNAL') || die(); /** * Class for manipulating with the template records. * * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class template_controller { /** * View action. */ const ACTION_VIEW = 'view'; /** * Add action. */ const ACTION_ADD = 'add'; /** * Edit action. */ const ACTION_EDIT = 'edit'; /** * Delete action. */ const ACTION_DELETE = 'delete'; /** * Hide action. */ const ACTION_HIDE = 'hide'; /** * Show action. */ const ACTION_SHOW = 'show'; /** * Locally cached $OUTPUT object. * @var \bootstrap_renderer */ protected $output; /** * region_manager constructor. */ public function __construct() { global $OUTPUT; $this->output = $OUTPUT; } /** * Execute required action. * * @param string $action Action to execute. */ public function execute($action) { $this->set_external_page(); switch($action) { case self::ACTION_ADD: case self::ACTION_EDIT: $this->edit($action, optional_param('id', null, PARAM_INT)); break; case self::ACTION_DELETE: $this->delete(required_param('id', PARAM_INT)); break; case self::ACTION_HIDE: $this->hide(required_param('id', PARAM_INT)); break; case self::ACTION_SHOW: $this->show(required_param('id', PARAM_INT)); break; case self::ACTION_VIEW: default: $this->view(); break; } } /** * Set external page for the manager. */ protected function set_external_page() { admin_externalpage_setup('quizaccess_seb/template'); } /** * Return record instance. * * @param int $id * @param \stdClass|null $data * * @return \quizaccess_seb\template */ protected function get_instance($id = 0, ?\stdClass $data = null) { return new template($id, $data); } /** * Print out all records in a table. */ protected function display_all_records() { $records = template::get_records([], 'id'); $table = new template_list(); $table->display($records); } /** * Returns a text for create new record button. * @return string */ protected function get_create_button_text(): string { return get_string('addtemplate', 'quizaccess_seb'); } /** * Returns form for the record. * * @param \quizaccess_seb\template|null $instance * * @return \quizaccess_seb\local\form\template */ protected function get_form($instance): \quizaccess_seb\local\form\template { global $PAGE; return new \quizaccess_seb\local\form\template($PAGE->url->out(false), ['persistent' => $instance]); } /** * View page heading string. * @return string */ protected function get_view_heading(): string { return get_string('managetemplates', 'quizaccess_seb'); } /** * New record heading string. * @return string */ protected function get_new_heading(): string { return get_string('newtemplate', 'quizaccess_seb'); } /** * Edit record heading string. * @return string */ protected function get_edit_heading(): string { return get_string('edittemplate', 'quizaccess_seb'); } /** * Returns base URL for the manager. * @return string */ public static function get_base_url(): string { return '/mod/quiz/accessrule/seb/template.php'; } /** * Execute edit action. * * @param string $action Could be edit or create. * @param null|int $id Id of the region or null if creating a new one. */ protected function edit($action, $id = null) { global $PAGE; $PAGE->set_url(new \moodle_url(static::get_base_url(), ['action' => $action, 'id' => $id])); $instance = null; if ($id) { $instance = $this->get_instance($id); } $form = $this->get_form($instance); if ($form->is_cancelled()) { redirect(new \moodle_url(static::get_base_url())); } else if ($data = $form->get_data()) { unset($data->submitbutton); try { if (empty($data->id)) { $data->content = $form->get_file_content('content'); $persistent = $this->get_instance(0, $data); $persistent->create(); \quizaccess_seb\event\template_created::create_strict( $persistent, \context_system::instance() )->trigger(); $this->trigger_enabled_event($persistent); } else { $instance->from_record($data); $instance->update(); \quizaccess_seb\event\template_updated::create_strict( $instance, \context_system::instance() )->trigger(); $this->trigger_enabled_event($instance); } notification::success(get_string('changessaved')); } catch (\Exception $e) { notification::error($e->getMessage()); } redirect(new \moodle_url(static::get_base_url())); } else { if (empty($instance)) { $this->header($this->get_new_heading()); } else { if (!$instance->can_delete()) { notification::warning(get_string('cantedit', 'quizaccess_seb')); } $this->header($this->get_edit_heading()); } } $form->display(); $this->footer(); } /** * Execute delete action. * * @param int $id ID of the region. */ protected function delete($id) { require_sesskey(); $instance = $this->get_instance($id); if ($instance->can_delete()) { $instance->delete(); notification::success(get_string('deleted')); \quizaccess_seb\event\template_deleted::create_strict( $id, \context_system::instance() )->trigger(); redirect(new \moodle_url(static::get_base_url())); } else { notification::warning(get_string('cantdelete', 'quizaccess_seb')); redirect(new \moodle_url(static::get_base_url())); } } /** * Execute view action. */ protected function view() { global $PAGE; $this->header($this->get_view_heading()); $this->print_add_button(); $this->display_all_records(); // JS for Template management. $PAGE->requires->js_call_amd('quizaccess_seb/managetemplates', 'setup'); $this->footer(); } /** * Show the template. * * @param int $id The ID of the template to show. */ protected function show(int $id) { $this->show_hide($id, 1); } /** * Hide the template. * * @param int $id The ID of the template to hide. */ protected function hide($id) { $this->show_hide($id, 0); } /** * Show or Hide the template. * * @param int $id The ID of the template to hide. * @param int $visibility The intended visibility. */ protected function show_hide(int $id, int $visibility) { require_sesskey(); $template = $this->get_instance($id); $template->set('enabled', $visibility); $template->save(); $this->trigger_enabled_event($template); redirect(new \moodle_url(self::get_base_url())); } /** * Print out add button. */ protected function print_add_button() { echo $this->output->single_button( new \moodle_url(static::get_base_url(), ['action' => self::ACTION_ADD]), $this->get_create_button_text() ); } /** * Print out page header. * @param string $title Title to display. */ protected function header($title) { echo $this->output->header(); echo $this->output->heading($title); } /** * Print out the page footer. * * @return void */ protected function footer() { echo $this->output->footer(); } /** * Helper function to fire off an event that informs of if a template is enabled or not. * * @param template $template The template persistent object. */ private function trigger_enabled_event(template $template) { $eventstring = ($template->get('enabled') == 0 ? 'disabled' : 'enabled'); $func = '\quizaccess_seb\event\template_' . $eventstring; $func::create_strict( $template, \context_system::instance() )->trigger(); } }PK\d classes/hideif_rule.phpnu[. /** * Class to store data for "hide if" rules for the settings form. * * @package quizaccess_seb * @author Dmitrii Metelkin * @copyright 2019 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace quizaccess_seb; defined('MOODLE_INTERNAL') || die(); /** * Class to store data for "hide if" rules for the settings form. * * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class hideif_rule { /** * Name of the element to hide. * @var string */ protected $element; /** * Name of the element that $element is dependant on. * @var string */ protected $dependantname; /** * Condition. E.g. 'eq', 'noteq' and etc. * @var string */ protected $condition; /** * Value to check the $condition against. * @var string */ protected $dependantvalue; /** * Constructor. * * @param string $element Name of the element to hide. * @param string $dependantname Name of the element that $element is dependant on. * @param string $condition Condition. E.g. 'eq', 'noteq' and etc. * @param string $dependantvalue Value to check the $condition against. */ public function __construct(string $element, string $dependantname, string $condition, string $dependantvalue) { $this->element = $element; $this->dependantname = $dependantname; $this->condition = $condition; $this->dependantvalue = $dependantvalue; } /** * Return name of the element to hide. * @return string */ public function get_element(): string { return $this->element; } /** * Returns name of the element that $element is dependant on. * @return string */ public function get_dependantname(): string { return $this->dependantname; } /** * Returns condition. E.g. 'eq', 'noteq' and etc * @return string */ public function get_condition(): string { return $this->condition; } /** * Returns value to check the $condition against. * @return string */ public function get_dependantvalue(): string { return $this->dependantvalue; } } PK\nb(WWclasses/helper.phpnu[. /** * Helper class. * * @package quizaccess_seb * @author Dmitrii Metelkin * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace quizaccess_seb; use CFPropertyList\CFPropertyList; defined('MOODLE_INTERNAL') || die(); /** * Helper class. * * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class helper { /** * Get a filler icon for display in the actions column of a table. * * @param string $url The URL for the icon. * @param string $icon The icon identifier. * @param string $alt The alt text for the icon. * @param string $iconcomponent The icon component. * @param array $options Display options. * @return string */ public static function format_icon_link($url, $icon, $alt, $iconcomponent = 'moodle', $options = []) { global $OUTPUT; return $OUTPUT->action_icon( $url, new \pix_icon($icon, $alt, $iconcomponent, [ 'title' => $alt, ]), null, $options ); } /** * Validate seb config string. * * @param string $sebconfig * @return bool */ public static function is_valid_seb_config(string $sebconfig): bool { $result = true; set_error_handler(function($errno, $errstr, $errfile, $errline ){ throw new \ErrorException($errstr, $errno, 0, $errfile, $errline); }); $plist = new CFPropertyList(); try { $plist->parse($sebconfig); } catch (\ErrorException $e) { $result = false; } catch (\Exception $e) { $result = false; } restore_error_handler(); return $result; } /** * A helper function to get a list of seb config file headers. * * @param int|null $expiretime Unix timestamp * @return array */ public static function get_seb_file_headers(?int $expiretime = null): array { if (is_null($expiretime)) { $expiretime = time(); } $headers = []; $headers[] = 'Cache-Control: private, max-age=1, no-transform'; $headers[] = 'Expires: '. gmdate('D, d M Y H:i:s', $expiretime) .' GMT'; $headers[] = 'Pragma: no-cache'; $headers[] = 'Content-Disposition: attachment; filename=config.seb'; $headers[] = 'Content-Type: application/seb'; return $headers; } /** * Get seb config content for a particular quiz. This method checks caps. * * @param string $cmid The course module ID for a quiz with config. * @return string SEB config string. */ public static function get_seb_config_content(string $cmid): string { // Try and get the course module. $cm = get_coursemodule_from_id('quiz', $cmid, 0, false, MUST_EXIST); // Make sure the user is logged in and has access to the module. require_login($cm->course, false, $cm); // Retrieve the config for quiz. $config = seb_quiz_settings::get_config_by_quiz_id($cm->instance); if (empty($config)) { throw new \moodle_exception('noconfigfound', 'quizaccess_seb', '', $cm->id); } return $config; } /** * Serve a file to browser for download. * * @param string $contents Contents of file. */ public static function send_seb_config_file(string $contents) { // We can now send the file back to the browser. foreach (self::get_seb_file_headers() as $header) { header($header); } echo($contents); } } PK\FTjccclasses/link_generator.phpnu[. /** * Generate the links to open/download the Safe Exam Browser with correct settings. * * @package quizaccess_seb * @author Andrew Madden * @copyright 2019 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace quizaccess_seb; use moodle_url; defined('MOODLE_INTERNAL') || die(); /** * Generate the links to open/download the Safe Exam Browser with correct settings. * * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class link_generator { /** * Get a link to force the download of the file over https or sebs protocols. * * @param string $cmid Course module ID. * @param bool $seb Whether to use a seb:// scheme or fall back to http:// scheme. * @param bool $secure Whether to use HTTPS or HTTP protocol. * @return string A URL. */ public static function get_link(string $cmid, bool $seb = false, bool $secure = true): string { // Check if course module exists. get_coursemodule_from_id('quiz', $cmid, 0, false, MUST_EXIST); $url = new moodle_url('/mod/quiz/accessrule/seb/config.php?cmid=' . $cmid); if ($seb) { $secure ? $url->set_scheme('sebs') : $url->set_scheme('seb'); } else { $secure ? $url->set_scheme('https') : $url->set_scheme('http'); } return $url->out(); } } PK\8VN N classes/config_key.phpnu[. /** * Class for generating and representing a Safe Exam Browser config key. * * @package quizaccess_seb * @author Andrew Madden * @copyright 2019 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace quizaccess_seb; defined('MOODLE_INTERNAL') || die(); /** * Class for generating and representing a Safe Exam Browser config key. * * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class config_key { /** @var string $hash The Config Key hash. */ private $hash; /** * The config_key constructor. * * @param string $hash The Config Key hash. */ public function __construct(string $hash) { $this->hash = $hash; } /** * Generate the Config Key hash from an SEB Config XML string. * * See https://safeexambrowser.org/developer/seb-config-key.html for more information about the process. * * @param string $xml A PList XML string, representing SEB config. * @return config_key This config key instance. */ public static function generate(string $xml): config_key { if (!empty($xml) && !helper::is_valid_seb_config($xml)) { throw new \invalid_parameter_exception('Invalid a PList XML string, representing SEB config'); } $plist = new property_list($xml); // Remove the key "originatorVersion" first. This key is exempted from the SEB-JSON hash (it's a special key // which doesn't have any functionality, it's just meta data indicating which SEB version saved the config file). $plist->delete_element('originatorVersion'); // Convert the plist XML of a decrypted/unencrypted SEB config file to a ordered JSON-like "SEB-JSON" object. $hash = $plist->to_json(); // Hash the JSON with SHA256. Defaults to required Base16 encoding. $hash = hash('SHA256', $hash); return new self($hash); } /** * Get the Config Key hash. * * @return string The Config Key hash */ public function get_hash(): string { return $this->hash; } } PK\2D``classes/settings_provider.phpnu[. /** * Class for providing quiz settings, to make setting up quiz form manageable. * * To make sure there are no inconsistencies between data sets, run tests in tests/phpunit/settings_provider_test.php. * * @package quizaccess_seb * @author Luca Bösch * @author Andrew Madden * @author Dmitrii Metelkin * @copyright 2019 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace quizaccess_seb; use context_module; use context_user; use lang_string; use stdClass; use stored_file; defined('MOODLE_INTERNAL') || die(); /** * Helper class for providing quiz settings, to make setting up quiz form manageable. * * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class settings_provider { /** * No SEB should be used. */ const USE_SEB_NO = 0; /** * Use SEB and configure it manually. */ const USE_SEB_CONFIG_MANUALLY = 1; /** * Use SEB config from pre configured template. */ const USE_SEB_TEMPLATE = 2; /** * Use SEB config from uploaded config file. */ const USE_SEB_UPLOAD_CONFIG = 3; /** * Use client config. Not SEB config is required. */ const USE_SEB_CLIENT_CONFIG = 4; /** * Insert form element. * * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built. * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm. * @param \HTML_QuickForm_element $element Element to insert. * @param string $before Insert element before. */ protected static function insert_element(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform, \HTML_QuickForm_element $element, $before = 'security') { $mform->insertElementBefore($element, $before); } /** * Remove element from the form. * * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built. * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm. * @param string $elementname Element name. */ protected static function remove_element(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform, string $elementname) { if ($mform->elementExists($elementname)) { $mform->removeElement($elementname); $mform->setDefault($elementname, null); } } /** * Add help button to the element. * * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built. * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm. * @param string $elementname Element name. */ protected static function add_help_button(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform, string $elementname) { if ($mform->elementExists($elementname)) { $mform->addHelpButton($elementname, $elementname, 'quizaccess_seb'); } } /** * Set default value for the element. * * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built. * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm. * @param string $elementname Element name. * @param mixed $value Default value. */ protected static function set_default(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform, string $elementname, $value) { $mform->setDefault($elementname, $value); } /** * Set element type. * * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built. * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm. * @param string $elementname Element name. * @param string $type Type of the form element. */ protected static function set_type(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform, string $elementname, string $type) { $mform->setType($elementname, $type); } /** * Freeze form element. * * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built. * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm. * @param string $elementname Element name. */ protected static function freeze_element(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform, string $elementname) { if ($mform->elementExists($elementname)) { $mform->freeze($elementname); } } /** * Add SEB header element to the form. * * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built. * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm. */ protected static function add_seb_header_element(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) { global $OUTPUT; $element = $mform->createElement('header', 'seb', get_string('seb', 'quizaccess_seb')); self::insert_element($quizform, $mform, $element); // Display notification about locked settings. if (self::is_seb_settings_locked($quizform->get_instance())) { $notify = new \core\output\notification( get_string('settingsfrozen', 'quizaccess_seb'), \core\output\notification::NOTIFY_WARNING ); $notifyelement = $mform->createElement('html', $OUTPUT->render($notify)); self::insert_element($quizform, $mform, $notifyelement); } if (self::is_conflicting_permissions($quizform->get_context())) { $notify = new \core\output\notification( get_string('conflictingsettings', 'quizaccess_seb'), \core\output\notification::NOTIFY_WARNING ); $notifyelement = $mform->createElement('html', $OUTPUT->render($notify)); self::insert_element($quizform, $mform, $notifyelement); } } /** * Add SEB usage element with all available options. * * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built. * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm. */ protected static function add_seb_usage_options(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) { $element = $mform->createElement( 'select', 'seb_requiresafeexambrowser', get_string('seb_requiresafeexambrowser', 'quizaccess_seb'), self::get_requiresafeexambrowser_options($quizform->get_context()) ); self::insert_element($quizform, $mform, $element); self::set_type($quizform, $mform, 'seb_requiresafeexambrowser', PARAM_INT); self::set_default($quizform, $mform, 'seb_requiresafeexambrowser', self::USE_SEB_NO); self::add_help_button($quizform, $mform, 'seb_requiresafeexambrowser'); if (self::is_conflicting_permissions($quizform->get_context())) { self::freeze_element($quizform, $mform, 'seb_requiresafeexambrowser'); } } /** * Add Templates element. * * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built. * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm. */ protected static function add_seb_templates(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) { $context = $quizform->get_context(); if (self::can_use_seb_template($context) || self::is_conflicting_permissions($context)) { $element = $mform->createElement( 'select', 'seb_templateid', get_string('seb_templateid', 'quizaccess_seb'), self::get_template_options($context->instanceid) ); } else { $element = $mform->createElement('hidden', 'seb_templateid'); } self::insert_element($quizform, $mform, $element); self::set_type($quizform, $mform, 'seb_templateid', PARAM_INT); self::set_default($quizform, $mform, 'seb_templateid', 0); self::add_help_button($quizform, $mform, 'seb_templateid'); // In case if the user can't use templates, but the quiz is configured to use them, // we'd like to display template, but freeze it. if (self::is_conflicting_permissions($context)) { self::freeze_element($quizform, $mform, 'seb_templateid'); } } /** * Add upload config file element. * * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built. * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm. */ protected static function add_seb_config_file(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) { $itemid = 0; $draftitemid = 0; file_prepare_draft_area( $draftitemid, $quizform->get_context()->id, 'quizaccess_seb', 'filemanager_sebconfigfile', $itemid ); if (self::can_upload_seb_file($quizform->get_context())) { $element = $mform->createElement( 'filemanager', 'filemanager_sebconfigfile', get_string('filemanager_sebconfigfile', 'quizaccess_seb'), null, self::get_filemanager_options() ); } else { $element = $mform->createElement('hidden', 'filemanager_sebconfigfile'); } self::insert_element($quizform, $mform, $element); self::set_type($quizform, $mform, 'filemanager_sebconfigfile', PARAM_RAW); self::set_default($quizform, $mform, 'filemanager_sebconfigfile', $draftitemid); self::add_help_button($quizform, $mform, 'filemanager_sebconfigfile'); } /** * Add Show Safe Exam Browser download button. * * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built. * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm. */ protected static function add_seb_show_download_link(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) { if (self::can_change_seb_showsebdownloadlink($quizform->get_context())) { $element = $mform->createElement('selectyesno', 'seb_showsebdownloadlink', get_string('seb_showsebdownloadlink', 'quizaccess_seb') ); self::insert_element($quizform, $mform, $element); self::set_type($quizform, $mform, 'seb_showsebdownloadlink', PARAM_BOOL); self::set_default($quizform, $mform, 'seb_showsebdownloadlink', 1); self::add_help_button($quizform, $mform, 'seb_showsebdownloadlink'); } } /** * Add Allowed Browser Exam Keys setting. * * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built. * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm. */ protected static function add_seb_allowedbrowserexamkeys(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) { if (self::can_change_seb_allowedbrowserexamkeys($quizform->get_context())) { $element = $mform->createElement('textarea', 'seb_allowedbrowserexamkeys', get_string('seb_allowedbrowserexamkeys', 'quizaccess_seb') ); self::insert_element($quizform, $mform, $element); self::set_type($quizform, $mform, 'seb_allowedbrowserexamkeys', PARAM_RAW); self::set_default($quizform, $mform, 'seb_allowedbrowserexamkeys', ''); self::add_help_button($quizform, $mform, 'seb_allowedbrowserexamkeys'); } } /** * Add SEB config elements. * * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built. * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm. */ protected static function add_seb_config_elements(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) { $defaults = self::get_seb_config_element_defaults(); $types = self::get_seb_config_element_types(); foreach (self::get_seb_config_elements() as $name => $type) { if (!self::can_manage_seb_config_setting($name, $quizform->get_context())) { $type = 'hidden'; } $element = $mform->createElement($type, $name, get_string($name, 'quizaccess_seb')); self::insert_element($quizform, $mform, $element); unset($element); // We need to make sure each &element only references the current element in loop. self::add_help_button($quizform, $mform, $name); if (isset($defaults[$name])) { self::set_default($quizform, $mform, $name, $defaults[$name]); } if (isset($types[$name])) { self::set_type($quizform, $mform, $name, $types[$name]); } } } /** * Add setting fields. * * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built. * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm. */ public static function add_seb_settings_fields(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) { if (self::can_configure_seb($quizform->get_context())) { self::add_seb_header_element($quizform, $mform); self::add_seb_usage_options($quizform, $mform); self::add_seb_templates($quizform, $mform); self::add_seb_config_file($quizform, $mform); self::add_seb_show_download_link($quizform, $mform); self::add_seb_config_elements($quizform, $mform); self::add_seb_allowedbrowserexamkeys($quizform, $mform); self::hide_seb_elements($quizform, $mform); self::lock_seb_elements($quizform, $mform); } } /** * Hide SEB elements if required. * * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built. * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm. */ protected static function hide_seb_elements(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) { foreach (self::get_quiz_hideifs() as $elname => $rules) { if ($mform->elementExists($elname)) { foreach ($rules as $hideif) { $mform->hideIf( $hideif->get_element(), $hideif->get_dependantname(), $hideif->get_condition(), $hideif->get_dependantvalue() ); } } } } /** * Lock SEB elements if required. * * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built. * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm. */ protected static function lock_seb_elements(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) { if (self::is_seb_settings_locked($quizform->get_instance()) || self::is_conflicting_permissions($quizform->get_context())) { // Freeze common quiz settings. self::freeze_element($quizform, $mform, 'seb_requiresafeexambrowser'); self::freeze_element($quizform, $mform, 'seb_templateid'); self::freeze_element($quizform, $mform, 'seb_showsebdownloadlink'); self::freeze_element($quizform, $mform, 'seb_allowedbrowserexamkeys'); $quizsettings = seb_quiz_settings::get_by_quiz_id((int) $quizform->get_instance()); // If the file has been uploaded, then replace it with the link to download the file. if (!empty($quizsettings) && $quizsettings->get('requiresafeexambrowser') == self::USE_SEB_UPLOAD_CONFIG) { self::remove_element($quizform, $mform, 'filemanager_sebconfigfile'); if ($link = self::get_uploaded_seb_file_download_link($quizform, $mform)) { $element = $mform->createElement( 'static', 'filemanager_sebconfigfile', get_string('filemanager_sebconfigfile', 'quizaccess_seb'), $link ); self::insert_element($quizform, $mform, $element, 'seb_showsebdownloadlink'); } } // Remove template ID if not using template for this quiz. if (empty($quizsettings) || $quizsettings->get('requiresafeexambrowser') != self::USE_SEB_TEMPLATE) { $mform->removeElement('seb_templateid'); } // Freeze all SEB specific settings. foreach (self::get_seb_config_elements() as $element => $type) { self::freeze_element($quizform, $mform, $element); } } } /** * Return uploaded SEB config file link. * * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built. * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm. * @return string */ protected static function get_uploaded_seb_file_download_link(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform): string { $link = ''; $file = self::get_module_context_sebconfig_file($quizform->get_coursemodule()->id); if ($file) { $url = \moodle_url::make_pluginfile_url( $file->get_contextid(), $file->get_component(), $file->get_filearea(), $file->get_itemid(), $file->get_filepath(), $file->get_filename(), true ); $link = \html_writer::link($url, get_string('downloadsebconfig', 'quizaccess_seb')); } return $link; } /** * Get the type of element for each of the form elements in quiz settings. * * Contains all setting elements. Array key is name of 'form element'/'database column (excluding prefix)'. * * @return array All quiz form elements to be added and their types. */ public static function get_seb_config_elements(): array { return [ 'seb_linkquitseb' => 'text', 'seb_userconfirmquit' => 'selectyesno', 'seb_allowuserquitseb' => 'selectyesno', 'seb_quitpassword' => 'passwordunmask', 'seb_allowreloadinexam' => 'selectyesno', 'seb_showsebtaskbar' => 'selectyesno', 'seb_showreloadbutton' => 'selectyesno', 'seb_showtime' => 'selectyesno', 'seb_showkeyboardlayout' => 'selectyesno', 'seb_showwificontrol' => 'selectyesno', 'seb_enableaudiocontrol' => 'selectyesno', 'seb_muteonstartup' => 'selectyesno', 'seb_allowcapturecamera' => 'selectyesno', 'seb_allowcapturemicrophone' => 'selectyesno', 'seb_allowspellchecking' => 'selectyesno', 'seb_activateurlfiltering' => 'selectyesno', 'seb_filterembeddedcontent' => 'selectyesno', 'seb_expressionsallowed' => 'textarea', 'seb_regexallowed' => 'textarea', 'seb_expressionsblocked' => 'textarea', 'seb_regexblocked' => 'textarea', ]; } /** * Get the types of the quiz settings elements. * @return array List of types for the setting elements. */ public static function get_seb_config_element_types(): array { return [ 'seb_linkquitseb' => PARAM_RAW, 'seb_userconfirmquit' => PARAM_BOOL, 'seb_allowuserquitseb' => PARAM_BOOL, 'seb_quitpassword' => PARAM_RAW, 'seb_allowreloadinexam' => PARAM_BOOL, 'seb_showsebtaskbar' => PARAM_BOOL, 'seb_showreloadbutton' => PARAM_BOOL, 'seb_showtime' => PARAM_BOOL, 'seb_showkeyboardlayout' => PARAM_BOOL, 'seb_showwificontrol' => PARAM_BOOL, 'seb_enableaudiocontrol' => PARAM_BOOL, 'seb_muteonstartup' => PARAM_BOOL, 'seb_allowcapturecamera' => PARAM_BOOL, 'seb_allowcapturemicrophone' => PARAM_BOOL, 'seb_allowspellchecking' => PARAM_BOOL, 'seb_activateurlfiltering' => PARAM_BOOL, 'seb_filterembeddedcontent' => PARAM_BOOL, 'seb_expressionsallowed' => PARAM_RAW, 'seb_regexallowed' => PARAM_RAW, 'seb_expressionsblocked' => PARAM_RAW, 'seb_regexblocked' => PARAM_RAW, ]; } /** * Check that we have conflicting permissions with the current SEB settings. * * Check if the existing settings of the quiz (if any) are conflicting with the * capabilities of the managing user. * * E.g. a quiz is using an SEB template and a site admin is able to select this * option while a course manager cannot. Therefore it will return true for a course * manager and return false for a site admin. * * @param \context $context Context used with capability checking. * * @return bool */ public static function is_conflicting_permissions(\context $context) { if ($context instanceof \context_course) { return false; } $settings = seb_quiz_settings::get_record(['cmid' => (int) $context->instanceid]); if (empty($settings)) { return false; } if (!self::can_use_seb_template($context) && $settings->get('requiresafeexambrowser') == self::USE_SEB_TEMPLATE) { return true; } if (!self::can_use_seb_client_config($context) && $settings->get('requiresafeexambrowser') == self::USE_SEB_CLIENT_CONFIG) { return true; } if (!self::can_upload_seb_file($context) && $settings->get('requiresafeexambrowser') == self::USE_SEB_UPLOAD_CONFIG) { return true; } if (!self::can_configure_manually($context) && $settings->get('requiresafeexambrowser') == self::USE_SEB_CONFIG_MANUALLY) { return true; } return false; } /** * Returns a list of all options of SEB usage. * * @param \context $context Context used with capability checking selection options. * @return array */ public static function get_requiresafeexambrowser_options(\context $context): array { $options[self::USE_SEB_NO] = get_string('no'); if (self::can_configure_manually($context) || self::is_conflicting_permissions($context)) { $options[self::USE_SEB_CONFIG_MANUALLY] = get_string('seb_use_manually', 'quizaccess_seb'); } if (self::can_use_seb_template($context) || self::is_conflicting_permissions($context)) { if (!empty(self::get_template_options($context->instanceid))) { $options[self::USE_SEB_TEMPLATE] = get_string('seb_use_template', 'quizaccess_seb'); } } if (self::can_upload_seb_file($context) || self::is_conflicting_permissions($context)) { $options[self::USE_SEB_UPLOAD_CONFIG] = get_string('seb_use_upload', 'quizaccess_seb'); } if (self::can_use_seb_client_config($context) || self::is_conflicting_permissions($context)) { $options[self::USE_SEB_CLIENT_CONFIG] = get_string('seb_use_client', 'quizaccess_seb'); } return $options; } /** * Returns a list of templates. * @return array */ protected static function get_template_options($cmid): array { $templates = []; $templatetable = template::TABLE; $sebquizsettingstable = seb_quiz_settings::TABLE; $select = "enabled = 1 OR EXISTS ( SELECT 1 FROM {{$sebquizsettingstable}} WHERE templateid = {{$templatetable}}.id AND cmid = ? )"; if ($records = template::get_records_select($select, [$cmid], 'id, name')) { foreach ($records as $record) { $templates[$record->get('id')] = $record->get('name'); } } return $templates; } /** * Returns a list of options for the file manager element. * @return array */ public static function get_filemanager_options(): array { return [ 'subdirs' => 0, 'maxfiles' => 1, 'accepted_types' => ['.seb'] ]; } /** * Get the default values of the quiz settings. * * Array key is name of 'form element'/'database column (excluding prefix)'. * * @return array List of settings and their defaults. */ public static function get_seb_config_element_defaults(): array { return [ 'seb_linkquitseb' => '', 'seb_userconfirmquit' => 1, 'seb_allowuserquitseb' => 1, 'seb_quitpassword' => '', 'seb_allowreloadinexam' => 1, 'seb_showsebtaskbar' => 1, 'seb_showreloadbutton' => 1, 'seb_showtime' => 1, 'seb_showkeyboardlayout' => 1, 'seb_showwificontrol' => 0, 'seb_enableaudiocontrol' => 0, 'seb_allowcapturecamera' => 0, 'seb_allowcapturemicrophone' => 0, 'seb_muteonstartup' => 0, 'seb_allowspellchecking' => 0, 'seb_activateurlfiltering' => 0, 'seb_filterembeddedcontent' => 0, 'seb_expressionsallowed' => '', 'seb_regexallowed' => '', 'seb_expressionsblocked' => '', 'seb_regexblocked' => '', ]; } /** * Validate that if a file has been uploaded by current user, that it is a valid PLIST XML file. * This function is only called if requiresafeexambrowser == settings_provider::USE_SEB_UPLOAD_CONFIG. * * @param string $itemid Item ID of file in user draft file area. * @return void|lang_string */ public static function validate_draftarea_configfile($itemid) { // When saving the settings, this value will be null. if (is_null($itemid)) { return; } // If there is a config file uploaded, make sure it is a PList XML file. $file = self::get_current_user_draft_file($itemid); // If we require an SEB config uploaded, and the file exists, parse it. if ($file) { if (!helper::is_valid_seb_config($file->get_content())) { return new lang_string('fileparsefailed', 'quizaccess_seb'); } } // If we require an SEB config uploaded, and the file does not exist, error. if (!$file) { return new lang_string('filenotpresent', 'quizaccess_seb'); } } /** * Try and get a file in the user draft filearea by itemid. * * @param string $itemid Item ID of the file. * @return stored_file|null Returns null if no file is found. */ public static function get_current_user_draft_file(string $itemid): ?stored_file { global $USER; $context = context_user::instance($USER->id); $fs = get_file_storage(); if (!$files = $fs->get_area_files($context->id, 'user', 'draft', $itemid, 'id DESC', false)) { return null; } return reset($files); } /** * Get the file that is stored in the course module file area. * * @param string $cmid The course module id which is used as an itemid reference. * @return stored_file|null Returns null if no file is found. */ public static function get_module_context_sebconfig_file(string $cmid): ?stored_file { $fs = new \file_storage(); $context = context_module::instance($cmid); if (!$files = $fs->get_area_files($context->id, 'quizaccess_seb', 'filemanager_sebconfigfile', 0, 'id DESC', false)) { return null; } return reset($files); } /** * Saves filemanager_sebconfigfile files to the moodle storage backend. * * @param string $draftitemid The id of the draft area to use. * @param string $cmid The cmid of for the quiz. * @return bool Always true */ public static function save_filemanager_sebconfigfile_draftarea(string $draftitemid, string $cmid): bool { if ($draftitemid) { $context = context_module::instance($cmid); file_save_draft_area_files($draftitemid, $context->id, 'quizaccess_seb', 'filemanager_sebconfigfile', 0, []); } return true; } /** * Cleanup function to delete the saved config when it has not been specified. * This will be called when settings_provider::USE_SEB_UPLOAD_CONFIG is not true. * * @param string $cmid The cmid of for the quiz. * @return bool Always true or exception if error occurred */ public static function delete_uploaded_config_file(string $cmid): bool { $file = self::get_module_context_sebconfig_file($cmid); if (!empty($file)) { return $file->delete(); } return false; } /** * Check if the current user can configure SEB. * * @param \context $context Context to check access in. * @return bool */ public static function can_configure_seb(\context $context): bool { return has_capability('quizaccess/seb:manage_seb_requiresafeexambrowser', $context); } /** * Check if the current user can select to use the SEB client configuration. * * @param \context $context Context to check access in. * @return bool */ public static function can_use_seb_client_config(\context $context): bool { return has_capability('quizaccess/seb:manage_seb_usesebclientconfig', $context); } /** * Check if the current user can use preconfigured templates. * * @param \context $context Context to check access in. * @return bool */ public static function can_use_seb_template(\context $context): bool { return has_capability('quizaccess/seb:manage_seb_templateid', $context); } /** * Check if the current user can upload own SEB config file. * * @param \context $context Context to check access in. * @return bool */ public static function can_upload_seb_file(\context $context): bool { return has_capability('quizaccess/seb:manage_filemanager_sebconfigfile', $context); } /** * Check if the current user can change Show Safe Exam Browser download button setting. * * @param \context $context Context to check access in. * @return bool */ public static function can_change_seb_showsebdownloadlink(\context $context): bool { return has_capability('quizaccess/seb:manage_seb_showsebdownloadlink', $context); } /** * Check if the current user can change Allowed Browser Exam Keys setting. * * @param \context $context Context to check access in. * @return bool */ public static function can_change_seb_allowedbrowserexamkeys(\context $context): bool { return has_capability('quizaccess/seb:manage_seb_allowedbrowserexamkeys', $context); } /** * Check if the current user can config SEB manually. * * @param \context $context Context to check access in. * @return bool */ public static function can_configure_manually(\context $context): bool { if (!has_capability('quizaccess/seb:manage_seb_configuremanually', $context)) { return false; } foreach (self::get_seb_config_elements() as $name => $type) { if (self::can_manage_seb_config_setting($name, $context)) { return true; } } return false; } /** * Check if the current user can manage provided SEB setting. * * @param string $settingname Name of the setting. * @param \context $context Context to check access in. * @return bool */ public static function can_manage_seb_config_setting(string $settingname, \context $context): bool { $capsttocheck = []; foreach (self::get_seb_settings_map() as $type => $settings) { $capsttocheck = self::build_config_capabilities_to_check($settingname, $settings); if (!empty($capsttocheck)) { break; } } foreach ($capsttocheck as $capability) { // Capability must exist. if (!$capinfo = get_capability_info($capability)) { throw new \coding_exception("Capability '{$capability}' was not found! This has to be fixed in code."); } } return has_all_capabilities($capsttocheck, $context); } /** * Helper method to build a list of capabilities to check. * * @param string $settingname Given setting name to build caps for. * @param array $settings A list of settings to go through. * @return array */ protected static function build_config_capabilities_to_check(string $settingname, array $settings): array { $capsttocheck = []; foreach ($settings as $setting => $children) { if ($setting == $settingname) { $capsttocheck[$setting] = self::build_setting_capability_name($setting); break; // Found what we need exit the loop. } // Recursively check all children. $capsttocheck = self::build_config_capabilities_to_check($settingname, $children); if (!empty($capsttocheck)) { // Matching child found, add the parent capability to the list of caps to check. $capsttocheck[$setting] = self::build_setting_capability_name($setting); break; // Found what we need exit the loop. } } return $capsttocheck; } /** * Helper method to return a map of all settings. * * @return array */ public static function get_seb_settings_map(): array { return [ self::USE_SEB_NO => [ ], self::USE_SEB_CONFIG_MANUALLY => [ 'seb_showsebdownloadlink' => [], 'seb_linkquitseb' => [], 'seb_userconfirmquit' => [], 'seb_allowuserquitseb' => [ 'seb_quitpassword' => [] ], 'seb_allowreloadinexam' => [], 'seb_showsebtaskbar' => [ 'seb_showreloadbutton' => [], 'seb_showtime' => [], 'seb_showkeyboardlayout' => [], 'seb_showwificontrol' => [], ], 'seb_enableaudiocontrol' => [ 'seb_muteonstartup' => [], ], 'seb_allowcapturecamera' => [], 'seb_allowcapturemicrophone' => [], 'seb_allowspellchecking' => [], 'seb_activateurlfiltering' => [ 'seb_filterembeddedcontent' => [], 'seb_expressionsallowed' => [], 'seb_regexallowed' => [], 'seb_expressionsblocked' => [], 'seb_regexblocked' => [], ], ], self::USE_SEB_TEMPLATE => [ 'seb_templateid' => [], 'seb_showsebdownloadlink' => [], 'seb_allowuserquitseb' => [ 'seb_quitpassword' => [], ], ], self::USE_SEB_UPLOAD_CONFIG => [ 'filemanager_sebconfigfile' => [], 'seb_showsebdownloadlink' => [], 'seb_allowedbrowserexamkeys' => [], ], self::USE_SEB_CLIENT_CONFIG => [ 'seb_showsebdownloadlink' => [], 'seb_allowedbrowserexamkeys' => [], ], ]; } /** * Get allowed settings for provided SEB usage type. * * @param int $requiresafeexambrowser SEB usage type. * @return array */ private static function get_allowed_settings(int $requiresafeexambrowser): array { $result = []; $map = self::get_seb_settings_map(); if (!key_exists($requiresafeexambrowser, $map)) { return $result; } return self::build_allowed_settings($map[$requiresafeexambrowser]); } /** * Recursive method to build a list of allowed settings. * * @param array $settings A list of settings from settings map. * @return array */ private static function build_allowed_settings(array $settings): array { $result = []; foreach ($settings as $name => $children) { $result[] = $name; foreach ($children as $childname => $child) { $result[] = $childname; $result = array_merge($result, self::build_allowed_settings($child)); } } return $result; } /** * Get the conditions that an element should be hid in the form. Expects matching using 'eq'. * * Array key is name of 'form element'/'database column (excluding prefix)'. * Values are instances of hideif_rule class. * * @return array List of rules per element. */ public static function get_quiz_hideifs(): array { $hideifs = []; // We are building rules based on the settings map, that means children will be dependant on parent. // In most cases it's all pretty standard. // However it could be some specific cases for some fields, which will be overridden later. foreach (self::get_seb_settings_map() as $type => $settings) { foreach ($settings as $setting => $children) { $hideifs[$setting][] = new hideif_rule($setting, 'seb_requiresafeexambrowser', 'noteq', $type); foreach ($children as $childname => $child) { $hideifs[$childname][] = new hideif_rule($childname, 'seb_requiresafeexambrowser', 'noteq', $type); $hideifs[$childname][] = new hideif_rule($childname, $setting, 'eq', 0); } } } // Specific case for "Enable quitting of SEB". It should available for Manual and Template. $hideifs['seb_allowuserquitseb'] = [ new hideif_rule('seb_allowuserquitseb', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_NO), new hideif_rule('seb_allowuserquitseb', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_CLIENT_CONFIG), new hideif_rule('seb_allowuserquitseb', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_UPLOAD_CONFIG), ]; // Specific case for "Quit password". It should be available for Manual and Template. As it's parent. $hideifs['seb_quitpassword'] = [ new hideif_rule('seb_quitpassword', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_NO), new hideif_rule('seb_quitpassword', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_CLIENT_CONFIG), new hideif_rule('seb_quitpassword', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_UPLOAD_CONFIG), new hideif_rule('seb_quitpassword', 'seb_allowuserquitseb', 'eq', 0), ]; // Specific case for "Show Safe Exam Browser download button". It should be available for all cases, except No Seb. $hideifs['seb_showsebdownloadlink'] = [ new hideif_rule('seb_showsebdownloadlink', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_NO) ]; // Specific case for "Allowed Browser Exam Keys". It should be available for Template and Browser config. $hideifs['seb_allowedbrowserexamkeys'] = [ new hideif_rule('seb_allowedbrowserexamkeys', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_NO), new hideif_rule('seb_allowedbrowserexamkeys', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_CONFIG_MANUALLY), new hideif_rule('seb_allowedbrowserexamkeys', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_TEMPLATE), ]; return $hideifs; } /** * Build a capability name for the provided SEB setting. * * @param string $settingname Name of the setting. * @return string */ public static function build_setting_capability_name(string $settingname): string { if (!key_exists($settingname, self::get_seb_config_elements())) { throw new \coding_exception('Incorrect SEB quiz setting ' . $settingname); } return 'quizaccess/seb:manage_' . $settingname; } /** * Check if settings is locked. * * @param int $quizid Quiz ID. * @return bool */ public static function is_seb_settings_locked($quizid): bool { if (empty($quizid)) { return false; } return quiz_has_attempts($quizid); } /** * Filter a standard class by prefix. * * @param stdClass $settings Quiz settings object. * @return stdClass Filtered object. */ private static function filter_by_prefix(\stdClass $settings): stdClass { $newsettings = new \stdClass(); foreach ($settings as $name => $setting) { // Only add it, if not there. if (strpos($name, "seb_") === 0) { $newsettings->$name = $setting; // Add new key. } } return $newsettings; } /** * Filter settings based on the setting map. Set value of not allowed settings to null. * * @param stdClass $settings Quiz settings. * @return \stdClass */ private static function filter_by_settings_map(stdClass $settings): stdClass { if (!isset($settings->seb_requiresafeexambrowser)) { return $settings; } $newsettings = new \stdClass(); $newsettings->seb_requiresafeexambrowser = $settings->seb_requiresafeexambrowser; $allowedsettings = self::get_allowed_settings((int)$newsettings->seb_requiresafeexambrowser); unset($settings->seb_requiresafeexambrowser); foreach ($settings as $name => $value) { if (!in_array($name, $allowedsettings)) { $newsettings->$name = null; } else { $newsettings->$name = $value; } } return $newsettings; } /** * Filter quiz settings for this plugin only. * * @param stdClass $settings Quiz settings. * @return stdClass Filtered settings. */ public static function filter_plugin_settings(stdClass $settings): stdClass { $settings = self::filter_by_prefix($settings); $settings = self::filter_by_settings_map($settings); return self::strip_all_prefixes($settings); } /** * Strip the seb_ prefix from each setting key. * * @param \stdClass $settings Object containing settings. * @return \stdClass The modified settings object. */ private static function strip_all_prefixes(\stdClass $settings): stdClass { $newsettings = new \stdClass(); foreach ($settings as $name => $setting) { $newname = preg_replace("/^seb_/", "", $name); $newsettings->$newname = $setting; // Add new key. } return $newsettings; } /** * Add prefix to string. * * @param string $name String to add prefix to. * @return string String with prefix. */ public static function add_prefix(string $name): string { if (strpos($name, 'seb_') !== 0) { $name = 'seb_' . $name; } return $name; } } PK\qU22classes/seb_access_manager.phpnu[. /** * Manage the access to the quiz. * * @package quizaccess_seb * @author Tim Hunt * @author Luca Bösch * @author Andrew Madden * @author Dmitrii Metelkin * @copyright 2019 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace quizaccess_seb; use context_module; use mod_quiz\quiz_settings; defined('MOODLE_INTERNAL') || die(); /** * Manage the access to the quiz. * * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class seb_access_manager { /** Header sent by Safe Exam Browser containing the Config Key hash. */ private const CONFIG_KEY_HEADER = 'HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'; /** Header sent by Safe Exam Browser containing the Browser Exam Key hash. */ private const BROWSER_EXAM_KEY_HEADER = 'HTTP_X_SAFEEXAMBROWSER_REQUESTHASH'; /** @var quiz_settings $quiz A quiz object containing all information pertaining to current quiz. */ private $quiz; /** @var seb_quiz_settings $quizsettings A quiz settings persistent object containing plugin settings */ private $quizsettings; /** @var context_module $context Context of this quiz activity. */ private $context; /** @var string|null $validconfigkey Expected valid SEB config key. */ private $validconfigkey = null; /** * The access_manager constructor. * * @param quiz_settings $quiz The details of the quiz. */ public function __construct(quiz_settings $quiz) { $this->quiz = $quiz; $this->context = context_module::instance($quiz->get_cmid()); $this->quizsettings = seb_quiz_settings::get_by_quiz_id($quiz->get_quizid()); $this->validconfigkey = seb_quiz_settings::get_config_key_by_quiz_id($quiz->get_quizid()); } /** * Validate browser exam key. It will validate a provided browser exam key if provided, then will fall back to checking * the header. * * @param string|null $browserexamkey Optional. Can validate a provided key, or will fall back to checking header. * @param string|null $url Optionally provide URL of page to validate. * @return bool */ public function validate_browser_exam_key(?string $browserexamkey = null, ?string $url = null): bool { if (!$this->should_validate_browser_exam_key()) { // Browser exam key should not be checked, so do not prevent access. return true; } if (!$this->is_allowed_browser_examkeys_configured()) { return true; // If no browser exam keys, no check required. } if (empty($browserexamkey)) { $browserexamkey = $this->get_received_browser_exam_key(); } $validbrowserexamkeys = $this->quizsettings->get('allowedbrowserexamkeys'); // If the Browser Exam Key header isn't present, prevent access. if (is_null($browserexamkey)) { return false; } return $this->check_browser_exam_keys($validbrowserexamkeys, $browserexamkey, $url); } /** * Validate a config key. It will check a provided config key if provided then will fall back to checking config * key in header. * * @param string|null $configkey Optional. Can validate a provided key, or will fall back to checking header. * @param string|null $url URL of page to validate. * @return bool */ public function validate_config_key(?string $configkey = null, ?string $url = null): bool { if (!$this->should_validate_config_key()) { // Config key should not be checked, so do not prevent access. return true; } // If using client config, or with no requirement, then no check required. $requiredtype = $this->get_seb_use_type(); if ($requiredtype == settings_provider::USE_SEB_NO || $requiredtype == settings_provider::USE_SEB_CLIENT_CONFIG) { return true; } if (empty($configkey)) { $configkey = $this->get_received_config_key(); } if (empty($this->validconfigkey)) { return false; // No config key has been saved. } if (is_null($configkey)) { return false; } // Check if there is a valid config key supplied in the header. return $this->check_key($this->validconfigkey, $configkey, $url); } /** * Check if Safe Exam Browser is required to access quiz. * If quizsettings do not exist, then there is no requirement for using SEB. * * @return bool If required. */ public function seb_required(): bool { if (!$this->quizsettings) { return false; } else { return $this->get_seb_use_type() != settings_provider::USE_SEB_NO; } } /** * This is the basic check for the Safe Exam Browser previously used in the quizaccess_safebrowser plugin that * managed basic Moodle interactions with SEB. * * @return bool */ public function validate_basic_header(): bool { if (!$this->should_validate_basic_header()) { // Config key should not be checked, so do not prevent access. return true; } if ($this->get_seb_use_type() == settings_provider::USE_SEB_CLIENT_CONFIG) { return $this->is_using_seb(); } return true; } /** * Check if using Safe Exam Browser. * * @return bool */ public function is_using_seb(): bool { if (isset($_SERVER['HTTP_USER_AGENT'])) { return strpos($_SERVER['HTTP_USER_AGENT'], 'SEB') !== false; } return false; } /** * Check if user has any capability to bypass the Safe Exam Browser requirement. * * @return bool True if user can bypass check. */ public function can_bypass_seb(): bool { return has_capability('quizaccess/seb:bypassseb', $this->context); } /** * Return the full URL that was used to request the current page, which is * what we need for verifying the X-SafeExamBrowser-RequestHash header. */ private function get_this_page_url(): string { global $CFG, $FULLME; // If $FULLME not set fall back to wwwroot. if ($FULLME == null) { return $CFG->wwwroot; } return $FULLME; } /** * Return expected SEB config key. * * @return string|null */ public function get_valid_config_key(): ?string { return $this->validconfigkey; } /** * Getter for the quiz object. * * @return \mod_quiz\quiz_settings */ public function get_quiz(): quiz_settings { return $this->quiz; } /** * Check that at least one browser exam key exists in the quiz settings. * * @return bool True if one or more keys are set in quiz settings. */ private function is_allowed_browser_examkeys_configured(): bool { return !empty($this->quizsettings->get('allowedbrowserexamkeys')); } /** * Check the hash from the request header against the permitted browser exam keys. * * @param array $keys Allowed browser exam keys. * @param string $header The value of the X-SafeExamBrowser-RequestHash to check. * @param string|null $url URL of page to validate. * @return bool True if the hash matches. */ private function check_browser_exam_keys(array $keys, string $header, ?string $url = null): bool { foreach ($keys as $key) { if ($this->check_key($key, $header, $url)) { return true; } } return false; } /** * Check the hash from the request header against a single permitted key. * * @param string $validkey An allowed key. * @param string $key The value of X-SafeExamBrowser-RequestHash, X-SafeExamBrowser-ConfigKeyHash or a provided key to check. * @param string|null $url URL of page to validate. * @return bool True if the hash matches. */ private function check_key(string $validkey, string $key, ?string $url = null): bool { if (empty($url)) { $url = $this->get_this_page_url(); } return hash('sha256', $url . $validkey) === $key; } /** * Returns Safe Exam Browser Config Key hash. * * @return string|null */ public function get_received_config_key(): ?string { if (isset($_SERVER[self::CONFIG_KEY_HEADER])) { return trim($_SERVER[self::CONFIG_KEY_HEADER]); } return null; } /** * Returns the Browser Exam Key hash. * * @return string|null */ public function get_received_browser_exam_key(): ?string { if (isset($_SERVER[self::BROWSER_EXAM_KEY_HEADER])) { return trim($_SERVER[self::BROWSER_EXAM_KEY_HEADER]); } return null; } /** * Get type of SEB usage for the quiz. * * @return int */ public function get_seb_use_type(): int { if (empty($this->quizsettings)) { return settings_provider::USE_SEB_NO; } else { return $this->quizsettings->get('requiresafeexambrowser'); } } /** * Should validate basic header? * * @return bool */ public function should_validate_basic_header(): bool { return in_array($this->get_seb_use_type(), [ settings_provider::USE_SEB_CLIENT_CONFIG, ]); } /** * Should validate SEB config key? * @return bool */ public function should_validate_config_key(): bool { return in_array($this->get_seb_use_type(), [ settings_provider::USE_SEB_CONFIG_MANUALLY, settings_provider::USE_SEB_TEMPLATE, settings_provider::USE_SEB_UPLOAD_CONFIG, ]); } /** * Should validate browser exam key? * * @return bool */ public function should_validate_browser_exam_key(): bool { return in_array($this->get_seb_use_type(), [ settings_provider::USE_SEB_UPLOAD_CONFIG, settings_provider::USE_SEB_CLIENT_CONFIG, ]); } /** * Set session access for quiz. * * @param bool $accessallowed */ public function set_session_access(bool $accessallowed): void { global $SESSION; if (!isset($SESSION->quizaccess_seb_access)) { $SESSION->quizaccess_seb_access = []; } $SESSION->quizaccess_seb_access[$this->quiz->get_cmid()] = $accessallowed; } /** * Check session access for quiz if already set. * * @return bool */ public function validate_session_access(): bool { global $SESSION; return !empty($SESSION->quizaccess_seb_access[$this->quiz->get_cmid()]); } /** * Unset the global session access variable for this quiz. */ public function clear_session_access(): void { global $SESSION; unset($SESSION->quizaccess_seb_access[$this->quiz->get_cmid()]); } /** * Redirect to SEB config link. This will force Safe Exam Browser to be reconfigured. */ public function redirect_to_seb_config_link(): void { global $PAGE; $seblink = \quizaccess_seb\link_generator::get_link($this->quiz->get_cmid(), true, is_https()); $PAGE->requires->js_amd_inline("document.location.replace('" . $seblink . "')"); } /** * Check if we need to redirect to SEB config link. * * @return bool */ public function should_redirect_to_seb_config_link(): bool { // We check if there is an existing config key header. If there is none, we assume that // the SEB application is not using header verification so auto redirect should not proceed. $haskeyinheader = !is_null($this->get_received_config_key()); return $this->is_using_seb() && get_config('quizaccess_seb', 'autoreconfigureseb') && $haskeyinheader; } } PK\"classes/event/template_created.phpnu[. /** * Event for when a template is created. * * @package quizaccess_seb * @author Nicholas Hoobin * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace quizaccess_seb\event; use context_system; use core\event\base; use quizaccess_seb\template; defined('MOODLE_INTERNAL') || die(); /** * Event for when a template is created. * * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class template_created extends base { /** * Create event with strict parameters. * * Define strict parameters to create event with instead of relying on internal validation of array. Better code practice. * Easier for consumers of this class to know what data must be supplied and observers can have more trust in event data. * * @param template $template SEB template. * @param context_system $context Context system. * @return base */ public static function create_strict(template $template, context_system $context): base { global $USER; $tid = $template->get('id'); return self::create([ 'userid' => $USER->id, 'objectid' => $tid, 'context' => $context, ]); } /** * Initialize the event data. */ protected function init() { $this->data['objecttable'] = 'quizaccess_seb_template'; $this->data['crud'] = 'c'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; } /** * Get the name of the event. * * @return string Name of event. */ public static function get_name() { return get_string('event:templatecreated', 'quizaccess_seb'); } /** * Returns relevant URL. * @return \moodle_url */ public function get_url() { $params = [ 'id' => $this->objectid, 'action' => 'edit', ]; return new \moodle_url('/mod/quiz/accessrule/seb/template.php', $params); } /** * Returns non-localised event description with id's for admin use only. * * @return string Description. */ public function get_description() { return "The user with id '$this->userid' has created a template with id '$this->objectid'."; } /** * This is used when restoring course logs where it is required that we * map the objectid to it's new value in the new course. * * @return array Mapping of object id. */ public static function get_objectid_mapping(): array { return ['db' => 'quizaccess_seb_template', 'restore' => 'quizaccess_seb_template']; } /** * This is used when restoring course logs where it is required that we * map the information in 'other' to it's new value in the new course. * * @return array List of mapping of other ids. */ public static function get_other_mapping(): array { return []; } } PK\/DJ#classes/event/template_disabled.phpnu[. /** * Event for when a template is disabled. * * @package quizaccess_seb * @author Nicholas Hoobin * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace quizaccess_seb\event; use context_system; use core\event\base; use quizaccess_seb\template; defined('MOODLE_INTERNAL') || die(); /** * Event for when a template is disabled. * * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class template_disabled extends base { /** * Create event with strict parameters. * * Define strict parameters to create event with instead of relying on internal validation of array. Better code practice. * Easier for consumers of this class to know what data must be supplied and observers can have more trust in event data. * * @param template $template SEB template. * @param context_system $context Context system. * @return base */ public static function create_strict(template $template, context_system $context): base { global $USER; $tid = $template->get('id'); return self::create([ 'userid' => $USER->id, 'objectid' => $tid, 'context' => $context, ]); } /** * Initialize the event data. */ protected function init() { $this->data['objecttable'] = 'quizaccess_seb_template'; $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; } /** * Get the name of the event. * * @return string Name of event. */ public static function get_name() { return get_string('event:templatedisabled', 'quizaccess_seb'); } /** * Returns relevant URL. * @return \moodle_url */ public function get_url() { $params = [ 'id' => $this->objectid, 'action' => 'edit', ]; return new \moodle_url('/mod/quiz/accessrule/seb/template.php', $params); } /** * Returns non-localised event description with id's for admin use only. * * @return string Description. */ public function get_description() { return "The user with id '$this->userid' has disabled a template with id '$this->objectid'."; } /** * This is used when restoring course logs where it is required that we * map the objectid to it's new value in the new course. * * @return array Mapping of object id. */ public static function get_objectid_mapping(): array { return ['db' => 'quizaccess_seb_template', 'restore' => 'quizaccess_seb_template']; } /** * This is used when restoring course logs where it is required that we * map the information in 'other' to it's new value in the new course. * * @return array List of mapping of other ids. */ public static function get_other_mapping(): array { return []; } } PK\ @"classes/event/template_enabled.phpnu[. /** * Event for when a template is enabled. * * @package quizaccess_seb * @author Nicholas Hoobin * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace quizaccess_seb\event; use context_system; use core\event\base; use quizaccess_seb\template; defined('MOODLE_INTERNAL') || die(); /** * Event for when a template is enabled. * * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class template_enabled extends base { /** * Create event with strict parameters. * * Define strict parameters to create event with instead of relying on internal validation of array. Better code practice. * Easier for consumers of this class to know what data must be supplied and observers can have more trust in event data. * * @param template $template SEB template. * @param context_system $context Context system. * @return base */ public static function create_strict(template $template, context_system $context): base { global $USER; $tid = $template->get('id'); return self::create([ 'userid' => $USER->id, 'objectid' => $tid, 'context' => $context, ]); } /** * Initialize the event data. */ protected function init() { $this->data['objecttable'] = 'quizaccess_seb_template'; $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; } /** * Get the name of the event. * * @return string Name of event. */ public static function get_name() { return get_string('event:templateenabled', 'quizaccess_seb'); } /** * Returns relevant URL. * @return \moodle_url */ public function get_url() { $params = [ 'id' => $this->objectid, 'action' => 'edit', ]; return new \moodle_url('/mod/quiz/accessrule/seb/template.php', $params); } /** * Returns non-localised event description with id's for admin use only. * * @return string Description. */ public function get_description() { return "The user with id '$this->userid' has enabled a template with id '$this->objectid'."; } /** * This is used when restoring course logs where it is required that we * map the objectid to it's new value in the new course. * * @return array Mapping of object id. */ public static function get_objectid_mapping(): array { return ['db' => 'quizaccess_seb_template', 'restore' => 'quizaccess_seb_template']; } /** * This is used when restoring course logs where it is required that we * map the information in 'other' to it's new value in the new course. * * @return array List of mapping of other ids. */ public static function get_other_mapping(): array { return []; } } PK\"W "classes/event/template_deleted.phpnu[. /** * Event for when a template is deleted. * * @package quizaccess_seb * @author Nicholas Hoobin * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace quizaccess_seb\event; use context_system; use core\event\base; defined('MOODLE_INTERNAL') || die(); /** * Event for when a template is deleted. * * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class template_deleted extends base { /** * Create event with strict parameters. * * Define strict parameters to create event with instead of relying on internal validation of array. Better code practice. * Easier for consumers of this class to know what data must be supplied and observers can have more trust in event data. * * @param string $id The id of the template * @param context_system $context Context system. * @return base */ public static function create_strict(string $id, context_system $context): base { global $USER; return self::create([ 'userid' => $USER->id, 'objectid' => $id, 'context' => $context, ]); } /** * Initialize the event data. */ protected function init() { $this->data['objecttable'] = 'quizaccess_seb_template'; $this->data['crud'] = 'd'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; } /** * Get the name of the event. * * @return string Name of event. */ public static function get_name() { return get_string('event:templatedeleted', 'quizaccess_seb'); } /** * Returns relevant URL. * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/quiz/accessrule/seb/template.php'); } /** * Returns non-localised event description with id's for admin use only. * * @return string Description. */ public function get_description() { return "The user with id '$this->userid' has deleted a template with id '$this->objectid'."; } /** * This is used when restoring course logs where it is required that we * map the objectid to it's new value in the new course. * * @return array Mapping of object id. */ public static function get_objectid_mapping(): array { return ['db' => 'quizaccess_seb_template', 'restore' => 'quizaccess_seb_template']; } /** * This is used when restoring course logs where it is required that we * map the information in 'other' to it's new value in the new course. * * @return array List of mapping of other ids. */ public static function get_other_mapping(): array { return []; } } PK\UU"classes/event/access_prevented.phpnu[. /** * Event for when access to a quiz is prevented by this subplugin. * * @package quizaccess_seb * @author Andrew Madden * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace quizaccess_seb\event; use core\event\base; use quizaccess_seb\seb_access_manager; defined('MOODLE_INTERNAL') || die(); /** * Event for when access to a quiz is prevented by this subplugin. * * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class access_prevented extends base { /** * Create event with strict parameters. * * Define strict parameters to create event with instead of relying on internal validation of array. Better code practice. * Easier for consumers of this class to know what data must be supplied and observers can have more trust in event data. * * @param seb_access_manager $accessmanager Access manager. * @param string $reason Reason that access was prevented. * @param string|null $configkey A Safe Exam Browser config key. * @param string|null $browserexamkey A Safe Exam Browser browser exam key. * @return base */ public static function create_strict(seb_access_manager $accessmanager, string $reason, ?string $configkey = null, ?string $browserexamkey = null): base { global $USER; $other = []; $other['reason'] = $reason; $other['savedconfigkey'] = $accessmanager->get_valid_config_key(); $other['receivedconfigkey'] = !empty($configkey) ? $configkey : $accessmanager->get_received_config_key(); $other['receivedbrowserexamkey'] = !empty($browserexamkey) ? $browserexamkey : $accessmanager->get_received_browser_exam_key(); return self::create([ 'userid' => $USER->id, 'objectid' => $accessmanager->get_quiz()->get_quizid(), 'courseid' => $accessmanager->get_quiz()->get_courseid(), 'context' => $accessmanager->get_quiz()->get_context(), 'other' => $other, ]); } /** * Initialize the event data. */ protected function init() { $this->data['objecttable'] = 'quiz'; $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; } /** * Get the name of the event. * * @return string Name of event. */ public static function get_name() { return get_string('event:accessprevented', 'quizaccess_seb'); } /** * Returns non-localised event description with id's for admin use only. * * @return string Description. */ public function get_description() { $description = "The user with id '$this->userid' has been prevented from accessing quiz with id '$this->objectid' by the " . "Safe Exam Browser access plugin. The reason was '{$this->other['reason']}'. " . "Expected config key: '{$this->other['savedconfigkey']}'. " . "Received config key: '{$this->other['receivedconfigkey']}'. " . "Received browser exam key: '{$this->other['receivedbrowserexamkey']}'."; return $description; } /** * This is used when restoring course logs where it is required that we * map the objectid to it's new value in the new course. * * @return array Mapping of object id. */ public static function get_objectid_mapping(): array { return ['db' => 'quiz', 'restore' => 'quiz']; } /** * This is used when restoring course logs where it is required that we * map the information in 'other' to it's new value in the new course. * * @return array List of mapping of other ids. */ public static function get_other_mapping(): array { return [ 'cmid' => ['db' => 'course_modules', 'restore' => 'course_modules'] ]; } } PK\{HΗ"classes/event/template_updated.phpnu[. /** * Event for when a template is updated. * * @package quizaccess_seb * @author Nicholas Hoobin * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace quizaccess_seb\event; use context_system; use core\event\base; use quizaccess_seb\template; defined('MOODLE_INTERNAL') || die(); /** * Event for when a template is updated. * * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class template_updated extends base { /** * Create event with strict parameters. * * Define strict parameters to create event with instead of relying on internal validation of array. Better code practice. * Easier for consumers of this class to know what data must be supplied and observers can have more trust in event data. * * @param template $template SEB template. * @param context_system $context Context system. * @return base */ public static function create_strict(template $template, context_system $context): base { global $USER; $tid = $template->get('id'); return self::create([ 'userid' => $USER->id, 'objectid' => $tid, 'context' => $context, ]); } /** * Initialize the event data. */ protected function init() { $this->data['objecttable'] = 'quizaccess_seb_template'; $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; } /** * Get the name of the event. * * @return string Name of event. */ public static function get_name() { return get_string('event:templateupdated', 'quizaccess_seb'); } /** * Returns relevant URL. * @return \moodle_url */ public function get_url() { $params = [ 'id' => $this->objectid, 'action' => 'edit', ]; return new \moodle_url('/mod/quiz/accessrule/seb/template.php', $params); } /** * Returns non-localised event description with id's for admin use only. * * @return string Description. */ public function get_description() { return "The user with id '$this->userid' has updated a template with id '$this->objectid'."; } /** * This is used when restoring course logs where it is required that we * map the objectid to it's new value in the new course. * * @return array Mapping of object id. */ public static function get_objectid_mapping(): array { return ['db' => 'quizaccess_seb_template', 'restore' => 'quizaccess_seb_template']; } /** * This is used when restoring course logs where it is required that we * map the information in 'other' to it's new value in the new course. * * @return array List of mapping of other ids. */ public static function get_other_mapping(): array { return []; } } PK\aV5Z5Zclasses/seb_quiz_settings.phpnu[. /** * Entity model representing quiz settings for the seb plugin. * * @package quizaccess_seb * @author Andrew Madden * @copyright 2019 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace quizaccess_seb; use CFPropertyList\CFArray; use CFPropertyList\CFBoolean; use CFPropertyList\CFDictionary; use CFPropertyList\CFNumber; use CFPropertyList\CFString; use core\persistent; use lang_string; use moodle_exception; use moodle_url; defined('MOODLE_INTERNAL') || die(); /** * Entity model representing quiz settings for the seb plugin. * * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class seb_quiz_settings extends persistent { /** Table name for the persistent. */ const TABLE = 'quizaccess_seb_quizsettings'; /** @var property_list $plist The SEB config represented as a Property List object. */ private $plist; /** @var string $config The SEB config represented as a string. */ private $config; /** @var string $configkey The SEB config key represented as a string. */ private $configkey; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties(): array { return [ 'quizid' => [ 'type' => PARAM_INT, ], 'cmid' => [ 'type' => PARAM_INT, ], 'templateid' => [ 'type' => PARAM_INT, 'default' => 0, ], 'requiresafeexambrowser' => [ 'type' => PARAM_INT, 'default' => 0, ], 'showsebtaskbar' => [ 'type' => PARAM_INT, 'default' => 1, 'null' => NULL_ALLOWED, ], 'showwificontrol' => [ 'type' => PARAM_INT, 'default' => 0, 'null' => NULL_ALLOWED, ], 'showreloadbutton' => [ 'type' => PARAM_INT, 'default' => 1, 'null' => NULL_ALLOWED, ], 'showtime' => [ 'type' => PARAM_INT, 'default' => 1, 'null' => NULL_ALLOWED, ], 'showkeyboardlayout' => [ 'type' => PARAM_INT, 'default' => 1, 'null' => NULL_ALLOWED, ], 'allowuserquitseb' => [ 'type' => PARAM_INT, 'default' => 1, 'null' => NULL_ALLOWED, ], 'quitpassword' => [ 'type' => PARAM_TEXT, 'default' => '', 'null' => NULL_ALLOWED, ], 'linkquitseb' => [ 'type' => PARAM_URL, 'default' => '', 'null' => NULL_ALLOWED, ], 'userconfirmquit' => [ 'type' => PARAM_INT, 'default' => 1, 'null' => NULL_ALLOWED, ], 'enableaudiocontrol' => [ 'type' => PARAM_INT, 'default' => 0, 'null' => NULL_ALLOWED, ], 'muteonstartup' => [ 'type' => PARAM_INT, 'default' => 0, 'null' => NULL_ALLOWED, ], 'allowcapturecamera' => [ 'type' => PARAM_INT, 'default' => 0, 'null' => NULL_ALLOWED, ], 'allowcapturemicrophone' => [ 'type' => PARAM_INT, 'default' => 0, 'null' => NULL_ALLOWED, ], 'allowspellchecking' => [ 'type' => PARAM_INT, 'default' => 0, 'null' => NULL_ALLOWED, ], 'allowreloadinexam' => [ 'type' => PARAM_INT, 'default' => 1, 'null' => NULL_ALLOWED, ], 'activateurlfiltering' => [ 'type' => PARAM_INT, 'default' => 0, 'null' => NULL_ALLOWED, ], 'filterembeddedcontent' => [ 'type' => PARAM_INT, 'default' => 0, 'null' => NULL_ALLOWED, ], 'expressionsallowed' => [ 'type' => PARAM_TEXT, 'default' => '', 'null' => NULL_ALLOWED, ], 'regexallowed' => [ 'type' => PARAM_TEXT, 'default' => '', 'null' => NULL_ALLOWED, ], 'expressionsblocked' => [ 'type' => PARAM_TEXT, 'default' => '', 'null' => NULL_ALLOWED, ], 'regexblocked' => [ 'type' => PARAM_TEXT, 'default' => '', 'null' => NULL_ALLOWED, ], 'showsebdownloadlink' => [ 'type' => PARAM_INT, 'default' => 1, 'null' => NULL_ALLOWED, ], 'allowedbrowserexamkeys' => [ 'type' => PARAM_TEXT, 'default' => '', 'null' => NULL_ALLOWED, ], ]; } /** * Return an instance by quiz id. * * This method gets data from cache before doing any DB calls. * * @param int $quizid Quiz id. * @return false|\quizaccess_seb\seb_quiz_settings */ public static function get_by_quiz_id(int $quizid) { if ($data = self::get_quiz_settings_cache()->get($quizid)) { return new static(0, $data); } return self::get_record(['quizid' => $quizid]); } /** * Return cached SEB config represented as a string by quiz ID. * * @param int $quizid Quiz id. * @return string|null */ public static function get_config_by_quiz_id(int $quizid): ?string { $config = self::get_config_cache()->get($quizid); if ($config !== false) { return $config; } $config = null; if ($settings = self::get_by_quiz_id($quizid)) { $config = $settings->get_config(); self::get_config_cache()->set($quizid, $config); } return $config; } /** * Return cached SEB config key by quiz ID. * * @param int $quizid Quiz id. * @return string|null */ public static function get_config_key_by_quiz_id(int $quizid): ?string { $configkey = self::get_config_key_cache()->get($quizid); if ($configkey !== false) { return $configkey; } $configkey = null; if ($settings = self::get_by_quiz_id($quizid)) { $configkey = $settings->get_config_key(); self::get_config_key_cache()->set($quizid, $configkey); } return $configkey; } /** * Return SEB config key cache instance. * * @return \cache_application */ private static function get_config_key_cache(): \cache_application { return \cache::make('quizaccess_seb', 'configkey'); } /** * Return SEB config cache instance. * * @return \cache_application */ private static function get_config_cache(): \cache_application { return \cache::make('quizaccess_seb', 'config'); } /** * Return quiz settings cache object, * * @return \cache_application */ private static function get_quiz_settings_cache(): \cache_application { return \cache::make('quizaccess_seb', 'quizsettings'); } /** * Adds the new record to the cache. */ protected function after_create() { $this->after_save(); } /** * Updates the cache record. * * @param bool $result */ protected function after_update($result) { $this->after_save(); } /** * Helper method to execute common stuff after create and update. */ private function after_save() { self::get_quiz_settings_cache()->set($this->get('quizid'), $this->to_record()); self::get_config_cache()->set($this->get('quizid'), $this->config); self::get_config_key_cache()->set($this->get('quizid'), $this->configkey); } /** * Removes unnecessary stuff from db. */ protected function before_delete() { $key = $this->get('quizid'); self::get_quiz_settings_cache()->delete($key); self::get_config_cache()->delete($key); self::get_config_key_cache()->delete($key); } /** * Validate the browser exam keys string. * * @param string $keys Newline separated browser exam keys. * @return true|lang_string If there is an error, an error string is returned. */ protected function validate_allowedbrowserexamkeys($keys) { $keys = $this->split_keys($keys); foreach ($keys as $i => $key) { if (!preg_match('~^[a-f0-9]{64}$~', $key)) { return new lang_string('allowedbrowserkeyssyntax', 'quizaccess_seb'); } } if (count($keys) != count(array_unique($keys))) { return new lang_string('allowedbrowserkeysdistinct', 'quizaccess_seb'); } return true; } /** * Get the browser exam keys as a pre-split array instead of just as a string. * * @return array */ protected function get_allowedbrowserexamkeys(): array { $keysstring = $this->raw_get('allowedbrowserexamkeys'); $keysstring = empty($keysstring) ? '' : $keysstring; return $this->split_keys($keysstring); } /** * Hook to execute before an update. * * Please note that at this stage the data has already been validated and therefore * any new data being set will not be validated before it is sent to the database. */ protected function before_update() { $this->before_save(); } /** * Hook to execute before a create. * * Please note that at this stage the data has already been validated and therefore * any new data being set will not be validated before it is sent to the database. */ protected function before_create() { $this->before_save(); } /** * As there is no hook for before both create and update, this function is called by both hooks. */ private function before_save() { // Set template to 0 if using anything different to template. if ($this->get('requiresafeexambrowser') != settings_provider::USE_SEB_TEMPLATE) { $this->set('templateid', 0); } // Process configs to make sure that all data is set correctly. $this->process_configs(); } /** * Before validate hook. */ protected function before_validate() { // Template can't be null. if (is_null($this->raw_get('templateid'))) { $this->set('templateid', 0); } } /** * Create or update the config string based on the current quiz settings. */ private function process_configs() { switch ($this->get('requiresafeexambrowser')) { case settings_provider::USE_SEB_NO: $this->process_seb_config_no(); break; case settings_provider::USE_SEB_CONFIG_MANUALLY: $this->process_seb_config_manually(); break; case settings_provider::USE_SEB_TEMPLATE: $this->process_seb_template(); break; case settings_provider::USE_SEB_UPLOAD_CONFIG: $this->process_seb_upload_config(); break; default: // Also settings_provider::USE_SEB_CLIENT_CONFIG. $this->process_seb_client_config(); } // Generate config key based on given SEB config. if (!empty($this->config)) { $this->configkey = config_key::generate($this->config)->get_hash(); } else { $this->configkey = null; } } /** * Return SEB config key. * * @return string|null */ public function get_config_key(): ?string { $this->process_configs(); return $this->configkey; } /** * Return string representation of the config. * * @return string|null */ public function get_config(): ?string { $this->process_configs(); return $this->config; } /** * Case for USE_SEB_NO. */ private function process_seb_config_no() { $this->config = null; } /** * Case for USE_SEB_CONFIG_MANUALLY. This creates a plist and applies all settings from the posted form, along with * some defaults. */ private function process_seb_config_manually() { // If at any point a configuration file has been uploaded and parsed, clear the settings. $this->plist = new property_list(); $this->process_bool_settings(); $this->process_quit_password_settings(); $this->process_quit_url_from_settings(); $this->process_url_filters(); $this->process_required_enforced_settings(); // One of the requirements for USE_SEB_CONFIG_MANUALLY is setting examSessionClearCookiesOnStart to false. $this->plist->set_or_update_value('examSessionClearCookiesOnStart', new CFBoolean(false)); $this->plist->set_or_update_value('allowPreferencesWindow', new CFBoolean(false)); $this->config = $this->plist->to_xml(); } /** * Case for USE_SEB_TEMPLATE. This creates a plist from the template uploaded, then applies the quit password * setting and some defaults. */ private function process_seb_template() { $template = template::get_record(['id' => $this->get('templateid')]); $this->plist = new property_list($template->get('content')); $this->process_bool_setting('allowuserquitseb'); $this->process_quit_password_settings(); $this->process_quit_url_from_template_or_config(); $this->process_required_enforced_settings(); $this->config = $this->plist->to_xml(); } /** * Case for USE_SEB_UPLOAD_CONFIG. This creates a plist from an uploaded configuration file, then applies the quiz * password settings and some defaults. */ private function process_seb_upload_config() { $file = settings_provider::get_module_context_sebconfig_file($this->get('cmid')); // If there was no file, create an empty plist so the rest of this wont explode. if (empty($file)) { throw new moodle_exception('noconfigfilefound', 'quizaccess_seb', '', $this->get('cmid')); } else { $this->plist = new property_list($file->get_content()); } $this->process_quit_url_from_template_or_config(); $this->process_required_enforced_settings(); $this->config = $this->plist->to_xml(); } /** * Case for USE_SEB_CLIENT_CONFIG. This creates an empty plist to remove the config stored. */ private function process_seb_client_config() { $this->config = null; } /** * Sets or updates some sensible default settings, these are the items 'startURL' and 'sendBrowserExamKey'. */ private function process_required_enforced_settings() { global $CFG; $quizurl = new moodle_url($CFG->wwwroot . "/mod/quiz/view.php", ['id' => $this->get('cmid')]); $this->plist->set_or_update_value('startURL', new CFString($quizurl->out(true))); $this->plist->set_or_update_value('sendBrowserExamKey', new CFBoolean(true)); // Use the modern WebView and JS API if the SEB version supports it. // Documentation: https://safeexambrowser.org/developer/seb-config-key.html . // "Set the key browserWindowWebView to the policy "Prefer Modern" (value 3)". $this->plist->set_or_update_value('browserWindowWebView', new CFNumber(3)); } /** * Use the boolean map to add Moodle boolean setting to config PList. */ private function process_bool_settings() { $settings = $this->to_record(); $map = $this->get_bool_seb_setting_map(); foreach ($settings as $setting => $value) { if (isset($map[$setting])) { $this->process_bool_setting($setting); } } } /** * Process provided single bool setting. * * @param string $name Setting name matching one from self::get_bool_seb_setting_map. */ private function process_bool_setting(string $name) { $map = $this->get_bool_seb_setting_map(); if (!isset($map[$name])) { throw new \coding_exception('Provided setting name can not be found in known bool settings'); } $enabled = $this->raw_get($name) == 1 ? true : false; $this->plist->set_or_update_value($map[$name], new CFBoolean($enabled)); } /** * Turn hashed quit password and quit link into PList strings and add to config PList. */ private function process_quit_password_settings() { $settings = $this->to_record(); if (!empty($settings->quitpassword) && is_string($settings->quitpassword)) { // Hash quit password. $hashedpassword = hash('SHA256', $settings->quitpassword); $this->plist->add_element_to_root('hashedQuitPassword', new CFString($hashedpassword)); } else if (!is_null($this->plist->get_element_value('hashedQuitPassword'))) { $this->plist->delete_element('hashedQuitPassword'); } } /** * Sets the quitURL if found in the seb_quiz_settings. */ private function process_quit_url_from_settings() { $settings = $this->to_record(); if (!empty($settings->linkquitseb) && is_string($settings->linkquitseb)) { $this->plist->set_or_update_value('quitURL', new CFString($settings->linkquitseb)); } } /** * Sets the quiz_setting's linkquitseb if a quitURL value was found in a template or uploaded config. */ private function process_quit_url_from_template_or_config() { // Does the plist (template or config file) have an existing quitURL? $quiturl = $this->plist->get_element_value('quitURL'); if (!empty($quiturl)) { $this->set('linkquitseb', $quiturl); } } /** * Turn return separated strings for URL filters into a PList array and add to config PList. */ private function process_url_filters() { $settings = $this->to_record(); // Create rules to each expression provided and add to config. $urlfilterrules = []; // Get all rules separated by newlines and remove empty rules. $expallowed = array_filter(explode(PHP_EOL, $settings->expressionsallowed)); $expblocked = array_filter(explode(PHP_EOL, $settings->expressionsblocked)); $regallowed = array_filter(explode(PHP_EOL, $settings->regexallowed)); $regblocked = array_filter(explode(PHP_EOL, $settings->regexblocked)); foreach ($expallowed as $rulestring) { $urlfilterrules[] = $this->create_filter_rule($rulestring, true, false); } foreach ($expblocked as $rulestring) { $urlfilterrules[] = $this->create_filter_rule($rulestring, false, false); } foreach ($regallowed as $rulestring) { $urlfilterrules[] = $this->create_filter_rule($rulestring, true, true); } foreach ($regblocked as $rulestring) { $urlfilterrules[] = $this->create_filter_rule($rulestring, false, true); } $this->plist->add_element_to_root('URLFilterRules', new CFArray($urlfilterrules)); } /** * Create a CFDictionary represeting a URL filter rule. * * @param string $rulestring The expression to filter with. * @param bool $allowed Allowed or blocked. * @param bool $isregex Regex or simple. * @return CFDictionary A PList dictionary. */ private function create_filter_rule(string $rulestring, bool $allowed, bool $isregex): CFDictionary { $action = $allowed ? 1 : 0; return new CFDictionary([ 'action' => new CFNumber($action), 'active' => new CFBoolean(true), 'expression' => new CFString(trim($rulestring)), 'regex' => new CFBoolean($isregex), ]); } /** * Map the settings that are booleans to the Safe Exam Browser config keys. * * @return array Moodle setting as key, SEB setting as value. */ private function get_bool_seb_setting_map(): array { return [ 'activateurlfiltering' => 'URLFilterEnable', 'allowspellchecking' => 'allowSpellCheck', 'allowreloadinexam' => 'browserWindowAllowReload', 'allowuserquitseb' => 'allowQuit', 'enableaudiocontrol' => 'audioControlEnabled', 'allowcapturecamera' => 'browserMediaCaptureCamera', 'allowcapturemicrophone' => 'browserMediaCaptureMicrophone', 'filterembeddedcontent' => 'URLFilterEnableContentFilter', 'muteonstartup' => 'audioMute', 'showkeyboardlayout' => 'showInputLanguage', 'showreloadbutton' => 'showReloadButton', 'showsebtaskbar' => 'showTaskBar', 'showtime' => 'showTime', 'showwificontrol' => 'allowWlan', 'userconfirmquit' => 'quitURLConfirm', ]; } /** * This helper method takes list of browser exam keys in a string and splits it into an array of separate keys. * * @param string|null $keys the allowed keys. * @return array of string, the separate keys. */ private function split_keys($keys): array { $keys = preg_split('~[ \t\n\r,;]+~', $keys ?? '', -1, PREG_SPLIT_NO_EMPTY); foreach ($keys as $i => $key) { $keys[$i] = strtolower($key); } return $keys; } } PK\m@@classes/property_list.phpnu[. /** * Wrapper for CFPropertyList to handle low level iteration. * * @package quizaccess_seb * @author Andrew Madden * @copyright 2019 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace quizaccess_seb; use CFPropertyList\CFArray; use CFPropertyList\CFBoolean; use CFPropertyList\CFData; use CFPropertyList\CFDate; use CFPropertyList\CFDictionary; use CFPropertyList\CFNumber; use CFPropertyList\CFPropertyList; use CFPropertyList\CFString; use CFPropertyList\CFType; use \Collator; use \DateTime; /** * Wrapper for CFPropertyList to handle low level iteration. * * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class property_list { /** A random 4 character unicode string to replace backslashes during json_encode. */ private const BACKSLASH_SUBSTITUTE = "ؼҷҍԴ"; /** @var CFPropertyList $cfpropertylist */ private $cfpropertylist; /** * property_list constructor. * * @param string $xml A Plist XML string. */ public function __construct(string $xml = '') { $this->cfpropertylist = new CFPropertyList(); if (empty($xml)) { // If xml not provided, create a blank PList with root dictionary set up. $this->cfpropertylist->add(new CFDictionary([])); } else { // Parse the XML into a PList object. $this->cfpropertylist->parse($xml, CFPropertyList::FORMAT_XML); } } /** * Add a new element to the root dictionary element. * * @param string $key Key to assign to new element. * @param CFType $element The new element. May be a collection such as an array. */ public function add_element_to_root(string $key, CFType $element) { // Get the PList's root dictionary and add new element. $this->cfpropertylist->getValue()->add($key, $element); } /** * Get value of element identified by key. * * @param string $key Key of element. * @return mixed Value of element found, or null if none found. */ public function get_element_value(string $key) { $result = null; $this->plist_map( function($elvalue, $elkey, $parent) use ($key, &$result) { // Convert date to iso 8601 if date object. if ($key === $elkey) { $result = $elvalue->getValue(); } }, $this->cfpropertylist->getValue()); if (is_array($result)) { // Turn CFType elements in PHP elements. $result = $this->array_serialize_cftypes($result); } return $result; } /** * Update the value of any element with matching key. * * Only allow string, number and boolean elements to be updated. * * @param string $key Key of element to update. * @param mixed $value Value to update element with. */ public function update_element_value(string $key, $value) { if (is_array($value)) { throw new \invalid_parameter_exception('Use update_element_array to update a collection.'); } $this->plist_map( function($elvalue, $elkey, $parent) use ($key, $value) { // Set new value. if ($key === $elkey) { $element = $parent->get($elkey); // Limit update to boolean and strings types, and check value matches expected type. if (($element instanceof CFString && is_string($value)) || ($element instanceof CFNumber && is_numeric($value)) || ($element instanceof CFBoolean && is_bool($value))) { $element->setValue($value); } else { throw new \invalid_parameter_exception( 'Only string, number and boolean elements can be updated, or value type does not match element type: ' . get_class($element)); } } }, $this->cfpropertylist->getValue()); } /** * Update the array of any dict or array element with matching key. * * Will replace array. * * @param string $key Key of element to update. * @param array $value Array to update element with. */ public function update_element_array(string $key, array $value) { // Validate new array. foreach ($value as $element) { // If any element is not a CFType instance, then throw exception. if (!($element instanceof CFType)) { throw new \invalid_parameter_exception('New array must only contain CFType objects.'); } } $this->plist_map( function($elvalue, $elkey, $parent) use ($key, $value) { if ($key === $elkey) { $element = $parent->get($elkey); // Replace existing element with new element and array but same key. if ($element instanceof CFDictionary) { $parent->del($elkey); $parent->add($elkey, new CFDictionary($value)); } else if ($element instanceof CFArray) { $parent->del($elkey); $parent->add($elkey, new CFArray($value)); } } }, $this->cfpropertylist->getValue()); } /** * Delete any element with a matching key. * * @param string $key Key of element to delete. */ public function delete_element(string $key) { $this->plist_map( function($elvalue, $elkey, $parent) use ($key) { // Convert date to iso 8601 if date object. if ($key === $elkey) { $parent->del($key); } }, $this->cfpropertylist->getValue()); } /** * Helper function to either set or update a CF type value to the plist. * * @param string $key * @param CFType $input */ public function set_or_update_value(string $key, CFType $input) { $value = $this->get_element_value($key); if (empty($value)) { $this->add_element_to_root($key, $input); } else { $this->update_element_value($key, $input->getValue()); } } /** * Convert the PList to XML. * * @return string XML ready for creating an XML file. */ public function to_xml(): string { return $this->cfpropertylist->toXML(); } /** * Return a JSON representation of the PList. The JSON is constructed to be used to generate a SEB Config Key. * * See the developer documention for SEB for more information on the requirements on generating a SEB Config Key. * https://safeexambrowser.org/developer/seb-config-key.html * * 1. Don't add any whitespace or line formatting to the SEB-JSON string. * 2. Don't add character escaping (also backshlashes "\" as found in URL filter rules should not be escaped). * 3. All elements from the plist XML must be ordered (alphabetically sorted) by their key names. Use a * recursive method to apply ordering also to nested dictionaries contained in the root-level dictionary and in * arrays. Use non-localized (culture invariant), non-ASCII value based case insensitive ordering. For example the * key allowWlan comes before allowWLAN. Cocoa/Obj-C and .NET/C# usually use this case * insensitive ordering as default, but PHP for example doesn't. * 4. Remove empty elements (key/value). Current versions of SEB clients should anyways not generate empty * dictionaries, but this was possible with outdated versions. If config files have been generated that time, such * elements might still be around. * 5. All string elements must be UTF8 encoded. * 6. Base16 strings should use lower-case a-f characters, even though this isn't relevant in the current * implementation of the Config Key calculation. * 7. plist XML elements must be converted to Base64 strings. * 8. plist XML elements must be converted to ISO 8601 formatted strings. * * @return string A json encoded string. */ public function to_json(): string { // Create a clone of the PList, so main list isn't mutated. $jsonplist = new CFPropertyList(); $jsonplist->parse($this->cfpropertylist->toXML(), CFPropertyList::FORMAT_XML); // Pass root dict to recursively convert dates to ISO 8601 format, encode strings to UTF-8, // lock data to Base 64 encoding and remove empty dictionaries. $this->prepare_plist_for_json_encoding($jsonplist->getValue()); // Serialize PList to array. $plistarray = $jsonplist->toArray(); // Sort array alphabetically by key using case insensitive, natural sorting. See point 3 for more information. $plistarray = $this->array_sort($plistarray); // Encode in JSON with following rules from SEB docs. // 1. Don't add any whitespace or line formatting to the SEB-JSON string. // 2. Don't add unicode or slash escaping. $json = json_encode($plistarray, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); // There is no way to prevent json_encode from escaping backslashes. We replace each backslash with a unique string // prior to encoding in prepare_plist_for_json_encoding(). We can then replace the substitute with a single backslash. $json = str_replace(self::BACKSLASH_SUBSTITUTE, "\\", $json); return $json; } /** * Recursively convert PList date values from unix to iso 8601 format, and ensure strings are UTF 8 encoded. * * This will mutate the PList. */ /** * Recursively convert PList date values from unix to iso 8601 format, and ensure strings are UTF 8 encoded. * * This will mutate the PList. * @param \Iterator $root The root element of the PList. Must be a dictionary or array. */ private function prepare_plist_for_json_encoding($root) { $this->plist_map( function($value, $key, $parent) { // Convert date to ISO 8601 if date object. if ($value instanceof CFDate) { $date = DateTime::createFromFormat('U', $value->getValue()); $date->setTimezone(new \DateTimeZone('UTC')); // Zulu timezone a.k.a. UTC+00. $isodate = $date->format('c'); $value->setValue($isodate); } // Make sure strings are UTF 8 encoded. if ($value instanceof CFString) { // As literal backslashes will be lost during encoding, we must replace them with a unique substitute to be // reverted after JSON encoding. $string = str_replace("\\", self::BACKSLASH_SUBSTITUTE, $value->getValue()); $value->setValue(mb_convert_encoding($string, 'UTF-8')); } // Data should remain base 64 encoded, so convert to base encoded string for export. Otherwise // CFData will decode the data when serialized. if ($value instanceof CFData) { $data = trim($value->getCodedValue()); $parent->del($key); $parent->add($key, new CFString($data)); } // Empty dictionaries should be removed. if ($value instanceof CFDictionary && empty($value->getValue())) { $parent->del($key); } }, $root); } /** * Iterate through the PList elements, and call the callback on each. * * @param callable $callback A callback function called for every element. * @param \Iterator $root The root element of the PList. Must be a dictionary or array. * @param bool $recursive Whether the function should traverse dicts and arrays recursively. */ private function plist_map(callable $callback, \Iterator $root, bool $recursive = true) { $root->rewind(); while ($root->valid()) { $value = $root->current(); $key = $root->key(); // Recursively traverse all dicts and arrays if flag is true. if ($recursive && $value instanceof \Iterator) { $this->plist_map($callback, $value); } // Callback function called for every element. $callback($value, $key, $root); $root->next(); } } /** * Recursively sort array alphabetically by key. * * @link https://safeexambrowser.org/developer/seb-config-key.html * * @param array $array Top level array to process. * @return array Processed array. */ private function array_sort(array $array) { foreach ($array as $key => $value) { if (is_array($value)) { $array[$key] = $this->array_sort($array[$key]); } } // Sort assoc array. From SEB docs: // // All elements from the plist XML must be ordered (alphabetically sorted) by their key names. Use // a recursive method to apply ordering also to nested dictionaries contained in the root-level dictionary // and in arrays. Use non-localized (culture invariant), non-ASCII value based case insensitive ordering. // For example the key allowWlan comes before allowWLAN. Cocoa/Obj-C and .NET/C# // usually use this case insensitive ordering as default, but PHP for example doesn't. if ($this->is_associative_array($array)) { // Note this is a pragmatic solution as none of the native PHP *sort method appear to sort strings that // differ only in case (e.g. ["allowWLAN", "allowWlan"] is expected to have the lower version first). $keys = array_keys($array); (new Collator('root'))->asort($keys); // Use Unicode Collation Algorithm (UCA). $original = $array; $array = []; foreach ($keys as $key) { $array[$key] = $original[$key]; } } return $array; } /** * Recursively remove empty arrays. * * @param array $array Top level array to process. * @return array Processed array. */ private function array_remove_empty_arrays(array $array) { foreach ($array as $key => $value) { if (is_array($value)) { $array[$key] = $this->array_remove_empty_arrays($array[$key]); } // Remove empty arrays. if (is_array($array[$key]) && empty($array[$key])) { unset($array[$key]); } } return $array; } /** * If an array contains CFType objects, wrap array in a CFDictionary to allow recursive serialization of data * into a standard PHP array. * * @param array $array Array containing CFType objects. * @return array Standard PHP array. */ private function array_serialize_cftypes(array $array): array { $array = new CFDictionary($array); // Convert back to CFDictionary so serialization is recursive. return $array->toArray(); // Serialize. } /** * Check if an array is associative or sequential. * * @param array $array Array to check. * @return bool False if not associative. */ private function is_associative_array(array $array) { if (empty($array)) { return false; } // Check that all keys are not sequential integers starting from 0 (Which is what PHP arrays have behind the scenes.) return array_keys($array) !== range(0, count($array) - 1); } } PK\ohYYclasses/template.phpnu[. /** * Entity model representing template settings for the seb plugin. * * @package quizaccess_seb * @author Nicholas Hoobin * @author Dmitrii Metelkin * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace quizaccess_seb; use core\persistent; defined('MOODLE_INTERNAL') || die(); /** * Entity model representing template settings for the seb plugin. * * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class template extends persistent { /** Table name for the persistent. */ const TABLE = 'quizaccess_seb_template'; /** @var property_list $plist The SEB config represented as a Property List object. */ private $plist; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return [ 'name' => [ 'type' => PARAM_TEXT, 'default' => '', ], 'description' => [ 'type' => PARAM_TEXT, 'default' => '', ], 'content' => [ 'type' => PARAM_RAW, ], 'enabled' => [ 'type' => PARAM_INT, 'default' => 0, ], 'sortorder' => [ 'type' => PARAM_INT, 'default' => 0, ], ]; } /** * Hook to execute before an update. * * Please note that at this stage the data has already been validated and therefore * any new data being set will not be validated before it is sent to the database. */ protected function before_update() { $this->before_save(); } /** * Hook to execute before a create. * * Please note that at this stage the data has already been validated and therefore * any new data being set will not be validated before it is sent to the database. */ protected function before_create() { $this->before_save(); } /** * As there is no hook for before both create and update, this function is called by both hooks. */ private function before_save() { $this->plist = new property_list($this->get('content')); $this->set('content', $this->plist->to_xml()); } /** * Validate template content. * * @param string $content Content string to validate. * * @return bool|\lang_string */ protected function validate_content(string $content) { if (helper::is_valid_seb_config($content)) { return true; } else { return new \lang_string('invalidtemplate', 'quizaccess_seb'); } } /** * Check if we can delete the template. * * @return bool */ public function can_delete(): bool { $result = true; if ($this->get('id')) { $settings = seb_quiz_settings::get_records(['templateid' => $this->get('id')]); $result = empty($settings); } return $result; } } PK\2BFpkktemplates/loading.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template quizaccess_seb/loading Loading container. Example context (json): { "extraclasses": "class" } }} PK\7K amd/src/view.jsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * Manage the quiz views. * * @module quizaccess_seb/view * @author Andrew Madden * @copyright 2021 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import Notification from "core/notification"; import * as Templates from "core/templates"; import * as Str from "core/str"; import ModalAlert from "core/local/modal/alert"; /** @var SELECTOR List of CSS selectors. */ const SELECTOR = { MAIN: '#region-main', LOADING: '.seb-loading', }; /** @var Template List of mustache templates. */ const TEMPLATE = { LOADING: 'quizaccess_seb/loading', }; /** * Manages view when access has been granted. */ export const allowAccess = () => { window.location.reload(); }; /** * Add an alert to page to inform that Safe Exam Browser access is being checked. * * @return {Promise} */ export const addLoadingAlert = () => { return Templates.render(TEMPLATE.LOADING, {}).then((html, js) => { const alertRegion = window.document.querySelector(SELECTOR.MAIN); return Templates.prependNodeContents(alertRegion, html, js); }).catch(Notification.exception); }; /** * Remove the Safe Exam Browser access check alert from the page. */ export const clearLoadingAlert = () => { const alert = window.document.querySelector(SELECTOR.LOADING); if (alert) { Templates.replaceNode(alert, '', ''); } }; /** * Display validation failed modal. */ export const showValidationFailedModal = () => { ModalAlert.create({ title: Str.get_string('sebkeysvalidationfailed', 'quizaccess_seb'), body: Str.get_string('invalidkeys', 'quizaccess_seb'), large: false, show: true, }).catch(Notification.exception); }; PK\amd/src/validate_quiz_access.jsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * Validate Safe Exam Browser access keys. * * @module quizaccess_seb/validate_quiz_access * @author Andrew Madden * @copyright 2021 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import Ajax from 'core/ajax'; import Config from 'core/config'; import Notification from "core/notification"; import * as View from 'quizaccess_seb/view'; // SafeExamBrowser object will be automatically initialized if using the SafeExamBrowser application. window.SafeExamBrowser = window.SafeExamBrowser || null; /** * Once the keys are fetched, action checking access. * * @param {init} cmid Value of course module id of the quiz. * @param {boolean} autoreconfigure Value of Moodle setting: quizaccess_seb/autoreconfigureseb. */ const safeExamBrowserKeysUpdated = (cmid, autoreconfigure = false) => { // Action opening up the quiz. isQuizAccessValid(cmid).then((response) => { // Show the alert for an extra second to allow user to see it. setTimeout(View.clearLoadingAlert, 1000); if (response.configkey && response.browserexamkey) { View.allowAccess(); } else { // If autoreconfigureseb is enabled, attempt to reconfigure page with quiz settings. if (autoreconfigure === true && response.configkey === false) { reconfigureSafeExamBrowser(cmid); } setTimeout(View.showValidationFailedModal, 1000); } return response; }).catch(err => { Notification.exception(err); }); }; /** * Validate keys in Moodle backend. * * @param {init} cmid Value of course module id of the quiz. * @return {Promise} */ const isQuizAccessValid = (cmid) => { const request = { methodname: 'quizaccess_seb_validate_quiz_keys', args: { cmid: cmid, url: window.location.href, configkey: window.SafeExamBrowser.security.configKey, browserexamkey: window.SafeExamBrowser.security.browserExamKey }, }; return Ajax.call([request])[0]; }; /** * Check if the key is not yet set. * * @param {string} key config key or browser exam key. * @return {boolean} */ const isKeyEmpty = (key) => { // If the SafeExamBrowser object is defined, the default 'empty' value of the configKey and browserExamKey is ':'. return key === ":"; }; /** * Reload Safe Exam Browser with current quiz configuration. * * @param {init} cmid Value of course module id of the quiz. */ const reconfigureSafeExamBrowser = (cmid) => { const domain = Config.wwwroot.replace(/^http/i, 'seb'); const redirecturl = domain + '/mod/quiz/accessrule/seb/config.php?cmid=' + cmid; document.location.replace(redirecturl); }; /** * Initialize the process of fetching the keys. * * @param {init} cmid Value of course module id of the quiz. * @param {boolean} autoreconfigure Value of Moodle setting: quizaccess_seb/autoreconfigureseb. */ export const init = async(cmid, autoreconfigure = false) => { // If the SafeExamBrowser object is instantiated, try and use it to fetch the access keys. if (window.SafeExamBrowser !== null) { await View.addLoadingAlert(); // If the SEB keys are already set, we can call our callback directly. if (!isKeyEmpty(window.SafeExamBrowser.security.configKey) || !isKeyEmpty(window.SafeExamBrowser.security.browserExamKey)) { safeExamBrowserKeysUpdated(cmid, autoreconfigure); } else { window.SafeExamBrowser.security.updateKeys(safeExamBrowserKeysUpdated); } } }; PK\oNamd/src/managetemplates.jsnu[/** * Template management code. * * @module quizaccess_seb/managetemplates * @copyright 2020 Dmitrii Metelkin */ define( ['jquery', 'core/ajax', 'core/str', 'core/notification'], function($, ajax, str, notification) { var manager = { /** * Confirm removal of the specified template. * * @method removeTemplate * @param {EventFacade} e The EventFacade */ removeTemplate: function(e) { e.preventDefault(); var targetUrl = $(e.currentTarget).attr('href'); str.get_strings([ { key: 'confirmtemplateremovaltitle', component: 'quizaccess_seb' }, { key: 'confirmtemplateremovalquestion', component: 'quizaccess_seb' }, { key: 'yes', component: 'moodle' }, { key: 'no', component: 'moodle' } ]) .then(function(s) { notification.confirm(s[0], s[1], s[2], s[3], function() { window.location = targetUrl; }); return; }) .catch(); }, /** * Setup the template management UI. * * @method setup */ setup: function() { $('body').delegate('[data-action="delete"]', 'click', manager.removeTemplate); } }; return /** @alias module:quizaccess_seb/managetemplates */ { /** * Setup the template management UI. * * @method setup */ setup: manager.setup }; }); PK\HD $amd/build/managetemplates.min.js.mapnu[{"version":3,"file":"managetemplates.min.js","sources":["../src/managetemplates.js"],"sourcesContent":["/**\n * Template management code.\n *\n * @module quizaccess_seb/managetemplates\n * @copyright 2020 Dmitrii Metelkin \n */\ndefine(\n ['jquery', 'core/ajax', 'core/str', 'core/notification'],\n function($, ajax, str, notification) {\n var manager = {\n /**\n * Confirm removal of the specified template.\n *\n * @method removeTemplate\n * @param {EventFacade} e The EventFacade\n */\n removeTemplate: function(e) {\n e.preventDefault();\n var targetUrl = $(e.currentTarget).attr('href');\n str.get_strings([\n {\n key: 'confirmtemplateremovaltitle',\n component: 'quizaccess_seb'\n },\n {\n key: 'confirmtemplateremovalquestion',\n component: 'quizaccess_seb'\n },\n {\n key: 'yes',\n component: 'moodle'\n },\n {\n key: 'no',\n component: 'moodle'\n }\n ])\n .then(function(s) {\n notification.confirm(s[0], s[1], s[2], s[3], function() {\n window.location = targetUrl;\n });\n\n return;\n })\n .catch();\n },\n\n /**\n * Setup the template management UI.\n *\n * @method setup\n */\n setup: function() {\n $('body').delegate('[data-action=\"delete\"]', 'click', manager.removeTemplate);\n }\n };\n\n return /** @alias module:quizaccess_seb/managetemplates */ {\n /**\n * Setup the template management UI.\n *\n * @method setup\n */\n setup: manager.setup\n };\n });\n"],"names":["define","$","ajax","str","notification","manager","removeTemplate","e","preventDefault","targetUrl","currentTarget","attr","get_strings","key","component","then","s","confirm","window","location","catch","setup","delegate"],"mappings":"AAMAA,wCACI,CAAC,SAAU,YAAa,WAAY,sBACpC,SAASC,EAAGC,KAAMC,IAAKC,kBACfC,QAAU,CAOVC,eAAgB,SAASC,GACrBA,EAAEC,qBACEC,UAAYR,EAAEM,EAAEG,eAAeC,KAAK,QACxCR,IAAIS,YAAY,CACZ,CACIC,IAAY,8BACZC,UAAY,kBAEhB,CACID,IAAY,iCACZC,UAAY,kBAEhB,CACID,IAAY,MACZC,UAAY,UAEhB,CACID,IAAY,KACZC,UAAY,YAGnBC,MAAK,SAASC,GACXZ,aAAaa,QAAQD,EAAE,GAAIA,EAAE,GAAIA,EAAE,GAAIA,EAAE,IAAI,WACzCE,OAAOC,SAAWV,gBAKzBW,SAQLC,MAAO,WACHpB,EAAE,QAAQqB,SAAS,yBAA0B,QAASjB,QAAQC,wBAIX,CAMvDe,MAAOhB,QAAQgB"}PK\i)amd/build/validate_quiz_access.min.js.mapnu[{"version":3,"file":"validate_quiz_access.min.js","sources":["../src/validate_quiz_access.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Validate Safe Exam Browser access keys.\n *\n * @module quizaccess_seb/validate_quiz_access\n * @author Andrew Madden \n * @copyright 2021 Catalyst IT\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport Config from 'core/config';\nimport Notification from \"core/notification\";\nimport * as View from 'quizaccess_seb/view';\n\n// SafeExamBrowser object will be automatically initialized if using the SafeExamBrowser application.\nwindow.SafeExamBrowser = window.SafeExamBrowser || null;\n\n/**\n * Once the keys are fetched, action checking access.\n *\n * @param {init} cmid Value of course module id of the quiz.\n * @param {boolean} autoreconfigure Value of Moodle setting: quizaccess_seb/autoreconfigureseb.\n */\nconst safeExamBrowserKeysUpdated = (cmid, autoreconfigure = false) => {\n // Action opening up the quiz.\n isQuizAccessValid(cmid).then((response) => {\n // Show the alert for an extra second to allow user to see it.\n setTimeout(View.clearLoadingAlert, 1000);\n\n if (response.configkey && response.browserexamkey) {\n View.allowAccess();\n } else {\n // If autoreconfigureseb is enabled, attempt to reconfigure page with quiz settings.\n if (autoreconfigure === true && response.configkey === false) {\n reconfigureSafeExamBrowser(cmid);\n }\n setTimeout(View.showValidationFailedModal, 1000);\n }\n\n return response;\n }).catch(err => {\n Notification.exception(err);\n });\n};\n\n/**\n * Validate keys in Moodle backend.\n *\n * @param {init} cmid Value of course module id of the quiz.\n * @return {Promise}\n */\nconst isQuizAccessValid = (cmid) => {\n const request = {\n methodname: 'quizaccess_seb_validate_quiz_keys',\n args: {\n cmid: cmid,\n url: window.location.href,\n configkey: window.SafeExamBrowser.security.configKey,\n browserexamkey: window.SafeExamBrowser.security.browserExamKey\n },\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Check if the key is not yet set.\n *\n * @param {string} key config key or browser exam key.\n * @return {boolean}\n */\nconst isKeyEmpty = (key) => {\n // If the SafeExamBrowser object is defined, the default 'empty' value of the configKey and browserExamKey is ':'.\n return key === \":\";\n};\n\n/**\n * Reload Safe Exam Browser with current quiz configuration.\n *\n * @param {init} cmid Value of course module id of the quiz.\n */\nconst reconfigureSafeExamBrowser = (cmid) => {\n const domain = Config.wwwroot.replace(/^http/i, 'seb');\n const redirecturl = domain + '/mod/quiz/accessrule/seb/config.php?cmid=' + cmid;\n document.location.replace(redirecturl);\n};\n\n/**\n * Initialize the process of fetching the keys.\n *\n * @param {init} cmid Value of course module id of the quiz.\n * @param {boolean} autoreconfigure Value of Moodle setting: quizaccess_seb/autoreconfigureseb.\n */\nexport const init = async(cmid, autoreconfigure = false) => {\n // If the SafeExamBrowser object is instantiated, try and use it to fetch the access keys.\n if (window.SafeExamBrowser !== null) {\n await View.addLoadingAlert();\n // If the SEB keys are already set, we can call our callback directly.\n\n if (!isKeyEmpty(window.SafeExamBrowser.security.configKey) || !isKeyEmpty(window.SafeExamBrowser.security.browserExamKey)) {\n safeExamBrowserKeysUpdated(cmid, autoreconfigure);\n } else {\n window.SafeExamBrowser.security.updateKeys(safeExamBrowserKeysUpdated);\n }\n }\n};\n"],"names":["window","SafeExamBrowser","safeExamBrowserKeysUpdated","cmid","autoreconfigure","isQuizAccessValid","then","response","setTimeout","View","clearLoadingAlert","configkey","browserexamkey","allowAccess","reconfigureSafeExamBrowser","showValidationFailedModal","catch","err","exception","request","methodname","args","url","location","href","security","configKey","browserExamKey","Ajax","call","isKeyEmpty","key","redirecturl","Config","wwwroot","replace","document","async","addLoadingAlert","updateKeys"],"mappings":";;;;;;;;02BA8BAA,OAAOC,gBAAkBD,OAAOC,iBAAmB,WAQ7CC,2BAA6B,SAACC,UAAMC,wEAEtCC,kBAAkBF,MAAMG,MAAMC,WAE1BC,WAAWC,KAAKC,kBAAmB,KAE/BH,SAASI,WAAaJ,SAASK,eAC/BH,KAAKI,gBAGmB,IAApBT,kBAAmD,IAAvBG,SAASI,WACrCG,2BAA2BX,MAE/BK,WAAWC,KAAKM,0BAA2B,MAGxCR,YACRS,OAAMC,4BACQC,UAAUD,SAUzBZ,kBAAqBF,aACjBgB,QAAU,CACZC,WAAY,oCACZC,KAAM,CACFlB,KAAMA,KACNmB,IAAKtB,OAAOuB,SAASC,KACrBb,UAAWX,OAAOC,gBAAgBwB,SAASC,UAC3Cd,eAAgBZ,OAAOC,gBAAgBwB,SAASE,wBAIjDC,cAAKC,KAAK,CAACV,UAAU,IAS1BW,WAAcC,KAED,MAARA,IAQLjB,2BAA8BX,aAE1B6B,YADSC,gBAAOC,QAAQC,QAAQ,SAAU,OACnB,4CAA8ChC,KAC3EiC,SAASb,SAASY,QAAQH,4BASVK,eAAMlC,UAAMC,wEAEG,OAA3BJ,OAAOC,wBACDQ,KAAK6B,kBAGNR,WAAW9B,OAAOC,gBAAgBwB,SAASC,YAAeI,WAAW9B,OAAOC,gBAAgBwB,SAASE,gBAGtG3B,OAAOC,gBAAgBwB,SAASc,WAAWrC,4BAF3CA,2BAA2BC,KAAMC"}PK\h) amd/build/managetemplates.min.jsnu[define("quizaccess_seb/managetemplates",["jquery","core/ajax","core/str","core/notification"],(function($,ajax,str,notification){var manager={removeTemplate:function(e){e.preventDefault();var targetUrl=$(e.currentTarget).attr("href");str.get_strings([{key:"confirmtemplateremovaltitle",component:"quizaccess_seb"},{key:"confirmtemplateremovalquestion",component:"quizaccess_seb"},{key:"yes",component:"moodle"},{key:"no",component:"moodle"}]).then((function(s){notification.confirm(s[0],s[1],s[2],s[3],(function(){window.location=targetUrl}))})).catch()},setup:function(){$("body").delegate('[data-action="delete"]',"click",manager.removeTemplate)}};return{setup:manager.setup}})); //# sourceMappingURL=managetemplates.min.js.mapPK\JUm amd/build/view.min.js.mapnu[{"version":3,"file":"view.min.js","sources":["../src/view.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Manage the quiz views.\n *\n * @module quizaccess_seb/view\n * @author Andrew Madden \n * @copyright 2021 Catalyst IT\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Notification from \"core/notification\";\nimport * as Templates from \"core/templates\";\nimport * as Str from \"core/str\";\nimport ModalAlert from \"core/local/modal/alert\";\n\n/** @var SELECTOR List of CSS selectors. */\nconst SELECTOR = {\n MAIN: '#region-main',\n LOADING: '.seb-loading',\n};\n\n/** @var Template List of mustache templates. */\nconst TEMPLATE = {\n LOADING: 'quizaccess_seb/loading',\n};\n\n/**\n * Manages view when access has been granted.\n */\nexport const allowAccess = () => {\n window.location.reload();\n};\n\n/**\n * Add an alert to page to inform that Safe Exam Browser access is being checked.\n *\n * @return {Promise}\n */\nexport const addLoadingAlert = () => {\n return Templates.render(TEMPLATE.LOADING, {}).then((html, js) => {\n const alertRegion = window.document.querySelector(SELECTOR.MAIN);\n return Templates.prependNodeContents(alertRegion, html, js);\n }).catch(Notification.exception);\n};\n\n/**\n * Remove the Safe Exam Browser access check alert from the page.\n */\nexport const clearLoadingAlert = () => {\n const alert = window.document.querySelector(SELECTOR.LOADING);\n if (alert) {\n Templates.replaceNode(alert, '', '');\n }\n};\n\n/**\n * Display validation failed modal.\n */\nexport const showValidationFailedModal = () => {\n ModalAlert.create({\n title: Str.get_string('sebkeysvalidationfailed', 'quizaccess_seb'),\n body: Str.get_string('invalidkeys', 'quizaccess_seb'),\n large: false,\n show: true,\n }).catch(Notification.exception);\n};\n"],"names":["SELECTOR","TEMPLATE","window","location","reload","Templates","render","then","html","js","alertRegion","document","querySelector","prependNodeContents","catch","Notification","exception","alert","replaceNode","create","title","Str","get_string","body","large","show"],"mappings":";;;;;;;;8VA8BMA,cACI,eADJA,iBAEO,eAIPC,iBACO,8CAMc,KACvBC,OAAOC,SAASC,mCAQW,IACpBC,UAAUC,OAAOL,iBAAkB,IAAIM,MAAK,CAACC,KAAMC,YAChDC,YAAcR,OAAOS,SAASC,cAAcZ,sBAC3CK,UAAUQ,oBAAoBH,YAAaF,KAAMC,OACzDK,MAAMC,sBAAaC,sCAMO,WACvBC,MAAQf,OAAOS,SAASC,cAAcZ,kBACxCiB,OACAZ,UAAUa,YAAYD,MAAO,GAAI,wCAOA,oBAC1BE,OAAO,CACdC,MAAOC,IAAIC,WAAW,0BAA2B,kBACjDC,KAAMF,IAAIC,WAAW,cAAe,kBACpCE,OAAO,EACPC,MAAM,IACPX,MAAMC,sBAAaC"}PK\k^ ^ %amd/build/validate_quiz_access.min.jsnu[define("quizaccess_seb/validate_quiz_access",["exports","core/ajax","core/config","core/notification","quizaccess_seb/view"],(function(_exports,_ajax,_config,_notification,View){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Validate Safe Exam Browser access keys. * * @module quizaccess_seb/validate_quiz_access * @author Andrew Madden * @copyright 2021 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_ajax=_interopRequireDefault(_ajax),_config=_interopRequireDefault(_config),_notification=_interopRequireDefault(_notification),View=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(View),window.SafeExamBrowser=window.SafeExamBrowser||null;const safeExamBrowserKeysUpdated=function(cmid){let autoreconfigure=arguments.length>1&&void 0!==arguments[1]&&arguments[1];isQuizAccessValid(cmid).then((response=>(setTimeout(View.clearLoadingAlert,1e3),response.configkey&&response.browserexamkey?View.allowAccess():(!0===autoreconfigure&&!1===response.configkey&&reconfigureSafeExamBrowser(cmid),setTimeout(View.showValidationFailedModal,1e3)),response))).catch((err=>{_notification.default.exception(err)}))},isQuizAccessValid=cmid=>{const request={methodname:"quizaccess_seb_validate_quiz_keys",args:{cmid:cmid,url:window.location.href,configkey:window.SafeExamBrowser.security.configKey,browserexamkey:window.SafeExamBrowser.security.browserExamKey}};return _ajax.default.call([request])[0]},isKeyEmpty=key=>":"===key,reconfigureSafeExamBrowser=cmid=>{const redirecturl=_config.default.wwwroot.replace(/^http/i,"seb")+"/mod/quiz/accessrule/seb/config.php?cmid="+cmid;document.location.replace(redirecturl)};_exports.init=async function(cmid){let autoreconfigure=arguments.length>1&&void 0!==arguments[1]&&arguments[1];null!==window.SafeExamBrowser&&(await View.addLoadingAlert(),isKeyEmpty(window.SafeExamBrowser.security.configKey)&&isKeyEmpty(window.SafeExamBrowser.security.browserExamKey)?window.SafeExamBrowser.security.updateKeys(safeExamBrowserKeysUpdated):safeExamBrowserKeysUpdated(cmid,autoreconfigure))}})); //# sourceMappingURL=validate_quiz_access.min.js.mapPK\! 7 7 amd/build/view.min.jsnu[define("quizaccess_seb/view",["exports","core/notification","core/templates","core/str","core/local/modal/alert"],(function(_exports,_notification,Templates,Str,_alert){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Manage the quiz views. * * @module quizaccess_seb/view * @author Andrew Madden * @copyright 2021 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.showValidationFailedModal=_exports.clearLoadingAlert=_exports.allowAccess=_exports.addLoadingAlert=void 0,_notification=_interopRequireDefault(_notification),Templates=_interopRequireWildcard(Templates),Str=_interopRequireWildcard(Str),_alert=_interopRequireDefault(_alert);const SELECTOR_MAIN="#region-main",SELECTOR_LOADING=".seb-loading",TEMPLATE_LOADING="quizaccess_seb/loading";_exports.allowAccess=()=>{window.location.reload()};_exports.addLoadingAlert=()=>Templates.render(TEMPLATE_LOADING,{}).then(((html,js)=>{const alertRegion=window.document.querySelector(SELECTOR_MAIN);return Templates.prependNodeContents(alertRegion,html,js)})).catch(_notification.default.exception);_exports.clearLoadingAlert=()=>{const alert=window.document.querySelector(SELECTOR_LOADING);alert&&Templates.replaceNode(alert,"","")};_exports.showValidationFailedModal=()=>{_alert.default.create({title:Str.get_string("sebkeysvalidationfailed","quizaccess_seb"),body:Str.get_string("invalidkeys","quizaccess_seb"),large:!1,show:!0}).catch(_notification.default.exception)}})); //# sourceMappingURL=view.min.js.mapPK\g2!]!]rule.phpnu[. use mod_quiz\local\access_rule_base; use mod_quiz\quiz_attempt; use quizaccess_seb\seb_access_manager; use quizaccess_seb\seb_quiz_settings; use quizaccess_seb\settings_provider; use quizaccess_seb\event\access_prevented; /** * Implementation of the quizaccess_seb plugin. * * @package quizaccess_seb * @author Andrew Madden * @author Dmitrii Metelkin * @copyright 2019 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class quizaccess_seb extends access_rule_base { /** @var seb_access_manager $accessmanager Instance to manage the access to the quiz for this plugin. */ private $accessmanager; /** * Create an instance of this rule for a particular quiz. * * @param \mod_quiz\quiz_settings $quizobj information about the quiz in question. * @param int $timenow the time that should be considered as 'now'. * @param seb_access_manager $accessmanager the quiz accessmanager. */ public function __construct(\mod_quiz\quiz_settings $quizobj, int $timenow, seb_access_manager $accessmanager) { parent::__construct($quizobj, $timenow); $this->accessmanager = $accessmanager; } /** * Return an appropriately configured instance of this rule, if it is applicable * to the given quiz, otherwise return null. * * @param \mod_quiz\quiz_settings $quizobj information about the quiz in question. * @param int $timenow the time that should be considered as 'now'. * @param bool $canignoretimelimits whether the current user is exempt from * time limits by the mod/quiz:ignoretimelimits capability. * @return access_rule_base|null the rule, if applicable, else null. */ public static function make(\mod_quiz\quiz_settings $quizobj, $timenow, $canignoretimelimits) { $accessmanager = new seb_access_manager($quizobj); // If Safe Exam Browser is not required, this access rule is not applicable. if (!$accessmanager->seb_required()) { return null; } return new self($quizobj, $timenow, $accessmanager); } /** * Add any fields that this rule requires to the quiz settings form. This * method is called from {@link mod_quiz_mod_form::definition()}, while the * security section is being built. * * @param mod_quiz_mod_form $quizform the quiz settings form that is being built. * @param MoodleQuickForm $mform the wrapped MoodleQuickForm. */ public static function add_settings_form_fields(mod_quiz_mod_form $quizform, MoodleQuickForm $mform) { settings_provider::add_seb_settings_fields($quizform, $mform); } /** * Validate the data from any form fields added using {@link add_settings_form_fields()}. * * If the managing user cannot configure SEB by either lack of permissions or locked * settings, then the form fields will be frozen and no validation will be done. * * @param array $errors the errors found so far. * @param array $data the submitted form data. * @param array $files information about any uploaded files. * @param mod_quiz_mod_form $quizform the quiz form object. * @return array $errors the updated $errors array. */ public static function validate_settings_form_fields(array $errors, array $data, $files, mod_quiz_mod_form $quizform): array { $quizid = $data['instance']; $cmid = $data['coursemodule']; $context = $quizform->get_context(); if (!settings_provider::can_configure_seb($context)) { return $errors; } if (settings_provider::is_seb_settings_locked($quizid)) { return $errors; } if (settings_provider::is_conflicting_permissions($context)) { return $errors; } $settings = settings_provider::filter_plugin_settings((object) $data); // Validate basic settings using persistent class. $quizsettings = (new seb_quiz_settings())->from_record($settings); // Set non-form fields. $quizsettings->set('quizid', $quizid); $quizsettings->set('cmid', $cmid); $quizsettings->validate(); // Add any errors to list. foreach ($quizsettings->get_errors() as $name => $error) { $name = settings_provider::add_prefix($name); // Re-add prefix to match form element. $errors[$name] = $error->out(); } // Edge case for filemanager_sebconfig. if ($quizsettings->get('requiresafeexambrowser') == settings_provider::USE_SEB_UPLOAD_CONFIG) { $errorvalidatefile = settings_provider::validate_draftarea_configfile($data['filemanager_sebconfigfile']); if (!empty($errorvalidatefile)) { $errors['filemanager_sebconfigfile'] = $errorvalidatefile; } } // Edge case to force user to select a template. if ($quizsettings->get('requiresafeexambrowser') == settings_provider::USE_SEB_TEMPLATE) { if (empty($data['seb_templateid'])) { $errors['seb_templateid'] = get_string('invalidtemplate', 'quizaccess_seb'); } } if ($quizsettings->get('requiresafeexambrowser') != settings_provider::USE_SEB_NO) { // Global settings may be active which require a quiz password to be set if using SEB. if (!empty(get_config('quizaccess_seb', 'quizpasswordrequired')) && empty($data['quizpassword'])) { $errors['quizpassword'] = get_string('passwordnotset', 'quizaccess_seb'); } } return $errors; } /** * Save any submitted settings when the quiz settings form is submitted. This * is called from {@link quiz_after_add_or_update()} in lib.php. * * @param stdClass $quiz the data from the quiz form, including $quiz->id * which is the id of the quiz being saved. */ public static function save_settings($quiz) { $context = context_module::instance($quiz->coursemodule); if (!settings_provider::can_configure_seb($context)) { return; } if (settings_provider::is_seb_settings_locked($quiz->id)) { return; } if (settings_provider::is_conflicting_permissions($context)) { return; } $cm = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course, false, MUST_EXIST); $settings = settings_provider::filter_plugin_settings($quiz); $settings->quizid = $quiz->id; $settings->cmid = $cm->id; // Get existing settings or create new settings if none exist. $quizsettings = seb_quiz_settings::get_by_quiz_id($quiz->id); if (empty($quizsettings)) { $quizsettings = new seb_quiz_settings(0, $settings); } else { $settings->id = $quizsettings->get('id'); $quizsettings->from_record($settings); } // Process uploaded files if required. if ($quizsettings->get('requiresafeexambrowser') == settings_provider::USE_SEB_UPLOAD_CONFIG) { $draftitemid = file_get_submitted_draft_itemid('filemanager_sebconfigfile'); settings_provider::save_filemanager_sebconfigfile_draftarea($draftitemid, $cm->id); } else { settings_provider::delete_uploaded_config_file($cm->id); } // Save or delete settings. if ($quizsettings->get('requiresafeexambrowser') != settings_provider::USE_SEB_NO) { $quizsettings->save(); } else if ($quizsettings->get('id')) { $quizsettings->delete(); } } /** * Delete any rule-specific settings when the quiz is deleted. This is called * from {@link quiz_delete_instance()} in lib.php. * * @param stdClass $quiz the data from the database, including $quiz->id * which is the id of the quiz being deleted. */ public static function delete_settings($quiz) { $quizsettings = seb_quiz_settings::get_by_quiz_id($quiz->id); // Check that there are existing settings. if ($quizsettings !== false) { $quizsettings->delete(); } } /** * Return the bits of SQL needed to load all the settings from all the access * plugins in one DB query. The easiest way to understand what you need to do * here is probably to read the code of {@see \mod_quiz\access_manager::load_settings()}. * * If you have some settings that cannot be loaded in this way, then you can * use the {@link get_extra_settings()} method instead, but that has * performance implications. * * @param int $quizid the id of the quiz we are loading settings for. This * can also be accessed as quiz.id in the SQL. (quiz is a table alisas for {quiz}.) * @return array with three elements: * 1. fields: any fields to add to the select list. These should be alised * if neccessary so that the field name starts the name of the plugin. * 2. joins: any joins (should probably be LEFT JOINS) with other tables that * are needed. * 3. params: array of placeholder values that are needed by the SQL. You must * used named placeholders, and the placeholder names should start with the * plugin name, to avoid collisions. */ public static function get_settings_sql($quizid): array { return [ 'seb.requiresafeexambrowser AS seb_requiresafeexambrowser, ' . 'seb.showsebtaskbar AS seb_showsebtaskbar, ' . 'seb.showwificontrol AS seb_showwificontrol, ' . 'seb.showreloadbutton AS seb_showreloadbutton, ' . 'seb.showtime AS seb_showtime, ' . 'seb.showkeyboardlayout AS seb_showkeyboardlayout, ' . 'seb.allowuserquitseb AS seb_allowuserquitseb, ' . 'seb.quitpassword AS seb_quitpassword, ' . 'seb.linkquitseb AS seb_linkquitseb, ' . 'seb.userconfirmquit AS seb_userconfirmquit, ' . 'seb.enableaudiocontrol AS seb_enableaudiocontrol, ' . 'seb.muteonstartup AS seb_muteonstartup, ' . 'seb.allowcapturecamera AS seb_allowcapturecamera, ' . 'seb.allowcapturemicrophone AS seb_allowcapturemicrophone, ' . 'seb.allowspellchecking AS seb_allowspellchecking, ' . 'seb.allowreloadinexam AS seb_allowreloadinexam, ' . 'seb.activateurlfiltering AS seb_activateurlfiltering, ' . 'seb.filterembeddedcontent AS seb_filterembeddedcontent, ' . 'seb.expressionsallowed AS seb_expressionsallowed, ' . 'seb.regexallowed AS seb_regexallowed, ' . 'seb.expressionsblocked AS seb_expressionsblocked, ' . 'seb.regexblocked AS seb_regexblocked, ' . 'seb.allowedbrowserexamkeys AS seb_allowedbrowserexamkeys, ' . 'seb.showsebdownloadlink AS seb_showsebdownloadlink, ' . 'sebtemplate.id AS seb_templateid ' , 'LEFT JOIN {quizaccess_seb_quizsettings} seb ON seb.quizid = quiz.id ' . 'LEFT JOIN {quizaccess_seb_template} sebtemplate ON seb.templateid = sebtemplate.id ' , [] ]; } /** * Whether the user should be blocked from starting a new attempt or continuing * an attempt now. * * @return string false if access should be allowed, a message explaining the * reason if access should be prevented. */ public function prevent_access() { global $PAGE; if (!$this->accessmanager->seb_required()) { return false; } if ($this->accessmanager->can_bypass_seb()) { return false; } // If the rule is active, enforce a secure view whilst taking the quiz. $PAGE->set_pagelayout('secure'); $this->prevent_display_blocks(); // Access has previously been validated for this session and quiz. if ($this->accessmanager->validate_session_access()) { return false; } if (!$this->accessmanager->validate_basic_header()) { access_prevented::create_strict($this->accessmanager, $this->get_reason_text('not_seb'))->trigger(); return $this->get_require_seb_error_message(); } if (!$this->accessmanager->validate_config_key()) { if ($this->accessmanager->should_redirect_to_seb_config_link()) { $this->accessmanager->redirect_to_seb_config_link(); } access_prevented::create_strict($this->accessmanager, $this->get_reason_text('invalid_config_key'))->trigger(); return $this->get_invalid_key_error_message(); } if (!$this->accessmanager->validate_browser_exam_key()) { access_prevented::create_strict($this->accessmanager, $this->get_reason_text('invalid_browser_key'))->trigger(); return $this->get_invalid_key_error_message(); } // Set the state of the access for this Moodle session. $this->accessmanager->set_session_access(true); return false; } /** * Returns a list of finished attempts for the current user. * * @return array */ private function get_user_finished_attempts(): array { global $USER; return quiz_get_user_attempts( $this->quizobj->get_quizid(), $USER->id, quiz_attempt::FINISHED, false ); } /** * Prevent block displaying as configured. */ private function prevent_display_blocks() { global $PAGE; if ($PAGE->has_set_url() && $PAGE->url == $this->quizobj->view_url()) { $attempts = $this->get_user_finished_attempts(); // Don't display blocks before starting an attempt. if (empty($attempts) && !get_config('quizaccess_seb', 'displayblocksbeforestart')) { $PAGE->blocks->show_only_fake_blocks(); } // Don't display blocks after finishing an attempt. if (!empty($attempts) && !get_config('quizaccess_seb', 'displayblockswhenfinished')) { $PAGE->blocks->show_only_fake_blocks(); } } } /** * Returns reason for access prevention as a text. * * @param string $identifier Reason string identifier. * @return string */ private function get_reason_text(string $identifier): string { if (in_array($identifier, ['not_seb', 'invalid_config_key', 'invalid_browser_key'])) { return get_string($identifier, 'quizaccess_seb'); } return get_string('unknown_reason', 'quizaccess_seb'); } /** * Return error message when a SEB key is not valid. * * @return string */ private function get_invalid_key_error_message(): string { // Return error message with download link and links to get the seb config. if ($this->accessmanager->is_using_seb()) { return get_string('invalidkeys', 'quizaccess_seb') . $this->display_buttons($this->get_action_buttons()); } return $this->display_buttons($this->get_action_buttons()); } /** * Return error message when a SEB browser is not used. * * @return string */ private function get_require_seb_error_message(): string { $message = get_string('clientrequiresseb', 'quizaccess_seb'); if ($this->should_display_download_seb_link()) { $message .= $this->display_buttons($this->get_download_seb_button()); } // Return error message with download link. return $message; } /** * Helper function to display an Exit Safe Exam Browser button if configured to do so and attempts are > 0. * * @return string empty or a button which has the configured seb quit link. */ private function get_quit_button(): string { $quitbutton = ''; if (empty($this->get_user_finished_attempts())) { return $quitbutton; } // Only display if the link has been configured and attempts are greater than 0. if (!empty($this->quiz->seb_linkquitseb)) { $quitbutton = html_writer::link( $this->quiz->seb_linkquitseb, get_string('exitsebbutton', 'quizaccess_seb'), ['class' => 'btn btn-secondary'] ); } return $quitbutton; } /** * 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(): array { global $PAGE; $messages = [get_string('sebrequired', 'quizaccess_seb')]; // Display download SEB config link for those who can bypass using SEB. if ($this->accessmanager->can_bypass_seb() && $this->accessmanager->should_validate_config_key()) { $messages[] = $this->display_buttons($this->get_download_config_button()); } // Those with higher level access will be able to see the button if they've made an attempt. if (!$this->prevent_access()) { $messages[] = $this->display_buttons($this->get_quit_button()); } else { $PAGE->requires->js_call_amd('quizaccess_seb/validate_quiz_access', 'init', [$this->quiz->cmid, (bool)get_config('quizaccess_seb', 'autoreconfigureseb')]); } return $messages; } /** * Sets up the attempt (review or summary) page with any special extra * properties required by this rule. * * @param moodle_page $page the page object to initialise. */ public function setup_attempt_page($page) { $page->set_title($this->quizobj->get_course()->shortname . ': ' . $page->title); $page->set_popup_notification_allowed(false); // Prevent message notifications. $page->set_heading($page->title); $page->set_pagelayout('secure'); } /** * This is called when the current attempt at the quiz is finished. */ public function current_attempt_finished() { $this->accessmanager->clear_session_access(); } /** * Prepare buttons HTML code for being displayed on the screen. * * @param string $buttonshtml Html string of the buttons. * @param string $class Optional CSS class (or classes as space-separated list) * @param array $attributes Optional other attributes as array * * @return string HTML code of the provided buttons. */ private function display_buttons(string $buttonshtml, $class = '', ?array $attributes = null): string { $html = ''; if (!empty($buttonshtml)) { $html = html_writer::div($buttonshtml, $class, $attributes); } return $html; } /** * Get buttons to prompt user to download SEB or config file or launch SEB. * * @return string Html block of all action buttons. */ private function get_action_buttons(): string { $buttons = ''; if ($this->should_display_download_seb_link()) { $buttons .= $this->get_download_seb_button(); } // Get config for displaying links. $linkconfig = explode(',', get_config('quizaccess_seb', 'showseblinks')); // Display links to download config/launch SEB only if required. if ($this->accessmanager->should_validate_config_key()) { if (in_array('seb', $linkconfig)) { $buttons .= $this->get_launch_seb_button(); } if (in_array('http', $linkconfig)) { $buttons .= $this->get_download_config_button(); } } return $buttons; } /** * Get a button to download SEB. * * @return string A link to download SafeExam Browser. */ private function get_download_seb_button(): string { global $OUTPUT; $button = ''; if (!empty($this->get_seb_download_url())) { $sebdownloadlink = $this->get_seb_download_url(); $button = html_writer::start_tag('div', ['class' => 'singlebutton']); $button .= html_writer::link($sebdownloadlink, get_string('sebdownloadbutton', 'quizaccess_seb'), ['class' => 'btn btn-secondary', 'target' => '_blank']); $button .= html_writer::end_tag('div'); } return $button; } /** * Get a button to launch Safe Exam Browser. * * @return string A link to launch Safe Exam Browser. */ private function get_launch_seb_button(): string { // Rendering as a href and not as button in a form to circumvent browser warnings for sending to URL with unknown protocol. $seblink = \quizaccess_seb\link_generator::get_link($this->quiz->cmid, true, is_https()); $buttonlink = html_writer::start_tag('div', ['class' => 'singlebutton']); $buttonlink .= html_writer::link($seblink, get_string('seblinkbutton', 'quizaccess_seb'), ['class' => 'btn btn-secondary', 'title' => get_string('seblinkbutton', 'quizaccess_seb')]); $buttonlink .= html_writer::end_tag('div'); return $buttonlink; } /** * Get a button to download Safe Exam Browser config. * * @return string A link to launch Safe Exam Browser. */ private function get_download_config_button(): string { // Rendering as a href and not as button in a form to circumvent browser warnings for sending to URL with unknown protocol. $httplink = \quizaccess_seb\link_generator::get_link($this->quiz->cmid, false, is_https()); $buttonlink = html_writer::start_tag('div', ['class' => 'singlebutton']); $buttonlink .= html_writer::link($httplink, get_string('httplinkbutton', 'quizaccess_seb'), ['class' => 'btn btn-secondary', 'title' => get_string('httplinkbutton', 'quizaccess_seb')]); $buttonlink .= html_writer::end_tag('div'); return $buttonlink; } /** * Returns SEB download URL. * * @return string */ private function get_seb_download_url(): string { return get_config('quizaccess_seb', 'downloadlink'); } /** * Check if we should display a link to download Safe Exam Browser. * * @return bool */ private function should_display_download_seb_link(): bool { return !empty($this->quiz->seb_showsebdownloadlink); } } PK\O##tests/helper_test.phpnu[. namespace quizaccess_seb; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/test_helper_trait.php'); /** * PHPUnit tests for helper class. * * @package quizaccess_seb * @author Dmitrii Metelkin * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class helper_test extends \advanced_testcase { use \quizaccess_seb_test_helper_trait; /** * Test that we can check valid seb string. */ public function test_is_valid_seb_config(): void { $validseb = " showTaskBarallowWlanshowReloadButton" . "showTimeshowInputLanguageallowQuit" . "quitURLConfirmaudioControlEnabledaudioMute" . "browserMediaCaptureCamerabrowserMediaCaptureMicrophone" . "allowSpellCheckbrowserWindowAllowReloadURLFilterEnable" . "URLFilterEnableContentFilterhashedQuitPassword" . "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08URLFilterRules" . "action1activeexpression" . "test.comregex" . "sendBrowserExamKey\n"; $invalidseb = 'Invalid seb'; $emptyseb = ''; $this->assertTrue(\quizaccess_seb\helper::is_valid_seb_config($validseb)); $this->assertFalse(\quizaccess_seb\helper::is_valid_seb_config($invalidseb)); $this->assertFalse(\quizaccess_seb\helper::is_valid_seb_config($emptyseb)); } /** * Test that we can get seb file headers. */ public function test_get_seb_file_headers(): void { $expiretime = 1582767914; $headers = \quizaccess_seb\helper::get_seb_file_headers($expiretime); $this->assertCount(5, $headers); $this->assertEquals('Cache-Control: private, max-age=1, no-transform', $headers[0]); $this->assertEquals('Expires: Thu, 27 Feb 2020 01:45:14 GMT', $headers[1]); $this->assertEquals('Pragma: no-cache', $headers[2]); $this->assertEquals('Content-Disposition: attachment; filename=config.seb', $headers[3]); $this->assertEquals('Content-Type: application/seb', $headers[4]); } /** * Test that the course module must exist to get a seb config file content. */ public function test_can_not_get_config_content_with_invalid_cmid(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $this->getDataGenerator()->enrol_user($user->id, $course->id); $this->setUser($user); // Log user in. $this->expectException(\dml_exception::class); $this->expectExceptionMessage("Can't find data record in database. (SELECT cm.*, m.name, md.name AS modname \n" . " FROM {course_modules} cm\n" . " JOIN {modules} md ON md.id = cm.module\n" . " JOIN {quiz} m ON m.id = cm.instance\n" . " \n" . " WHERE cm.id = :cmid AND md.name = :modulename\n" . " \n" . "[array (\n" . " 'cmid' => '999',\n" . " 'modulename' => 'quiz',\n" .')])'); \quizaccess_seb\helper::get_seb_config_content('999'); } /** * Test that the user must be enrolled to get seb config content. */ public function test_can_not_get_config_content_when_user_not_enrolled_in_course(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $quiz = $this->create_test_quiz($course, \quizaccess_seb\settings_provider::USE_SEB_CONFIG_MANUALLY); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Log user in. $this->expectException(\moodle_exception::class); $this->expectExceptionMessage('Unsupported redirect detected, script execution terminated'); \quizaccess_seb\helper::get_seb_config_content($quiz->cmid); } /** * Test that if SEB quiz settings can't be found, a seb config content won't be provided. */ public function test_can_not_get_config_content_if_config_not_found_for_cmid(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $quiz = $this->create_test_quiz($course); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id); $this->setUser($user); // Log user in. $this->expectException(\moodle_exception::class); $this->expectExceptionMessage("No SEB config could be found for quiz with cmid: $quiz->cmid"); \quizaccess_seb\helper::get_seb_config_content($quiz->cmid); } /** * That that if config is empty for a quiz, a seb config content won't be provided. */ public function test_can_not_get_config_content_if_config_empty(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $quiz = $this->create_test_quiz($course, \quizaccess_seb\settings_provider::USE_SEB_NO); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id); $this->setUser($user); // Log user in. $this->expectException(\moodle_exception::class); $this->expectExceptionMessage("No SEB config could be found for quiz with cmid: $quiz->cmid"); \quizaccess_seb\helper::get_seb_config_content($quiz->cmid); } /** * Test config content is provided successfully. */ public function test_config_provided(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $quiz = $this->create_test_quiz($course, \quizaccess_seb\settings_provider::USE_SEB_CONFIG_MANUALLY); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id); $this->setUser($user); // Log user in. $config = \quizaccess_seb\helper::get_seb_config_content($quiz->cmid); $url = new \moodle_url("/mod/quiz/view.php", ['id' => $quiz->cmid]); $this->assertEquals("\n" . "\n" . "showTaskBarallowWlan" . "showReloadButtonshowTimeshowInputLanguage" . "allowQuitquitURLConfirmaudioControlEnabled" . "audioMutebrowserMediaCaptureCamerabrowserMediaCaptureMicrophone" . "allowSpellCheckbrowserWindowAllowReload" . "URLFilterEnableURLFilterEnableContentFilter" . "URLFilterRulesstartURL$url" . "sendBrowserExamKeybrowserWindowWebView3" . "examSessionClearCookiesOnStart" . "allowPreferencesWindow\n", $config); } } PK\D+tests/fixtures/JSON_unencrypted_mac_001.txtnu[{"additionalResources":[],"allowBrowsingBackForward":false,"allowDictation":false,"allowDictionaryLookup":false,"allowDisplayMirroring":false,"allowDownUploads":false,"allowedDisplayBuiltin":true,"allowedDisplaysMaxNumber":1,"allowFlashFullscreen":false,"allowiOSBetaVersionNumber":0,"allowiOSVersionNumberMajor":9,"allowiOSVersionNumberMinor":3,"allowiOSVersionNumberPatch":5,"allowPDFPlugIn":false,"allowPreferencesWindow":true,"allowQuit":true,"allowScreenSharing":false,"allowSiri":false,"allowSpellCheck":false,"allowSwitchToApplications":false,"allowUserAppFolderInstall":false,"allowUserSwitching":true,"allowVideoCapture":false,"allowVirtualMachine":false,"allowWlan":false,"blacklistURLFilter":"","blockPopUpWindows":false,"browserMediaAutoplay":false,"browserMessagingPingTime":120000,"browserMessagingSocket":"ws:\localhost:8706","browserScreenKeyboard":false,"browserURLSalt":true,"browserUserAgent":"","browserUserAgentiOS":0,"browserUserAgentiOSCustom":"","browserUserAgentMac":0,"browserUserAgentMacCustom":"","browserUserAgentWinDesktopMode":0,"browserUserAgentWinDesktopModeCustom":"","browserUserAgentWinTouchMode":0,"browserUserAgentWinTouchModeCustom":"","browserUserAgentWinTouchModeIPad":"Mozilla/5.0 (iPad; CPU OS 11_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.0 Mobile/15E148 Safari/604.1","browserViewMode":0,"browserWindowAllowReload":true,"browserWindowShowURL":0,"chooseFileToUploadPolicy":0,"configFileCreateIdentity":false,"configFileEncryptUsingIdentity":false,"configKeySalt":"","createNewDesktop":true,"detectStoppedProcess":true,"downloadAndOpenSebConfig":true,"downloadDirectoryOSX":"/Users/andrewmadden/Downloads","downloadDirectoryWin":"Downloads","downloadPDFFiles":false,"embeddedCertificates":[],"enableAltEsc":false,"enableAltF4":false,"enableAltMouseWheel":false,"enableAltTab":true,"enableAppSwitcherCheck":true,"enableBrowserWindowToolbar":false,"enableCtrlEsc":false,"enableDrawingEditor":false,"enableEsc":true,"enableF1":true,"enableF10":true,"enableF11":true,"enableF12":true,"enableF2":true,"enableF3":true,"enableF4":true,"enableF5":true,"enableF6":true,"enableF7":true,"enableF8":true,"enableF9":true,"enableJava":false,"enableJavaScript":true,"enableLogging":true,"enablePlugIns":true,"enablePrintScreen":false,"enablePrivateClipboard":true,"enableRightMouse":true,"enableSebBrowser":true,"enableStartMenu":false,"enableTouchExit":false,"enableZoomPage":true,"enableZoomText":true,"examKeySalt":"4iLONVAc1yKWLxbe6Vbf2vWYQO6zIRAobKgc3f25LH8=","examSessionClearSessionCookies":true,"examSessionReconfigureAllow":false,"examSessionReconfigureConfigURL":"","exitKey1":2,"exitKey2":10,"exitKey3":5,"forceAppFolderInstall":true,"hashedAdminPassword":"","hashedQuitPassword":"","hideBrowserWindowToolbar":false,"hookKeys":true,"ignoreExitKeys":true,"ignoreQuitPassword":false,"insideSebEnableChangeAPassword":false,"insideSebEnableEaseOfAccess":false,"insideSebEnableLockThisComputer":false,"insideSebEnableLogOff":false,"insideSebEnableNetworkConnectionSelector":false,"insideSebEnableShutDown":false,"insideSebEnableStartTaskManager":false,"insideSebEnableSwitchUser":false,"insideSebEnableVmWareClientShade":false,"killExplorerShell":false,"logDirectoryOSX":"","logDirectoryWin":"","logLevel":4,"logSendingRequiresAdminPassword":false,"mainBrowserWindowHeight":"100%","mainBrowserWindowPositioning":1,"mainBrowserWindowWidth":"100%","minMacOSVersion":4,"mobileAllowInlineMediaPlayback":true,"mobileAllowPictureInPictureMediaPlayback":false,"mobileAllowQRCodeConfig":false,"mobileAllowSingleAppMode":false,"mobileCompactAllowInlineMediaPlayback":false,"mobileEnableASAM":true,"mobileEnableGuidedAccessLinkTransform":false,"mobilePreventAutoLock":true,"mobileShowSettings":false,"mobileStatusBarAppearance":1,"mobileStatusBarAppearanceExtended":1,"mobileSupportedFormFactorsCompact":true,"mobileSupportedFormFactorsNonTelephonyCompact":true,"mobileSupportedFormFactorsRegular":true,"mobileSupportedScreenOrientationsCompactLandscapeLeft":true,"mobileSupportedScreenOrientationsCompactLandscapeRight":true,"mobileSupportedScreenOrientationsCompactPortrait":true,"mobileSupportedScreenOrientationsCompactPortraitUpsideDown":false,"mobileSupportedScreenOrientationsRegularLandscapeLeft":true,"mobileSupportedScreenOrientationsRegularLandscapeRight":true,"mobileSupportedScreenOrientationsRegularPortrait":true,"mobileSupportedScreenOrientationsRegularPortraitUpsideDown":true,"monitorProcesses":true,"newBrowserWindowAllowReload":true,"newBrowserWindowByLinkBlockForeign":false,"newBrowserWindowByLinkHeight":"100%","newBrowserWindowByLinkPolicy":1,"newBrowserWindowByLinkPositioning":2,"newBrowserWindowByLinkWidth":"1000","newBrowserWindowByScriptBlockForeign":false,"newBrowserWindowByScriptPolicy":2,"newBrowserWindowNavigation":true,"newBrowserWindowShowReloadWarning":false,"newBrowserWindowShowURL":2,"openDownloads":false,"oskBehavior":2,"permittedProcesses":[],"pinEmbeddedCertificates":false,"prohibitedProcesses":[],"proxies":{"org_safeexambrowser_SEB_AutoConfigurationEnabled":false,"org_safeexambrowser_SEB_AutoConfigurationJavaScript":"","org_safeexambrowser_SEB_AutoConfigurationURL":"","org_safeexambrowser_SEB_AutoDiscoveryEnabled":false,"org_safeexambrowser_SEB_ExceptionsList":[],"org_safeexambrowser_SEB_ExcludeSimpleHostnames":false,"org_safeexambrowser_SEB_FTPEnable":false,"org_safeexambrowser_SEB_FTPPassive":true,"org_safeexambrowser_SEB_FTPPassword":"","org_safeexambrowser_SEB_FTPPort":21,"org_safeexambrowser_SEB_FTPProxy":"","org_safeexambrowser_SEB_FTPRequiresPassword":false,"org_safeexambrowser_SEB_FTPUsername":"","org_safeexambrowser_SEB_HTTPEnable":false,"org_safeexambrowser_SEB_HTTPPassword":"","org_safeexambrowser_SEB_HTTPPort":80,"org_safeexambrowser_SEB_HTTPProxy":"","org_safeexambrowser_SEB_HTTPRequiresPassword":false,"org_safeexambrowser_SEB_HTTPSEnable":false,"org_safeexambrowser_SEB_HTTPSPassword":"","org_safeexambrowser_SEB_HTTPSPort":443,"org_safeexambrowser_SEB_HTTPSProxy":"","org_safeexambrowser_SEB_HTTPSRequiresPassword":false,"org_safeexambrowser_SEB_HTTPSUsername":"","org_safeexambrowser_SEB_HTTPUsername":"","org_safeexambrowser_SEB_RTSPEnable":false,"org_safeexambrowser_SEB_RTSPPassword":"","org_safeexambrowser_SEB_RTSPPort":554,"org_safeexambrowser_SEB_RTSPProxy":"","org_safeexambrowser_SEB_RTSPRequiresPassword":false,"org_safeexambrowser_SEB_RTSPUsername":"","org_safeexambrowser_SEB_SOCKSEnable":false,"org_safeexambrowser_SEB_SOCKSPassword":"","org_safeexambrowser_SEB_SOCKSPort":1080,"org_safeexambrowser_SEB_SOCKSProxy":"","org_safeexambrowser_SEB_SOCKSRequiresPassword":false,"org_safeexambrowser_SEB_SOCKSUsername":""},"proxySettingsPolicy":0,"quitURL":"","quitURLConfirm":true,"quitURLRestart":false,"removeBrowserProfile":false,"removeLocalStorage":false,"restartExamPasswordProtected":true,"restartExamText":"","restartExamURL":"","restartExamUseStartURL":false,"sebConfigPurpose":0,"sebMode":0,"sebServerFallback":false,"sebServerURL":"","sebServicePolicy":2,"sendBrowserExamKey":true,"showBackToStartButton":true,"showInputLanguage":false,"showMenuBar":true,"showNavigationButtons":false,"showQuitButton":true,"showReloadButton":true,"showReloadWarning":false,"showScanQRCodeButton":false,"showTaskBar":true,"showTime":true,"startURL":"https://www.google.com","startURLAllowDeepLink":false,"startURLAppendQueryParameter":false,"taskBarHeight":40,"touchOptimized":false,"URLFilterEnable":false,"URLFilterEnableContentFilter":false,"URLFilterIgnoreList":[],"URLFilterMessage":0,"urlFilterRegex":true,"URLFilterRules":[],"urlFilterTrustedContent":false,"whitelistURLFilter":"","zoomMode":0}PK\ x{$tests/fixtures/simpleunencrypted.sebnu[ aaaaaa 1940-10-09T22:13:56Z originatorVersion SEB_Win_2.1.1 startURL https://safeexambrowser.org/start startResource sebServerURL hashedAdminPassword allowQuit ignoreExitKeys allowUserAppFolderInstall allowSiri allowDictation detectStoppedProcess allowDisplayMirroring allowedDisplaysMaxNumber 1 allowedDisplayBuiltin PK\ؙp1717&tests/fixtures/unencrypted_mac_001.sebnu[ URLFilterEnable URLFilterEnableContentFilter URLFilterIgnoreList URLFilterMessage 0 URLFilterRules additionalResources allowBrowsingBackForward allowDictation allowDictionaryLookup allowDisplayMirroring allowDownUploads allowFlashFullscreen allowPDFPlugIn allowPreferencesWindow allowQuit allowScreenSharing allowSiri allowSpellCheck allowSwitchToApplications allowUserAppFolderInstall allowUserSwitching allowVideoCapture allowVirtualMachine allowWlan allowedDisplayBuiltin allowedDisplaysMaxNumber 1 allowiOSBetaVersionNumber 0 allowiOSVersionNumberMajor 9 allowiOSVersionNumberMinor 3 allowiOSVersionNumberPatch 5 blacklistURLFilter blockPopUpWindows browserMediaAutoplay browserMessagingPingTime 120000 browserMessagingSocket ws:\localhost:8706 browserScreenKeyboard browserURLSalt browserUserAgent browserUserAgentMac 0 browserUserAgentMacCustom browserUserAgentWinDesktopMode 0 browserUserAgentWinDesktopModeCustom browserUserAgentWinTouchMode 0 browserUserAgentWinTouchModeCustom browserUserAgentWinTouchModeIPad Mozilla/5.0 (iPad; CPU OS 11_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.0 Mobile/15E148 Safari/604.1 browserUserAgentiOS 0 browserUserAgentiOSCustom browserViewMode 0 browserWindowAllowReload browserWindowShowURL 0 chooseFileToUploadPolicy 0 configFileCreateIdentity configFileEncryptUsingIdentity configKeySalt createNewDesktop detectStoppedProcess downloadAndOpenSebConfig downloadDirectoryOSX /Users/andrewmadden/Downloads downloadDirectoryWin Downloads downloadPDFFiles embeddedCertificates enableAltEsc enableAltF4 enableAltMouseWheel enableAltTab enableAppSwitcherCheck enableBrowserWindowToolbar enableCtrlEsc enableDrawingEditor enableEsc enableF1 enableF10 enableF11 enableF12 enableF2 enableF3 enableF4 enableF5 enableF6 enableF7 enableF8 enableF9 enableJava enableJavaScript enableLogging enablePlugIns enablePrintScreen enablePrivateClipboard enableRightMouse enableSebBrowser enableStartMenu enableTouchExit enableZoomPage enableZoomText examKeySalt 4iLONVAc1yKWLxbe6Vbf2vWYQO6zIRAobKgc3f25LH8= examSessionClearSessionCookies examSessionReconfigureAllow examSessionReconfigureConfigURL exitKey1 2 exitKey2 10 exitKey3 5 forceAppFolderInstall hashedAdminPassword hashedQuitPassword hideBrowserWindowToolbar hookKeys ignoreExitKeys ignoreQuitPassword insideSebEnableChangeAPassword insideSebEnableEaseOfAccess insideSebEnableLockThisComputer insideSebEnableLogOff insideSebEnableNetworkConnectionSelector insideSebEnableShutDown insideSebEnableStartTaskManager insideSebEnableSwitchUser insideSebEnableVmWareClientShade killExplorerShell logDirectoryOSX logDirectoryWin logLevel 4 logSendingRequiresAdminPassword mainBrowserWindowHeight 100% mainBrowserWindowPositioning 1 mainBrowserWindowWidth 100% minMacOSVersion 4 mobileAllowInlineMediaPlayback mobileAllowPictureInPictureMediaPlayback mobileAllowQRCodeConfig mobileAllowSingleAppMode mobileCompactAllowInlineMediaPlayback mobileEnableASAM mobileEnableGuidedAccessLinkTransform mobilePreventAutoLock mobileShowSettings mobileStatusBarAppearance 1 mobileStatusBarAppearanceExtended 1 mobileSupportedFormFactorsCompact mobileSupportedFormFactorsNonTelephonyCompact mobileSupportedFormFactorsRegular mobileSupportedScreenOrientationsCompactLandscapeLeft mobileSupportedScreenOrientationsCompactLandscapeRight mobileSupportedScreenOrientationsCompactPortrait mobileSupportedScreenOrientationsCompactPortraitUpsideDown mobileSupportedScreenOrientationsRegularLandscapeLeft mobileSupportedScreenOrientationsRegularLandscapeRight mobileSupportedScreenOrientationsRegularPortrait mobileSupportedScreenOrientationsRegularPortraitUpsideDown monitorProcesses newBrowserWindowAllowReload newBrowserWindowByLinkBlockForeign newBrowserWindowByLinkHeight 100% newBrowserWindowByLinkPolicy 1 newBrowserWindowByLinkPositioning 2 newBrowserWindowByLinkWidth 1000 newBrowserWindowByScriptBlockForeign newBrowserWindowByScriptPolicy 2 newBrowserWindowNavigation newBrowserWindowShowReloadWarning newBrowserWindowShowURL 2 openDownloads originatorVersion SEB_OSX_2.1.4_2B66 oskBehavior 2 permittedProcesses pinEmbeddedCertificates prohibitedProcesses proxies org_safeexambrowser_SEB_AutoConfigurationEnabled org_safeexambrowser_SEB_AutoConfigurationJavaScript org_safeexambrowser_SEB_AutoConfigurationURL org_safeexambrowser_SEB_AutoDiscoveryEnabled org_safeexambrowser_SEB_ExceptionsList org_safeexambrowser_SEB_ExcludeSimpleHostnames org_safeexambrowser_SEB_FTPEnable org_safeexambrowser_SEB_FTPPassive org_safeexambrowser_SEB_FTPPassword org_safeexambrowser_SEB_FTPPort 21 org_safeexambrowser_SEB_FTPProxy org_safeexambrowser_SEB_FTPRequiresPassword org_safeexambrowser_SEB_FTPUsername org_safeexambrowser_SEB_HTTPEnable org_safeexambrowser_SEB_HTTPPassword org_safeexambrowser_SEB_HTTPPort 80 org_safeexambrowser_SEB_HTTPProxy org_safeexambrowser_SEB_HTTPRequiresPassword org_safeexambrowser_SEB_HTTPSEnable org_safeexambrowser_SEB_HTTPSPassword org_safeexambrowser_SEB_HTTPSPort 443 org_safeexambrowser_SEB_HTTPSProxy org_safeexambrowser_SEB_HTTPSRequiresPassword org_safeexambrowser_SEB_HTTPSUsername org_safeexambrowser_SEB_HTTPUsername org_safeexambrowser_SEB_RTSPEnable org_safeexambrowser_SEB_RTSPPassword org_safeexambrowser_SEB_RTSPPort 554 org_safeexambrowser_SEB_RTSPProxy org_safeexambrowser_SEB_RTSPRequiresPassword org_safeexambrowser_SEB_RTSPUsername org_safeexambrowser_SEB_SOCKSEnable org_safeexambrowser_SEB_SOCKSPassword org_safeexambrowser_SEB_SOCKSPort 1080 org_safeexambrowser_SEB_SOCKSProxy org_safeexambrowser_SEB_SOCKSRequiresPassword org_safeexambrowser_SEB_SOCKSUsername proxySettingsPolicy 0 quitURL quitURLConfirm quitURLRestart removeBrowserProfile removeLocalStorage restartExamPasswordProtected restartExamText restartExamURL restartExamUseStartURL sebConfigPurpose 0 sebMode 0 sebServerFallback sebServerURL sebServicePolicy 2 sendBrowserExamKey showBackToStartButton showInputLanguage showMenuBar showNavigationButtons showQuitButton showReloadButton showReloadWarning showScanQRCodeButton showTaskBar showTime startURL https://www.google.com startURLAllowDeepLink startURLAppendQueryParameter taskBarHeight 40 touchOptimized urlFilterRegex urlFilterTrustedContent whitelistURLFilter zoomMode 0 PK\צ:= = tests/fixtures/encrypted.sebnu[& pswds{ZQN_b}e%삛Mk1P>iN$~RY;₎IU, )n.LgMM dB>p4t:Ec&odO]7 l+>%FK !T+YވtJΥ]#xI|^_u11)'8 p[Uef*qg@!/VhG7q76%:3gD`8u>HL Ղ~%߄R1Yx]9e$YV|Rh怰3$rOccN\mtּ\[` S8];!yX2?PS!TqN5ꀳ<]tGG08}P^Gm4 yuR.F Yo"ejM\,S" voNeAMCNYfkN~˕;,M /NˀNZ@]g-ED:ى?_xȓ_'#槭yxgvqɅ7'}4;5uu7Ү&t>rEٓ dTXFO($(&ϓ1ZhxꓹpeAM.*w7}ًTDEet*D4pY.A Wj]1x@,,O(C[ "JQmm$1ͦ9tT) F)fF!Py%MQ>qM$UהR;@O+K'Eu 7~3sVb# B6@N) K#Y2~zs5TT j( M{x&R|XbzyŊ Q)(u o0;p{ԖyU~k{ BТG{dDR^ejŰT*4X\e#a5{mu \GuI 驉R:Ss0wRaٙ6ufzbhfp ~K&xf;7UY @Z ˫FS Dc% originatorVersion SEB_Win_2.1.1 startURL https://safeexambrowser.org/start startResource sebServerURL hashedAdminPassword allowQuit ignoreExitKeys hashedQuitPassword exitKey1 2 exitKey2 10 exitKey3 5 sebMode 0 browserMessagingSocket ws://localhost:8706 browserMessagingPingTime 120000 sebConfigPurpose 0 allowPreferencesWindow useAsymmetricOnlyEncryption browserViewMode 0 mainBrowserWindowWidth 100% mainBrowserWindowHeight 100% mainBrowserWindowPositioning 1 enableBrowserWindowToolbar hideBrowserWindowToolbar showMenuBar showTaskBar taskBarHeight 40 touchOptimized enableZoomText enableZoomPage zoomMode 0 allowSpellCheck allowDictionaryLookup allowSpellCheckDictionary additionalDictionaries showReloadButton showTime showInputLanguage enableTouchExit oskBehavior 2 audioControlEnabled audioMute audioVolumeLevel 25 audioSetVolumeLevel browserScreenKeyboard newBrowserWindowByLinkPolicy 2 newBrowserWindowByScriptPolicy 2 newBrowserWindowByLinkBlockForeign newBrowserWindowByScriptBlockForeign newBrowserWindowByLinkWidth 100% newBrowserWindowByLinkHeight 100% newBrowserWindowByLinkPositioning 2 enablePlugIns enableJava enableJavaScript blockPopUpWindows allowVideoCapture allowAudioCapture allowBrowsingBackForward newBrowserWindowNavigation removeBrowserProfile removeLocalStorage enableSebBrowser browserWindowAllowReload newBrowserWindowAllowReload showReloadWarning newBrowserWindowShowReloadWarning browserUserAgentWinDesktopMode 0 browserUserAgentWinDesktopModeCustom browserUserAgentWinTouchMode 0 browserUserAgentWinTouchModeIPad Mozilla/5.0 (iPad; CPU OS 11_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.3 Mobile/15E216 Safari/605.1.15 browserUserAgentWinTouchModeCustom browserUserAgent browserUserAgentMac 0 browserUserAgentMacCustom browserWindowTitleSuffix allowDownUploads downloadDirectoryOSX ~/Downloads downloadDirectoryWin openDownloads chooseFileToUploadPolicy 0 downloadPDFFiles allowPDFPlugIn downloadAndOpenSebConfig backgroundOpenSEBConfig examKeySalt 0zVdSEqKAnK0ZNd4QFlwVKBTgw60o6xRjpfDokUYYnk= browserExamKey browserURLSalt sendBrowserExamKey quitURL quitURLConfirm restartExamURL restartExamUseStartURL restartExamText restartExamPasswordProtected additionalResources monitorProcesses allowSwitchToApplications allowFlashFullscreen permittedProcesses active autostart iconInTaskbar runInBackground allowUserToChooseApp strongKill os 1 title SEB description executable firefox.exe originalName firefox.exe path ../xulrunner/ identifier Firefox windowHandlingProcess arguments prohibitedProcesses active currentUser strongKill os 1 executable join.me originalName join.me description identifier windowHandlingProcess user active currentUser strongKill os 1 executable RPCSuite originalName RPCSuite description identifier windowHandlingProcess user active currentUser strongKill os 1 executable RPCService originalName RPCService description identifier windowHandlingProcess user active currentUser strongKill os 1 executable RemotePCDesktop originalName RemotePCDesktop description identifier windowHandlingProcess user active currentUser strongKill os 1 executable beamyourscreen-host originalName beamyourscreen-host description identifier windowHandlingProcess user active currentUser strongKill os 1 executable AeroAdmin originalName AeroAdmin description identifier windowHandlingProcess user active currentUser strongKill os 1 executable Mikogo-host originalName Mikogo-host description identifier windowHandlingProcess user active currentUser strongKill os 1 executable chromoting originalName chromoting description identifier windowHandlingProcess user active currentUser strongKill os 1 executable vncserverui originalName vncserverui description identifier windowHandlingProcess user active currentUser strongKill os 1 executable vncviewer originalName vncviewer description identifier windowHandlingProcess user active currentUser strongKill os 1 executable vncserver originalName vncserver description identifier windowHandlingProcess user active currentUser strongKill os 1 executable TeamViewer originalName TeamViewer description identifier windowHandlingProcess user active currentUser strongKill os 1 executable GotoMeetingWinStore originalName GotoMeetingWinStore description identifier windowHandlingProcess user active currentUser strongKill os 1 executable g2mcomm.exe originalName g2mcomm.exe description identifier windowHandlingProcess user active currentUser strongKill os 1 executable SkypeHost originalName SkypeHost description identifier windowHandlingProcess user active currentUser strongKill os 1 executable Skype originalName Skype description identifier windowHandlingProcess user enableURLFilter enableURLContentFilter URLFilterRules URLFilterEnable URLFilterEnableContentFilter blacklistURLFilter whitelistURLFilter urlFilterTrustedContent urlFilterRegex embeddedCertificates pinEmbeddedCertificates proxySettingsPolicy 0 proxies ExceptionsList ExcludeSimpleHostnames AutoDiscoveryEnabled AutoConfigurationEnabled AutoConfigurationJavaScript AutoConfigurationURL FTPPassive HTTPEnable HTTPPort 80 HTTPProxy HTTPRequiresPassword HTTPUsername HTTPPassword HTTPSEnable HTTPSPort 443 HTTPSProxy HTTPSRequiresPassword HTTPSUsername HTTPSPassword FTPEnable FTPPort 21 FTPProxy FTPRequiresPassword FTPUsername FTPPassword SOCKSEnable SOCKSPort 1080 SOCKSProxy SOCKSRequiresPassword SOCKSUsername SOCKSPassword RTSPEnable RTSPPort 554 RTSPProxy RTSPRequiresPassword RTSPUsername RTSPPassword sebServicePolicy 1 allowVirtualMachine allowScreenSharing enablePrivateClipboard createNewDesktop killExplorerShell enableLogging logDirectoryOSX ~/Documents logDirectoryWin allowWlan lockOnMessageSocketClose minMacOSVersion 4 enableAppSwitcherCheck forceAppFolderInstall allowUserAppFolderInstall allowSiri allowDictation detectStoppedProcess allowDisplayMirroring allowedDisplaysMaxNumber 1 allowedDisplayBuiltin insideSebEnableSwitchUser insideSebEnableLockThisComputer insideSebEnableChangeAPassword insideSebEnableStartTaskManager insideSebEnableLogOff insideSebEnableShutDown insideSebEnableEaseOfAccess insideSebEnableVmWareClientShade insideSebEnableEnableNetworkConnectionSelector hookKeys enableEsc enableCtrlEsc enableAltEsc enableAltTab enableAltF4 enableStartMenu enableRightMouse enablePrintScreen enableAltMouseWheel enableF1 enableF2 enableF3 enableF4 enableF5 enableF6 enableF7 enableF8 enableF9 enableF10 enableF11 enableF12 PK\1!P!P&tests/fixtures/unencrypted_win_223.sebnu[ batteryChargeThresholdCritical 0.1 batteryChargeThresholdLow 0.2 originatorVersion SEB_Win_2.1.1 startURL https://safeexambrowser.org/start startResource sebServerURL hashedAdminPassword allowQuit ignoreExitKeys hashedQuitPassword exitKey1 2 exitKey2 10 exitKey3 5 sebMode 0 browserMessagingSocket ws://localhost:8706 browserMessagingPingTime 120000 sebConfigPurpose 0 allowPreferencesWindow useAsymmetricOnlyEncryption browserViewMode 0 mainBrowserWindowWidth 100% mainBrowserWindowHeight 100% mainBrowserWindowPositioning 1 enableBrowserWindowToolbar hideBrowserWindowToolbar showMenuBar showTaskBar taskBarHeight 40 touchOptimized enableZoomText enableZoomPage zoomMode 0 allowSpellCheck allowDictionaryLookup allowSpellCheckDictionary additionalDictionaries showReloadButton showTime showInputLanguage enableTouchExit oskBehavior 2 audioControlEnabled audioMute audioVolumeLevel 25 audioSetVolumeLevel browserScreenKeyboard newBrowserWindowByLinkPolicy 2 newBrowserWindowByScriptPolicy 2 newBrowserWindowByLinkBlockForeign newBrowserWindowByScriptBlockForeign newBrowserWindowByLinkWidth 1000 newBrowserWindowByLinkHeight 100% newBrowserWindowByLinkPositioning 2 enablePlugIns enableJava enableJavaScript blockPopUpWindows allowVideoCapture allowAudioCapture allowBrowsingBackForward newBrowserWindowNavigation removeBrowserProfile removeLocalStorage enableSebBrowser browserWindowAllowReload newBrowserWindowAllowReload showReloadWarning newBrowserWindowShowReloadWarning browserUserAgentWinDesktopMode 0 browserUserAgentWinDesktopModeCustom browserUserAgentWinTouchMode 0 browserUserAgentWinTouchModeIPad Mozilla/5.0 (iPad; CPU OS 11_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.3 Mobile/15E216 Safari/605.1.15 browserUserAgentWinTouchModeCustom browserUserAgent browserUserAgentMac 0 browserUserAgentMacCustom browserWindowTitleSuffix allowDownUploads downloadDirectoryOSX ~/Downloads downloadDirectoryWin openDownloads chooseFileToUploadPolicy 0 downloadPDFFiles allowPDFPlugIn downloadAndOpenSebConfig backgroundOpenSEBConfig examKeySalt QJAqvg89YMP6JagAshUm6QqpqpsrVS9ZWUYjdZhfEao= browserExamKey browserURLSalt sendBrowserExamKey quitURL quitURLConfirm restartExamURL restartExamUseStartURL restartExamText restartExamPasswordProtected additionalResources monitorProcesses allowSwitchToApplications allowFlashFullscreen permittedProcesses active autostart iconInTaskbar runInBackground allowUserToChooseApp strongKill os 1 title SEB description executable firefox.exe originalName firefox.exe path ../xulrunner/ identifier Firefox windowHandlingProcess arguments prohibitedProcesses active currentUser strongKill os 1 executable join.me originalName join.me description identifier windowHandlingProcess user active currentUser strongKill os 1 executable RPCSuite originalName RPCSuite description identifier windowHandlingProcess user active currentUser strongKill os 1 executable RPCService originalName RPCService description identifier windowHandlingProcess user active currentUser strongKill os 1 executable RemotePCDesktop originalName RemotePCDesktop description identifier windowHandlingProcess user active currentUser strongKill os 1 executable beamyourscreen-host originalName beamyourscreen-host description identifier windowHandlingProcess user active currentUser strongKill os 1 executable AeroAdmin originalName AeroAdmin description identifier windowHandlingProcess user active currentUser strongKill os 1 executable Mikogo-host originalName Mikogo-host description identifier windowHandlingProcess user active currentUser strongKill os 1 executable chromoting originalName chromoting description identifier windowHandlingProcess user active currentUser strongKill os 1 executable vncserverui originalName vncserverui description identifier windowHandlingProcess user active currentUser strongKill os 1 executable vncviewer originalName vncviewer description identifier windowHandlingProcess user active currentUser strongKill os 1 executable vncserver originalName vncserver description identifier windowHandlingProcess user active currentUser strongKill os 1 executable TeamViewer originalName TeamViewer description identifier windowHandlingProcess user active currentUser strongKill os 1 executable GotoMeetingWinStore originalName GotoMeetingWinStore description identifier windowHandlingProcess user active currentUser strongKill os 1 executable g2mcomm.exe originalName g2mcomm.exe description identifier windowHandlingProcess user active currentUser strongKill os 1 executable SkypeHost originalName SkypeHost description identifier windowHandlingProcess user active currentUser strongKill os 1 executable Skype originalName Skype description identifier windowHandlingProcess user enableURLFilter enableURLContentFilter URLFilterRules URLFilterEnable URLFilterEnableContentFilter blacklistURLFilter whitelistURLFilter urlFilterTrustedContent urlFilterRegex embeddedCertificates pinEmbeddedCertificates proxySettingsPolicy 0 proxies ExceptionsList ExcludeSimpleHostnames AutoDiscoveryEnabled AutoConfigurationEnabled AutoConfigurationJavaScript AutoConfigurationURL FTPPassive HTTPEnable HTTPPort 80 HTTPProxy HTTPRequiresPassword HTTPUsername HTTPPassword HTTPSEnable HTTPSPort 443 HTTPSProxy HTTPSRequiresPassword HTTPSUsername HTTPSPassword FTPEnable FTPPort 21 FTPProxy FTPRequiresPassword FTPUsername FTPPassword SOCKSEnable SOCKSPort 1080 SOCKSProxy SOCKSRequiresPassword SOCKSUsername SOCKSPassword RTSPEnable RTSPPort 554 RTSPProxy RTSPRequiresPassword RTSPUsername RTSPPassword sebServicePolicy 1 allowVirtualMachine allowScreenSharing enablePrivateClipboard createNewDesktop killExplorerShell enableLogging logDirectoryOSX ~/Documents logDirectoryWin allowWlan lockOnMessageSocketClose minMacOSVersion 4 enableAppSwitcherCheck forceAppFolderInstall allowUserAppFolderInstall allowSiri allowDictation detectStoppedProcess allowDisplayMirroring allowedDisplaysMaxNumber 1 allowedDisplayBuiltin insideSebEnableSwitchUser insideSebEnableLockThisComputer insideSebEnableChangeAPassword insideSebEnableStartTaskManager insideSebEnableLogOff insideSebEnableShutDown insideSebEnableEaseOfAccess insideSebEnableVmWareClientShade insideSebEnableEnableNetworkConnectionSelector hookKeys enableEsc enableCtrlEsc enableAltEsc enableAltTab enableAltF4 enableStartMenu enableRightMouse enablePrintScreen enableAltMouseWheel enableF1 enableF2 enableF3 enableF4 enableF5 enableF6 enableF7 enableF8 enableF9 enableF10 enableF11 enableF12 PK\755tests/fixtures/simpleunencryptedwithoutoriginator.sebnu[ aaaaaa 1940-10-09T22:13:56Z startURL https://safeexambrowser.org/start startResource sebServerURL hashedAdminPassword allowQuit ignoreExitKeys allowUserAppFolderInstall allowSiri allowDictation detectStoppedProcess allowDisplayMirroring allowedDisplaysMaxNumber 1 allowedDisplayBuiltin PK\~22tests/backup_restore_test.phpnu[. namespace quizaccess_seb; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/test_helper_trait.php'); /** * PHPUnit tests for backup and restore functionality. * * @package quizaccess_seb * @author Dmitrii Metelkin * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class backup_restore_test extends \advanced_testcase { use \quizaccess_seb_test_helper_trait; /** @var template $template A test template. */ protected $template; /** * Called before every test. */ public function setUp(): void { global $USER; parent::setUp(); $this->resetAfterTest(); $this->setAdminUser(); $this->course = $this->getDataGenerator()->create_course(); $this->template = $this->create_template(); $this->user = $USER; } /** * A helper method to create a quiz with template usage of SEB. * * @return seb_quiz_settings */ protected function create_quiz_with_template() { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $quizsettings->set('templateid', $this->template->get('id')); $quizsettings->save(); return $quizsettings; } /** * A helper method to emulate backup and restore of the quiz. * * @return \cm_info|null */ protected function backup_and_restore_quiz() { return duplicate_module($this->course, get_fast_modinfo($this->course)->get_cm($this->quiz->cmid)); } /** * A helper method to backup test quiz. * * @return mixed A backup ID ready to be restored. */ protected function backup_quiz() { global $CFG; // Get the necessary files to perform backup and restore. require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); $backupid = 'test-seb-backup-restore'; $bc = new \backup_controller(\backup::TYPE_1ACTIVITY, $this->quiz->coursemodule, \backup::FORMAT_MOODLE, \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $this->user->id); $bc->execute_plan(); $results = $bc->get_results(); $file = $results['backup_destination']; $fp = get_file_packer('application/vnd.moodle.backup'); $filepath = $CFG->dataroot . '/temp/backup/' . $backupid; $file->extract_to_pathname($fp, $filepath); $bc->destroy(); return $backupid; } /** * A helper method to restore provided backup. * * @param string $backupid Backup ID to restore. */ protected function restore_quiz($backupid) { $rc = new \restore_controller($backupid, $this->course->id, \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $this->user->id, \backup::TARGET_CURRENT_ADDING); $this->assertTrue($rc->execute_precheck()); $rc->execute_plan(); $rc->destroy(); } /** * A helper method to emulate restoring to a different site. */ protected function change_site() { set_config('siteidentifier', random_string(32) . 'not the same site'); } /** * A helper method to validate backup and restore results. * * @param cm_info $newcm Restored course_module object. */ protected function validate_backup_restore(\cm_info $newcm) { $this->assertEquals(2, seb_quiz_settings::count_records()); $actual = seb_quiz_settings::get_record(['quizid' => $newcm->instance]); $expected = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertEquals($expected->get('templateid'), $actual->get('templateid')); $this->assertEquals($expected->get('requiresafeexambrowser'), $actual->get('requiresafeexambrowser')); $this->assertEquals($expected->get('showsebdownloadlink'), $actual->get('showsebdownloadlink')); $this->assertEquals($expected->get('allowuserquitseb'), $actual->get('allowuserquitseb')); $this->assertEquals($expected->get('quitpassword'), $actual->get('quitpassword')); $this->assertEquals($expected->get('allowedbrowserexamkeys'), $actual->get('allowedbrowserexamkeys')); // Validate specific SEB config settings. foreach (settings_provider::get_seb_config_elements() as $name => $notused) { $name = preg_replace("/^seb_/", "", $name); $this->assertEquals($expected->get($name), $actual->get($name)); } } /** * Test backup and restore when no seb. */ public function test_backup_restore_no_seb(): void { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_NO); $this->assertEquals(0, seb_quiz_settings::count_records()); $this->backup_and_restore_quiz(); $this->assertEquals(0, seb_quiz_settings::count_records()); } /** * Test backup and restore when manually configured. */ public function test_backup_restore_manual_config(): void { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $expected = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $expected->set('showsebdownloadlink', 0); $expected->set('quitpassword', '123'); $expected->save(); $this->assertEquals(1, seb_quiz_settings::count_records()); $newcm = $this->backup_and_restore_quiz(); $this->validate_backup_restore($newcm); } /** * Test backup and restore when using template. */ public function test_backup_restore_template_config(): void { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $expected = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $template = $this->create_template(); $expected->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $expected->set('templateid', $template->get('id')); $expected->save(); $this->assertEquals(1, seb_quiz_settings::count_records()); $newcm = $this->backup_and_restore_quiz(); $this->validate_backup_restore($newcm); } /** * Test backup and restore when using template when said template is disabled. * * @covers \quizaccess_seb\seb_quiz_settings::get_record * @covers \restore_quizaccess_seb_subplugin::process_quizaccess_seb_quizsettings */ public function test_backup_restore_disabled_template_config(): void { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $expected = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $template = $this->create_template(); $expected->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $expected->set('templateid', $template->get('id')); $expected->save(); // Disable template. $template->set('enabled', 0); $template->save(); $this->assertEquals(1, seb_quiz_settings::count_records()); $newcm = $this->backup_and_restore_quiz(); $this->assertEquals(2, seb_quiz_settings::count_records()); $actual = seb_quiz_settings::get_record(['quizid' => $newcm->instance]); // Test that the restored quiz no longer uses SEB. $expected = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertEquals(0, $actual->get('templateid')); $this->assertEquals(settings_provider::USE_SEB_NO, $actual->get('requiresafeexambrowser')); $this->assertEquals($expected->get('showsebdownloadlink'), $actual->get('showsebdownloadlink')); $this->assertEquals($expected->get('allowuserquitseb'), $actual->get('allowuserquitseb')); $this->assertEquals($expected->get('quitpassword'), $actual->get('quitpassword')); $this->assertEquals($expected->get('allowedbrowserexamkeys'), $actual->get('allowedbrowserexamkeys')); // Validate specific SEB config settings. foreach (settings_provider::get_seb_config_elements() as $name => $notused) { $name = preg_replace("/^seb_/", "", $name); $this->assertEquals($expected->get($name), $actual->get($name)); } } /** * Test backup and restore when using uploaded file. */ public function test_backup_restore_uploaded_config(): void { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $expected = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $expected->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); $xml = file_get_contents(self::get_fixture_path(__NAMESPACE__, 'unencrypted.seb')); $this->create_module_test_file($xml, $this->quiz->cmid); $expected->save(); $this->assertEquals(1, seb_quiz_settings::count_records()); $newcm = $this->backup_and_restore_quiz(); $this->validate_backup_restore($newcm); $expectedfile = settings_provider::get_module_context_sebconfig_file($this->quiz->cmid); $actualfile = settings_provider::get_module_context_sebconfig_file($newcm->id); $this->assertEquals($expectedfile->get_content(), $actualfile->get_content()); } /** * No new template should be restored if restoring to a different site, * but the template with the same name and content exists.. */ public function test_restore_template_to_a_different_site_when_the_same_template_exists(): void { $this->create_quiz_with_template(); $backupid = $this->backup_quiz(); $this->assertEquals(1, seb_quiz_settings::count_records()); $this->assertEquals(1, template::count_records()); $this->change_site(); $this->restore_quiz($backupid); // Should see additional setting record, but no new template record. $this->assertEquals(2, seb_quiz_settings::count_records()); $this->assertEquals(1, template::count_records()); } /** * A new template should be restored if restoring to a different site, but existing template * has the same content, but different name. */ public function test_restore_template_to_a_different_site_when_the_same_content_but_different_name(): void { $this->create_quiz_with_template(); $backupid = $this->backup_quiz(); $this->assertEquals(1, seb_quiz_settings::count_records()); $this->assertEquals(1, template::count_records()); $this->template->set('name', 'New name for template'); $this->template->save(); $this->change_site(); $this->restore_quiz($backupid); // Should see additional setting record, and new template record. $this->assertEquals(2, seb_quiz_settings::count_records()); $this->assertEquals(2, template::count_records()); } /** * A new template should be restored if restoring to a different site, but existing template * has the same name, but different content. */ public function test_restore_template_to_a_different_site_when_the_same_name_but_different_content(): void { global $CFG; $this->create_quiz_with_template(); $backupid = $this->backup_quiz(); $this->assertEquals(1, seb_quiz_settings::count_records()); $this->assertEquals(1, template::count_records()); $newxml = file_get_contents($CFG->dirroot . '/mod/quiz/accessrule/seb/tests/fixtures/simpleunencrypted.seb'); $this->template->set('content', $newxml); $this->template->save(); $this->change_site(); $this->restore_quiz($backupid); // Should see additional setting record, and new template record. $this->assertEquals(2, seb_quiz_settings::count_records()); $this->assertEquals(2, template::count_records()); } } PK\y++tests/rule_test.phpnu[. namespace quizaccess_seb; use quizaccess_seb; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/test_helper_trait.php'); /** * PHPUnit tests for plugin rule class. * * @package quizaccess_seb * @author Andrew Madden * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \quizaccess_seb */ final class rule_test extends \advanced_testcase { use \quizaccess_seb_test_helper_trait; /** * Called before every test. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(); $this->course = $this->getDataGenerator()->create_course(); } /** * Called after every test. */ public function tearDown(): void { global $SESSION; if (!empty($this->quiz)) { unset($SESSION->quizaccess_seb_access); } parent::tearDown(); } /** * Helper method to get SEB download link for testing. * * @return string */ private function get_seb_download_link() { return 'https://safeexambrowser.org/download_en.html'; } /** * Helper method to get SEB launch link for testing. * * @return string */ private function get_seb_launch_link() { return 'sebs://www.example.com/moodle/mod/quiz/accessrule/seb/config.php'; } /** * Helper method to get SEB config download link for testing. * * @return string */ private function get_seb_config_download_link() { return 'https://www.example.com/moodle/mod/quiz/accessrule/seb/config.php'; } /** * Provider to return valid form field data when saving settings. * * @return array */ public static function valid_form_data_provider(): array { return [ 'valid seb_requiresafeexambrowser' => ['seb_requiresafeexambrowser', '0'], 'valid seb_linkquitseb0' => ['seb_linkquitseb', 'http://safeexambrowser.org/macosx'], 'valid seb_linkquitseb1' => ['seb_linkquitseb', 'safeexambrowser.org/macosx'], 'valid seb_linkquitseb2' => ['seb_linkquitseb', 'www.safeexambrowser.org/macosx'], 'valid seb_linkquitseb3' => ['seb_linkquitseb', 'any.type.of.url.looking.thing'], 'valid seb_linkquitseb4' => ['seb_linkquitseb', 'http://any.type.of.url.looking.thing'], ]; } /** * Provider to return invalid form field data when saving settings. * * @return array */ public static function invalid_form_data_provider(): array { return [ 'invalid seb_requiresafeexambrowser' => ['seb_requiresafeexambrowser', 'Uh oh!'], 'invalid seb_linkquitseb0' => ['seb_linkquitseb', '\0'], 'invalid seb_linkquitseb1' => ['seb_linkquitseb', 'invalid url'], 'invalid seb_linkquitseb2' => ['seb_linkquitseb', 'http]://safeexambrowser.org/macosx'], 'invalid seb_linkquitseb3' => ['seb_linkquitseb', '0'], 'invalid seb_linkquitseb4' => ['seb_linkquitseb', 'seb://any.type.of.url.looking.thing'], ]; } /** * Test no errors are found with valid data. * * @param string $setting * @param string $data * * @dataProvider valid_form_data_provider */ public function test_validate_settings_with_valid_data(string $setting, string $data): void { $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $form = $this->createMock('mod_quiz_mod_form'); $form->method('get_context')->willReturn(\context_module::instance($this->quiz->cmid)); // Validate settings with a dummy form. $errors = quizaccess_seb::validate_settings_form_fields([], [ 'instance' => $this->quiz->id, 'coursemodule' => $this->quiz->cmid, $setting => $data ], [], $form); $this->assertEmpty($errors); } /** * Test errors are found with invalid data. * * @param string $setting * @param string $data * * @dataProvider invalid_form_data_provider */ public function test_validate_settings_with_invalid_data(string $setting, string $data): void { $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $form = $this->createMock('mod_quiz_mod_form'); $form->method('get_context')->willReturn(\context_module::instance($this->quiz->cmid)); // Validate settings with a dummy form and quiz instance. $errors = quizaccess_seb::validate_settings_form_fields([], [ 'instance' => $this->quiz->id, 'coursemodule' => $this->quiz->cmid, $setting => $data ], [], $form); $this->assertEquals([$setting => 'Data submitted is invalid'], $errors); } /** * Test settings validation is not run if settings are locked. */ public function test_settings_validation_is_not_run_if_settings_are_locked(): void { $user = $this->getDataGenerator()->create_user(); $this->quiz = $this->create_test_quiz($this->course); $this->attempt_quiz($this->quiz, $user); $this->setAdminUser(); $form = $this->createMock('mod_quiz_mod_form'); $form->method('get_context')->willReturn(\context_module::instance($this->quiz->cmid)); // Validate settings with a dummy form and quiz instance. $errors = quizaccess_seb::validate_settings_form_fields([], [ 'instance' => $this->quiz->id, 'coursemodule' => $this->quiz->cmid, 'seb_requiresafeexambrowser' => 'Uh oh!' ], [], $form); $this->assertEmpty($errors); } /** * Test settings validation is not run if settings are conflicting. */ public function test_settings_validation_is_not_run_if_conflicting_permissions(): void { $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $form = $this->createMock('mod_quiz_mod_form'); $form->method('get_context')->willReturn(\context_module::instance($this->quiz->cmid)); $user = $this->getDataGenerator()->create_user(); $roleid = $this->getDataGenerator()->create_role(); $context = \context_module::instance($this->quiz->cmid); assign_capability('quizaccess/seb:manage_seb_requiresafeexambrowser', CAP_ALLOW, $roleid, $context->id); $this->getDataGenerator()->role_assign($roleid, $user->id, $context->id); // By default The user won't have permissions to configure manually. $this->setUser($user); // Validate settings with a dummy form and quiz instance. $errors = quizaccess_seb::validate_settings_form_fields([], [ 'instance' => $this->quiz->id, 'coursemodule' => $this->quiz->cmid, 'seb_requiresafeexambrowser' => 'Uh oh!' ], [], $form); $this->assertEmpty($errors); } /** * Test bypassing validation if user don't have permissions to manage seb settings. */ public function test_validate_settings_is_not_run_if_a_user_do_not_have_permissions_to_manage_seb_settings(): void { // Set the user who can't change seb settings. So validation should be bypassed. $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $form = $this->createMock('mod_quiz_mod_form'); $form->method('get_context')->willReturn(\context_module::instance($this->quiz->cmid)); // Validate settings with a dummy form and quiz instance. $errors = quizaccess_seb::validate_settings_form_fields([], [ 'instance' => $this->quiz->id, 'coursemodule' => $this->quiz->cmid, 'seb_requiresafeexambrowser' => 'Uh oh!' ], [], $form); $this->assertEmpty($errors); } /** * Test settings are saved to DB. */ public function test_create_settings_with_existing_quiz(): void { global $DB; $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_NO); $this->assertFalse($DB->record_exists('quizaccess_seb_quizsettings', ['quizid' => $this->quiz->id])); $this->quiz->seb_requiresafeexambrowser = settings_provider::USE_SEB_CONFIG_MANUALLY; quizaccess_seb::save_settings($this->quiz); $this->assertNotFalse($DB->record_exists('quizaccess_seb_quizsettings', ['quizid' => $this->quiz->id])); } /** * Test settings are not saved to DB if settings are locked. */ public function test_settings_are_not_saved_if_settings_are_locked(): void { global $DB; $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $this->attempt_quiz($this->quiz, $user); $this->setAdminUser(); $this->quiz->seb_requiresafeexambrowser = settings_provider::USE_SEB_CONFIG_MANUALLY; quizaccess_seb::save_settings($this->quiz); $this->assertFalse($DB->record_exists('quizaccess_seb_quizsettings', ['quizid' => $this->quiz->id])); } /** * Test settings are not saved to DB if conflicting permissions. */ public function test_settings_are_not_saved_if_conflicting_permissions(): void { $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $user = $this->getDataGenerator()->create_user(); $roleid = $this->getDataGenerator()->create_role(); $context = \context_module::instance($this->quiz->cmid); assign_capability('quizaccess/seb:manage_seb_requiresafeexambrowser', CAP_ALLOW, $roleid, $context->id); $this->getDataGenerator()->role_assign($roleid, $user->id, $context->id); // By default The user won't have permissions to configure manually. $this->setUser($user); $this->quiz->seb_requiresafeexambrowser = settings_provider::USE_SEB_NO; quizaccess_seb::save_settings($this->quiz); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertEquals(settings_provider::USE_SEB_CONFIG_MANUALLY, $quizsettings->get('requiresafeexambrowser')); } /** * Test exception thrown if cm could not be found while saving settings. */ public function test_save_settings_throw_an_exception_if_cm_not_found(): void { global $DB; $this->expectException(\dml_missing_record_exception::class); $this->expectExceptionMessage('Can\'t find data record in database.'); $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $DB->delete_records('quiz', ['id' => $this->quiz->id]); $this->quiz->seb_requiresafeexambrowser = settings_provider::USE_SEB_NO; quizaccess_seb::save_settings($this->quiz); } /** * Test nothing happens when deleted is called without settings saved. */ public function test_delete_settings_without_existing_settings(): void { global $DB; $this->setAdminUser(); $quiz = new \stdClass(); $quiz->id = 1; quizaccess_seb::delete_settings($quiz); $this->assertFalse($DB->record_exists('quizaccess_seb_quizsettings', ['quizid' => $quiz->id])); } /** * Test settings are deleted from DB. */ public function test_delete_settings_with_existing_settings(): void { global $DB; $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); // Using a generator will create the quiz_settings record. $this->assertNotFalse($DB->record_exists('quizaccess_seb_quizsettings', ['quizid' => $this->quiz->id])); quizaccess_seb::delete_settings($this->quiz); $this->assertFalse($DB->record_exists('quizaccess_seb_quizsettings', ['quizid' => $this->quiz->id])); } /** * A helper method to check invalid config key. */ protected function check_invalid_config_key() { // Create an event sink, trigger event and retrieve event. $sink = $this->redirectEvents(); // Check that correct error message is returned. $errormsg = $this->make_rule()->prevent_access(); $this->assertNotEmpty($errormsg); $this->assertStringContainsString("The Safe Exam Browser keys could not be validated. " . "Check that you're using Safe Exam Browser with the correct configuration file.", $errormsg); $this->assertStringContainsString($this->get_seb_download_link(), $errormsg); $this->assertStringContainsString($this->get_seb_launch_link(), $errormsg); $this->assertStringContainsString($this->get_seb_config_download_link(), $errormsg); $events = $sink->get_events(); $this->assertEquals(1, count($events)); $event = reset($events); // Test that the event data is as expected. $this->assertInstanceOf('\quizaccess_seb\event\access_prevented', $event); $this->assertEquals('Invalid SEB config key', $event->other['reason']); } /** * Test access prevented if config key is invalid. */ public function test_access_prevented_if_config_key_invalid(): void { global $FULLME; $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); // Set up dummy request. $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4'; $_SERVER['HTTP_USER_AGENT'] = 'SEB'; $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = 'Broken config key'; $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $this->check_invalid_config_key(); } /** * Test access prevented if config key is invalid when not using the Safe Exam Browser. */ public function test_access_prevented_if_config_key_invalid_not_using_seb(): void { global $FULLME; $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); // Set up dummy request. $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4'; $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = 'Broken config key'; $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $errormsg = $this->make_rule()->prevent_access(); $this->assertNotEmpty($errormsg); $this->assertStringNotContainsString("The Safe Exam Browser keys could not be validated. " . "Check that you're using Safe Exam Browser with the correct configuration file.", $errormsg); } /** * Test access prevented if config keys is invalid and using uploaded config. */ public function test_access_prevented_if_config_key_invalid_uploaded_config(): void { global $FULLME; $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); // Set quiz setting to require seb and save BEK. $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); $xml = file_get_contents(self::get_fixture_path(__NAMESPACE__, 'unencrypted.seb')); $this->create_module_test_file($xml, $this->quiz->cmid); $quizsettings->save(); // Set up dummy request. $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4'; $_SERVER['HTTP_USER_AGENT'] = 'SEB'; $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = 'Broken config key'; $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $this->check_invalid_config_key(); } /** * Test access prevented if config keys is invalid and using template. */ public function test_access_prevented_if_config_key_invalid_uploaded_template(): void { global $FULLME; $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); // Set quiz setting to require seb and save BEK. $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $quizsettings->set('templateid', $this->create_template()->get('id')); $quizsettings->save(); // Set up dummy request. $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4'; $_SERVER['HTTP_USER_AGENT'] = 'SEB'; $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = 'Broken config key'; $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $this->check_invalid_config_key(); } /** * Test access not prevented if config key matches header. */ public function test_access_allowed_if_config_key_valid(): void { global $FULLME; $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Set quiz setting to require seb. $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); // Set up dummy request. $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4'; $expectedhash = hash('sha256', $FULLME . $quizsettings->get_config_key()); $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = $expectedhash; // Check that correct error message is returned. $this->assertFalse($this->make_rule()->prevent_access()); } /** * Test access not prevented if config key matches header. */ public function test_access_allowed_if_config_key_valid_uploaded_config(): void { global $FULLME; $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); // Set quiz setting to require seb and save BEK. $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $quizsettings->set('templateid', $this->create_template()->get('id')); $quizsettings->save(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Set up dummy request. $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4'; $expectedhash = hash('sha256', $FULLME . $quizsettings->get_config_key()); $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = $expectedhash; // Check that correct error message is returned. $this->assertFalse($this->make_rule()->prevent_access()); } /** * Test access not prevented if config key matches header. */ public function test_access_allowed_if_config_key_valid_template(): void { global $FULLME; $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); // Set quiz setting to require seb and save BEK. $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); $xml = file_get_contents(self::get_fixture_path(__NAMESPACE__, 'unencrypted.seb')); $this->create_module_test_file($xml, $this->quiz->cmid); $quizsettings->save(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Set up dummy request. $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4'; $expectedhash = hash('sha256', $FULLME . $quizsettings->get_config_key()); $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = $expectedhash; // Check that correct error message is returned. $this->assertFalse($this->make_rule()->prevent_access()); } /** * Test access not prevented if browser exam keys match headers. */ public function test_access_allowed_if_browser_exam_keys_valid(): void { global $FULLME; $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Set quiz setting to require seb and save BEK. $browserexamkey = hash('sha256', 'testkey'); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_CLIENT_CONFIG); // Doesn't check config key. $quizsettings->set('allowedbrowserexamkeys', $browserexamkey); $quizsettings->save(); // Set up dummy request. $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4'; $expectedhash = hash('sha256', $FULLME . $browserexamkey); $_SERVER['HTTP_X_SAFEEXAMBROWSER_REQUESTHASH'] = $expectedhash; $_SERVER['HTTP_USER_AGENT'] = 'SEB'; // Check that correct error message is returned. $this->assertFalse($this->make_rule()->prevent_access()); } /** * Test access not prevented if browser exam keys match headers. */ public function test_access_allowed_if_browser_exam_keys_valid_use_uploaded_file(): void { global $FULLME; $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); // Set quiz setting to require seb and save BEK. $browserexamkey = hash('sha256', 'testkey'); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); $quizsettings->set('allowedbrowserexamkeys', $browserexamkey); $xml = file_get_contents(self::get_fixture_path(__NAMESPACE__, 'unencrypted.seb')); $this->create_module_test_file($xml, $this->quiz->cmid); $quizsettings->save(); // Set up dummy request. $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4'; $expectedbrowserkey = hash('sha256', $FULLME . $browserexamkey); $_SERVER['HTTP_X_SAFEEXAMBROWSER_REQUESTHASH'] = $expectedbrowserkey; $expectedconfigkey = hash('sha256', $FULLME . $quizsettings->get_config_key()); $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = $expectedconfigkey; $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Check that correct error message is returned. $this->assertFalse($this->make_rule()->prevent_access()); } public function test_access_allowed_if_access_state_stored_in_session(): void { global $SESSION; $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Check that access is prevented. $this->check_invalid_basic_header(); $SESSION->quizaccess_seb_access = [$this->quiz->cmid => true]; // Check access is now not prevented. $this->assertFalse($this->make_rule()->prevent_access()); } /** * A helper method to check invalid browser key. * * @param bool $downloadseblink Make sure download SEB link is present. * @param bool $launchlink Make sure launch SEB link is present. * @param bool $downloadconfiglink Make download config link is present. */ protected function check_invalid_browser_exam_key($downloadseblink = true, $launchlink = true, $downloadconfiglink = true) { // Create an event sink, trigger event and retrieve event. $sink = $this->redirectEvents(); // Check that correct error message is returned. $errormsg = $this->make_rule()->prevent_access(); $this->assertNotEmpty($errormsg); $this->assertStringContainsString("The Safe Exam Browser keys could not be validated. " . "Check that you're using Safe Exam Browser with the correct configuration file.", $errormsg); if ($downloadseblink) { $this->assertStringContainsString($this->get_seb_download_link(), $errormsg); } else { $this->assertStringNotContainsString($this->get_seb_download_link(), $errormsg); } if ($launchlink) { $this->assertStringContainsString($this->get_seb_launch_link(), $errormsg); } else { $this->assertStringNotContainsString($this->get_seb_launch_link(), $errormsg); } if ($downloadconfiglink) { $this->assertStringContainsString($this->get_seb_config_download_link(), $errormsg); } else { $this->assertStringNotContainsString($this->get_seb_config_download_link(), $errormsg); } $events = $sink->get_events(); $this->assertEquals(1, count($events)); $event = reset($events); // Test that the event data is as expected. $this->assertInstanceOf('\quizaccess_seb\event\access_prevented', $event); $this->assertEquals('Invalid SEB browser key', $event->other['reason']); } /** * Test access prevented if browser exam keys do not match headers. */ public function test_access_prevented_if_browser_exam_keys_are_invalid(): void { $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Set quiz setting to require seb and save BEK. $browserexamkey = hash('sha256', 'testkey'); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_CLIENT_CONFIG); // Doesn't check config key. $quizsettings->set('allowedbrowserexamkeys', $browserexamkey); $quizsettings->save(); // Set up dummy request. $_SERVER['HTTP_X_SAFEEXAMBROWSER_REQUESTHASH'] = 'Broken browser key'; $_SERVER['HTTP_USER_AGENT'] = 'SEB'; $this->check_invalid_browser_exam_key(true, false, false); } /** * Test access prevented if browser exam keys do not match headers and using uploaded config. */ public function test_access_prevented_if_browser_exam_keys_are_invalid_use_uploaded_file(): void { global $FULLME; $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); // Set quiz setting to require seb and save BEK. $browserexamkey = hash('sha256', 'testkey'); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); $quizsettings->set('allowedbrowserexamkeys', $browserexamkey); $xml = file_get_contents(self::get_fixture_path(__NAMESPACE__, 'unencrypted.seb')); $this->create_module_test_file($xml, $this->quiz->cmid); $quizsettings->save(); // Set up dummy request. $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4'; $expectedhash = hash('sha256', $FULLME . $quizsettings->get_config_key()); $_SERVER['HTTP_USER_AGENT'] = 'SEB'; $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = $expectedhash; // Set up broken browser key. $_SERVER['HTTP_X_SAFEEXAMBROWSER_REQUESTHASH'] = 'Broken browser key'; $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $this->check_invalid_browser_exam_key(); } /** * Test access not prevented if browser exam keys do not match headers and using template. */ public function test_access_prevented_if_browser_exam_keys_are_invalid_use_template(): void { global $FULLME; $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); // Set quiz setting to require seb and save BEK. $browserexamkey = hash('sha256', 'testkey'); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $quizsettings->set('allowedbrowserexamkeys', $browserexamkey); $quizsettings->set('templateid', $this->create_template()->get('id')); $quizsettings->save(); // Set up dummy request. $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4'; $expectedhash = hash('sha256', $FULLME . $quizsettings->get_config_key()); $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = $expectedhash; // Set up broken browser key. $_SERVER['HTTP_X_SAFEEXAMBROWSER_REQUESTHASH'] = 'Broken browser key'; $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Check that correct error message is returned. $this->assertFalse($this->make_rule()->prevent_access()); } /** * Test access allowed if using client configuration and SEB user agent header is valid. */ public function test_access_allowed_if_using_client_config_basic_header_is_valid(): void { $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Set quiz setting to require seb. $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_CLIENT_CONFIG); // Doesn't check config key. $quizsettings->save(); // Set up basic dummy request. $_SERVER['HTTP_USER_AGENT'] = 'SEB_TEST_SITE'; // Check that correct error message is returned. $this->assertFalse($this->make_rule()->prevent_access()); } /** * Test access prevented if using client configuration and SEB user agent header is invalid. */ public function test_access_prevented_if_using_client_configuration_and_basic_head_is_invalid(): void { $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Set quiz setting to require seb. $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_CLIENT_CONFIG); // Doesn't check config key. $quizsettings->save(); // Set up basic dummy request. $_SERVER['HTTP_USER_AGENT'] = 'WRONG_TEST_SITE'; // Create an event sink, trigger event and retrieve event. $this->check_invalid_basic_header(); } /** * A helper method to check invalid basic header. */ protected function check_invalid_basic_header() { // Create an event sink, trigger event and retrieve event. $sink = $this->redirectEvents(); // Check that correct error message is returned. $this->assertStringContainsString( 'This quiz has been configured to use the Safe Exam Browser with client configuration.', $this->make_rule()->prevent_access() ); $events = $sink->get_events(); $this->assertEquals(1, count($events)); $event = reset($events); // Test that the event data is as expected. $this->assertInstanceOf('\quizaccess_seb\event\access_prevented', $event); $this->assertEquals('No Safe Exam Browser is being used.', $event->other['reason']); } /** * Test access allowed if using client configuration and SEB user agent header is invalid and use uploaded file. */ public function test_access_allowed_if_using_client_configuration_and_basic_head_is_invalid_use_uploaded_config(): void { global $FULLME; $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); // Set quiz setting to require seb. $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); // Doesn't check basic header. $xml = file_get_contents(self::get_fixture_path(__NAMESPACE__, 'unencrypted.seb')); $this->create_module_test_file($xml, $this->quiz->cmid); $quizsettings->save(); // Set up dummy request. $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4'; $expectedhash = hash('sha256', $FULLME . $quizsettings->get_config_key()); $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = $expectedhash; $_SERVER['HTTP_USER_AGENT'] = 'WRONG_TEST_SITE'; $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Check that correct error message is returned. $this->assertFalse($this->make_rule()->prevent_access()); } /** * Test access allowed if using client configuration and SEB user agent header is invalid and use template. */ public function test_access_allowed_if_using_client_configuration_and_basic_head_is_invalid_use_template(): void { global $FULLME; $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); // Set quiz setting to require seb. $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $quizsettings->set('templateid', $this->create_template()->get('id')); $quizsettings->save(); // Set up dummy request. $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4'; $expectedhash = hash('sha256', $FULLME . $quizsettings->get_config_key()); $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = $expectedhash; $_SERVER['HTTP_USER_AGENT'] = 'WRONG_TEST_SITE'; $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Check that correct error message is returned. $this->assertFalse($this->make_rule()->prevent_access()); } /** * Test access not prevented if SEB not required. */ public function test_access_allowed_if_seb_not_required(): void { $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Set quiz setting to not require seb. $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_NO); $quizsettings->save(); // The rule will not exist as the settings are not configured for SEB usage. $this->assertNull($this->make_rule()); } /** * Test access not prevented if USER has bypass capability. */ public function test_access_allowed_if_user_has_bypass_capability(): void { $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Set the bypass SEB check capability to $USER. $this->assign_user_capability('quizaccess/seb:bypassseb', \context_module::instance($this->quiz->cmid)->id); // Check that correct error message is returned. $this->assertFalse($this->make_rule()->prevent_access()); } /** * Test that quiz form cannot be saved if using template, but not actually pick one. */ public function test_mod_quiz_form_cannot_be_saved_using_template_and_template_is_not_set(): void { $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $form = $this->createMock('mod_quiz_mod_form'); $form->method('get_context')->willReturn(\context_module::instance($this->quiz->cmid)); // Validate settings with a dummy form. $errors = quizaccess_seb::validate_settings_form_fields([], [ 'instance' => $this->quiz->id, 'coursemodule' => $this->quiz->cmid, 'seb_requiresafeexambrowser' => settings_provider::USE_SEB_TEMPLATE ], [], $form); $this->assertContains(get_string('invalidtemplate', 'quizaccess_seb'), $errors); } /** * Test that quiz form cannot be saved if uploaded invalid file. */ public function test_mod_quiz_form_cannot_be_saved_using_uploaded_file_and_file_is_not_valid(): void { $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $form = $this->createMock('mod_quiz_mod_form'); $form->method('get_context')->willReturn(\context_module::instance($this->quiz->cmid)); // Validate settings with a dummy form. $errors = quizaccess_seb::validate_settings_form_fields([], [ 'instance' => $this->quiz->id, 'coursemodule' => $this->quiz->cmid, 'seb_requiresafeexambrowser' => settings_provider::USE_SEB_UPLOAD_CONFIG, 'filemanager_sebconfigfile' => 0, ], [], $form); $this->assertContainsEquals(get_string('filenotpresent', 'quizaccess_seb'), $errors); } /** * Test that quiz form cannot be saved if the global settings are set to require a password and no password is set. */ public function test_mod_quiz_form_cannot_be_saved_if_global_settings_force_quiz_password_and_none_is_set(): void { $this->setAdminUser(); // Set global settings to require quiz password but set password to be empty. set_config('quizpasswordrequired', '1', 'quizaccess_seb'); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $form = $this->createMock('mod_quiz_mod_form'); $form->method('get_context')->willReturn(\context_module::instance($this->quiz->cmid)); // Validate settings with a dummy form. $errors = quizaccess_seb::validate_settings_form_fields([], [ 'instance' => $this->quiz->id, 'coursemodule' => $this->quiz->cmid, 'seb_requiresafeexambrowser' => settings_provider::USE_SEB_CONFIG_MANUALLY, ], [], $form); $this->assertContains(get_string('passwordnotset', 'quizaccess_seb'), $errors); } /** * Test that access to quiz is allowed if global setting is set to restrict quiz if no quiz password is set, and global quiz * password is set. */ public function test_mod_quiz_form_can_be_saved_if_global_settings_force_quiz_password_and_is_set(): void { $this->setAdminUser(); // Set global settings to require quiz password but set password to be empty. set_config('quizpasswordrequired', '1', 'quizaccess_seb'); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $form = $this->createMock('mod_quiz_mod_form'); $form->method('get_context')->willReturn(\context_module::instance($this->quiz->cmid)); // Validate settings with a dummy form. $errors = quizaccess_seb::validate_settings_form_fields([], [ 'instance' => $this->quiz->id, 'coursemodule' => $this->quiz->cmid, 'quizpassword' => 'set', 'seb_requiresafeexambrowser' => settings_provider::USE_SEB_CONFIG_MANUALLY, ], [], $form); $this->assertNotContains(get_string('passwordnotset', 'quizaccess_seb'), $errors); } /** * Test that quiz form can be saved if the global settings are set to require a password and no seb usage selected. */ public function test_mod_quiz_form_can_be_saved_if_global_settings_force_quiz_password_and_none_no_seb(): void { $this->setAdminUser(); // Set global settings to require quiz password but set password to be empty. set_config('quizpasswordrequired', '1', 'quizaccess_seb'); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_NO); $form = $this->createMock('mod_quiz_mod_form'); $form->method('get_context')->willReturn(\context_module::instance($this->quiz->cmid)); // Validate settings with a dummy form. $errors = quizaccess_seb::validate_settings_form_fields([], [ 'instance' => $this->quiz->id, 'coursemodule' => $this->quiz->cmid, 'seb_requiresafeexambrowser' => settings_provider::USE_SEB_NO, ], [], $form); $this->assertNotContains(get_string('passwordnotset', 'quizaccess_seb'), $errors); } /** * Test get_download_seb_button, checks for empty config setting quizaccess_seb/downloadlink. */ public function test_get_download_seb_button(): void { $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $reflection = new \ReflectionClass('quizaccess_seb'); $method = $reflection->getMethod('get_download_seb_button'); // The current default contents. $this->assertStringContainsString($this->get_seb_download_link(), $method->invoke($this->make_rule())); set_config('downloadlink', '', 'quizaccess_seb'); // Will not return any button if the URL is empty. $this->assertSame('', $method->invoke($this->make_rule())); } /** * Test get_download_seb_button shows download SEB link when required, */ public function test_get_get_action_buttons_shows_download_seb_link(): void { $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $reflection = new \ReflectionClass('quizaccess_seb'); $method = $reflection->getMethod('get_action_buttons'); $this->assertStringContainsString($this->get_seb_download_link(), $method->invoke($this->make_rule())); $this->quiz->seb_showsebdownloadlink = 0; $this->assertStringNotContainsString($this->get_seb_download_link(), $method->invoke($this->make_rule())); } /** * Test get_download_seb_button shows SEB config related links when required. */ public function test_get_get_action_buttons_shows_launch_and_download_config_links(): void { $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $reflection = new \ReflectionClass('quizaccess_seb'); $method = $reflection->getMethod('get_action_buttons'); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); // Should see link when using manually. $this->assertStringContainsString($this->get_seb_launch_link(), $method->invoke($this->make_rule())); $this->assertStringContainsString($this->get_seb_config_download_link(), $method->invoke($this->make_rule())); // Should see links when using template. $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $quizsettings->set('templateid', $this->create_template()->get('id')); $quizsettings->save(); $this->assertStringContainsString($this->get_seb_launch_link(), $method->invoke($this->make_rule())); $this->assertStringContainsString($this->get_seb_config_download_link(), $method->invoke($this->make_rule())); // Should see links when using uploaded config. $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); $xml = file_get_contents(self::get_fixture_path(__NAMESPACE__, 'unencrypted.seb')); $this->create_module_test_file($xml, $this->quiz->cmid); $quizsettings->save(); $this->assertStringContainsString($this->get_seb_launch_link(), $method->invoke($this->make_rule())); $this->assertStringContainsString($this->get_seb_config_download_link(), $method->invoke($this->make_rule())); // Shouldn't see links if using client config. $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_CLIENT_CONFIG); $quizsettings->save(); $this->assertStringNotContainsString($this->get_seb_launch_link(), $method->invoke($this->make_rule())); $this->assertStringNotContainsString($this->get_seb_config_download_link(), $method->invoke($this->make_rule())); } /** * Test get_download_seb_button shows SEB config related links as configured in "showseblinks". */ public function test_get_get_action_buttons_shows_launch_and_download_config_links_as_configured(): void { $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $reflection = new \ReflectionClass('quizaccess_seb'); $method = $reflection->getMethod('get_action_buttons'); set_config('showseblinks', 'seb,http', 'quizaccess_seb'); $this->assertStringContainsString($this->get_seb_launch_link(), $method->invoke($this->make_rule())); $this->assertStringContainsString($this->get_seb_config_download_link(), $method->invoke($this->make_rule())); set_config('showseblinks', 'http', 'quizaccess_seb'); $this->assertStringNotContainsString($this->get_seb_launch_link(), $method->invoke($this->make_rule())); $this->assertStringContainsString($this->get_seb_config_download_link(), $method->invoke($this->make_rule())); set_config('showseblinks', 'seb', 'quizaccess_seb'); $this->assertStringContainsString($this->get_seb_launch_link(), $method->invoke($this->make_rule())); $this->assertStringNotContainsString($this->get_seb_config_download_link(), $method->invoke($this->make_rule())); set_config('showseblinks', '', 'quizaccess_seb'); $this->assertStringNotContainsString($this->get_seb_launch_link(), $method->invoke($this->make_rule())); $this->assertStringNotContainsString($this->get_seb_config_download_link(), $method->invoke($this->make_rule())); } /** * Test get_quit_button. If attempt count is greater than 0 */ public function test_get_quit_button(): void { $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG); $this->quiz->seb_linkquitseb = "http://test.quit.link"; $user = $this->getDataGenerator()->create_user(); $this->attempt_quiz($this->quiz, $user); $this->setUser($user); // Set-up the button to be called. $reflection = new \ReflectionClass('quizaccess_seb'); $method = $reflection->getMethod('get_quit_button'); $button = $method->invoke($this->make_rule()); $this->assertStringContainsString("http://test.quit.link", $button); } /** * Test description, checks for a valid SEB session and attempt count . */ public function test_description(): void { $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG); $this->quiz->seb_linkquitseb = "http://test.quit.link"; // Set up basic dummy request. $_SERVER['HTTP_USER_AGENT'] = 'SEB_TEST_SITE'; $user = $this->getDataGenerator()->create_user(); $this->attempt_quiz($this->quiz, $user); $description = $this->make_rule()->description(); $this->assertCount(2, $description); $this->assertEquals($description[0], get_string('sebrequired', 'quizaccess_seb')); $this->assertEquals($description[1], ''); // Set the user as display_quit_button() uses the global $USER. $this->setUser($user); $description = $this->make_rule()->description(); $this->assertCount(2, $description); $this->assertEquals($description[0], get_string('sebrequired', 'quizaccess_seb')); // The button is contained in the description when a quiz attempt is finished. $this->assertStringContainsString("http://test.quit.link", $description[1]); } /** * Test description displays download SEB config button when required. */ public function test_description_shows_download_config_link_when_required(): void { $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $user = $this->getDataGenerator()->create_user(); $roleid = $this->getDataGenerator()->create_role(); $context = \context_module::instance($this->quiz->cmid); assign_capability('quizaccess/seb:bypassseb', CAP_ALLOW, $roleid, $context->id); $this->setUser($user); // Can see just basic description with standard perms. $description = $this->make_rule()->description(); $this->assertCount(1, $description); $this->assertEquals($description[0], get_string('sebrequired', 'quizaccess_seb')); // Can see download config link as have bypass SEB permissions. $this->getDataGenerator()->role_assign($roleid, $user->id, $context->id); $description = $this->make_rule()->description(); $this->assertCount(3, $description); $this->assertEquals($description[0], get_string('sebrequired', 'quizaccess_seb')); $this->assertStringContainsString($this->get_seb_config_download_link(), $description[1]); // Can't see download config link as usage method doesn't have SEB config to download. $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_CLIENT_CONFIG); $quizsettings->save(); $description = $this->make_rule()->description(); $this->assertCount(2, $description); $this->assertEquals($description[0], get_string('sebrequired', 'quizaccess_seb')); } /** * Test block display before a quiz started. */ public function test_blocks_display_before_attempt_started(): void { global $PAGE; $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // We will check if we show only fake blocks. Which means no other blocks on a page. $reflection = new \ReflectionClass('block_manager'); $property = $reflection->getProperty('fakeblocksonly'); $this->assertFalse($property->getValue($PAGE->blocks)); // Don't display blocks before start. set_config('displayblocksbeforestart', 0, 'quizaccess_seb'); $this->set_up_quiz_view_page(); $this->make_rule()->prevent_access(); $this->assertEquals('secure', $PAGE->pagelayout); $this->assertTrue($property->getValue($PAGE->blocks)); // Display blocks before start. set_config('displayblocksbeforestart', 1, 'quizaccess_seb'); $this->set_up_quiz_view_page(); $this->make_rule()->prevent_access(); $this->assertEquals('secure', $PAGE->pagelayout); $this->assertFalse($property->getValue($PAGE->blocks)); } /** * Test block display after a quiz completed. */ public function test_blocks_display_after_attempt_finished(): void { global $PAGE; $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG); // Finish the quiz. $user = $this->getDataGenerator()->create_user(); $this->attempt_quiz($this->quiz, $user); $this->setUser($user); // We will check if we show only fake blocks. Which means no other blocks on a page. $reflection = new \ReflectionClass('block_manager'); $property = $reflection->getProperty('fakeblocksonly'); $this->assertFalse($property->getValue($PAGE->blocks)); // Don't display blocks after finish. set_config('displayblockswhenfinished', 0, 'quizaccess_seb'); $this->set_up_quiz_view_page(); $this->make_rule()->prevent_access(); $this->assertEquals('secure', $PAGE->pagelayout); $this->assertTrue($property->getValue($PAGE->blocks)); // Display blocks after finish. set_config('displayblockswhenfinished', 1, 'quizaccess_seb'); $this->set_up_quiz_view_page(); $this->make_rule()->prevent_access(); $this->assertEquals('secure', $PAGE->pagelayout); $this->assertFalse($property->getValue($PAGE->blocks)); } /** * Test cleanup when quiz is completed. */ public function test_current_attempt_finished(): void { global $SESSION; $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->save(); // Set access for Moodle session. $SESSION->quizaccess_seb_access = [$this->quiz->cmid => true]; $this->make_rule()->current_attempt_finished(); $this->assertTrue(empty($SESSION->quizaccess_seb_access[$this->quiz->cmid])); } } PK\ ^^tests/generator/lib.phpnu[. /** * Data generator the quizaccess_seb plugin. * * @package quizaccess_seb * @author Dmitrii Metelkin * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Data generator the quizaccess_seb plugin. * * @package quizaccess_seb * @author Dmitrii Metelkin * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class quizaccess_seb_generator extends component_generator_base { /** * Create SEB template. * * @param array $data Template data. * @return \quizaccess_seb\template */ public function create_template(array $data) { global $CFG; if (!isset($data['name'])) { $data['name'] = 'test'; } if (!isset($data['content'])) { $data['content'] = file_get_contents( $CFG->dirroot . '/mod/quiz/accessrule/seb/tests/fixtures/unencrypted.seb' ); } if (!isset($data['enabled'])) { $data['enabled'] = 1; } $template = new \quizaccess_seb\template(); $template->set('content', $data['content']); $template->set('name', $data['name']); $template->set('enabled', $data['enabled']); $template->save(); return $template; } } PK\o\\2tests/generator/behat_quizaccess_seb_generator.phpnu[. /** * Behat data generator the quizaccess_seb plugin. * * @package quizaccess_seb * @author Dmitrii Metelkin * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Behat data generator for quizaccess_seb. * * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_quizaccess_seb_generator extends behat_generator_base { /** * Get a list of the entities that can be created. * @return array entity name => information about how to generate. */ protected function get_creatable_entities(): array { return [ 'seb templates' => [ 'singular' => 'seb template', 'datagenerator' => 'template', 'required' => ['name'], ], ]; } } PK\np'p',tests/external/validate_quiz_access_test.phpnu[. namespace quizaccess_seb\external; use quizaccess_seb\seb_quiz_settings; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/../test_helper_trait.php'); /** * PHPUnit tests for external function. * * @package quizaccess_seb * @author Andrew Madden * @copyright 2021 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \quizaccess_seb\external\validate_quiz_access */ final class validate_quiz_access_test extends \advanced_testcase { use \quizaccess_seb_test_helper_trait; /** * This method runs before every test. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(); // Generate data objects. $this->course = $this->getDataGenerator()->create_course(); $this->quiz = $this->create_test_quiz($this->course); $this->user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($this->user->id, $this->course->id, 'student'); $this->setUser($this->user); } /** * Bad parameter provider. * * @return array */ public static function bad_parameters_provider(): array { return [ 'no params' => [ 'cmid' => null, 'url' => null, 'configkey' => null, 'messageregex' => '/Invalid parameter value detected \(Missing required key in single structure: cmid\)/' ], 'no course module id' => [ 'cmid' => null, 'url' => 'https://www.example.com/moodle', 'configkey' => hash('sha256', 'configkey'), 'messageregex' => '/Invalid parameter value detected \(Missing required key in single structure: cmid\)/' ], 'no url' => [ 'cmid' => 123, 'url' => null, 'configkey' => hash('sha256', 'configkey'), 'messageregex' => '/Invalid parameter value detected \(Missing required key in single structure: url\)/' ], 'cmid is not an int' => [ 'cmid' => 'test', 'url' => 'https://www.example.com/moodle', 'configkey' => null, 'messageregex' => '/Invalid external api parameter: the value is "test", the server was expecting "int" type/' ], 'url is not a url' => [ 'cmid' => 123, 'url' => 123, 'configkey' => hash('sha256', 'configkey'), 'messageregex' => '/Invalid external api parameter: the value is "123", the server was expecting "url" type/' ], ]; } /** * Test exception thrown for bad parameters. * * @param mixed $cmid Course module id. * @param mixed $url Page URL. * @param mixed $configkey SEB config key. * @param mixed $messageregex Error message regex to check. * * @dataProvider bad_parameters_provider */ public function test_invalid_parameters($cmid, $url, $configkey, $messageregex): void { $params = []; if (!empty($cmid)) { $params['cmid'] = $cmid; } if (!empty($url)) { $params['url'] = $url; } if (!empty($configkey)) { $params['configkey'] = $configkey; } $this->expectException(\invalid_parameter_exception::class); $this->expectExceptionMessageMatches($messageregex); \core_external\external_api::validate_parameters(validate_quiz_keys::execute_parameters(), $params); } /** * Test that the user has permissions to access context. */ public function test_context_is_not_valid_for_user(): void { // Set user as user not enrolled in course and quiz. $this->user = $this->getDataGenerator()->create_user(); $this->setUser($this->user); $this->expectException(\require_login_exception::class); $this->expectExceptionMessage('Course or activity not accessible. (Not enrolled)'); validate_quiz_keys::execute($this->quiz->cmid, 'https://www.example.com/moodle', 'configkey'); } /** * Test exception thrown when no key provided. */ public function test_no_keys_provided(): void { $this->expectException(\invalid_parameter_exception::class); $this->expectExceptionMessage('At least one Safe Exam Browser key must be provided.'); validate_quiz_keys::execute($this->quiz->cmid, 'https://www.example.com/moodle'); } /** * Test exception thrown if cmid doesn't match a quiz. */ public function test_quiz_does_not_exist(): void { $this->setAdminUser(); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $this->course->id]); $this->expectException(\invalid_parameter_exception::class); $this->expectExceptionMessage('Quiz not found matching course module ID: ' . $forum->cmid); validate_quiz_keys::execute($forum->cmid, 'https://www.example.com/moodle', 'configkey'); } /** * Test config key is valid. */ public function test_config_key_valid(): void { $sink = $this->redirectEvents(); // Test settings to populate the quiz. $settings = $this->get_test_settings([ 'quizid' => $this->quiz->id, 'cmid' => $this->quiz->cmid, ]); $url = 'https://www.example.com/moodle'; // Create the quiz settings. $quizsettings = new seb_quiz_settings(0, $settings); $quizsettings->save(); $fullconfigkey = hash('sha256', $url . $quizsettings->get_config_key()); $result = validate_quiz_keys::execute($this->quiz->cmid, $url, $fullconfigkey); $this->assertTrue($result['configkey']); $this->assertTrue($result['browserexamkey']); $events = $sink->get_events(); $this->assertCount(0, $events); } /** * Test config key is not valid. */ public function test_config_key_not_valid(): void { $sink = $this->redirectEvents(); // Test settings to populate the quiz. $settings = $this->get_test_settings([ 'quizid' => $this->quiz->id, 'cmid' => $this->quiz->cmid, ]); // Create the quiz settings. $quizsettings = new seb_quiz_settings(0, $settings); $quizsettings->save(); $result = validate_quiz_keys::execute($this->quiz->cmid, 'https://www.example.com/moodle', 'badconfigkey'); $this->assertFalse($result['configkey']); $this->assertTrue($result['browserexamkey']); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf('\quizaccess_seb\event\access_prevented', $event); $this->assertStringContainsString('Invalid SEB config key', $event->get_description()); } /** * Test browser exam key is valid. */ public function test_browser_exam_key_valid(): void { $sink = $this->redirectEvents(); // Test settings to populate the quiz. $url = 'https://www.example.com/moodle'; $validbrowserexamkey = hash('sha256', 'validbrowserexamkey'); $settings = $this->get_test_settings([ 'quizid' => $this->quiz->id, 'cmid' => $this->quiz->cmid, 'requiresafeexambrowser' => \quizaccess_seb\settings_provider::USE_SEB_CLIENT_CONFIG, 'allowedbrowserexamkeys' => $validbrowserexamkey, ]); // Create the quiz settings. $quizsettings = new seb_quiz_settings(0, $settings); $quizsettings->save(); $fullbrowserexamkey = hash('sha256', $url . $validbrowserexamkey); $result = validate_quiz_keys::execute($this->quiz->cmid, $url, null, $fullbrowserexamkey); $this->assertTrue($result['configkey']); $this->assertTrue($result['browserexamkey']); $events = $sink->get_events(); $this->assertCount(0, $events); } /** * Test browser exam key is not valid. */ public function test_browser_exam_key_not_valid(): void { $sink = $this->redirectEvents(); // Test settings to populate the quiz. $validbrowserexamkey = hash('sha256', 'validbrowserexamkey'); $settings = $this->get_test_settings([ 'quizid' => $this->quiz->id, 'cmid' => $this->quiz->cmid, 'requiresafeexambrowser' => \quizaccess_seb\settings_provider::USE_SEB_CLIENT_CONFIG, 'allowedbrowserexamkeys' => $validbrowserexamkey, ]); // Create the quiz settings. $quizsettings = new seb_quiz_settings(0, $settings); $quizsettings->save(); $result = validate_quiz_keys::execute($this->quiz->cmid, 'https://www.example.com/moodle', null, hash('sha256', 'badbrowserexamkey')); $this->assertTrue($result['configkey']); $this->assertFalse($result['browserexamkey']); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf('\quizaccess_seb\event\access_prevented', $event); $this->assertStringContainsString('Invalid SEB browser key', $event->get_description()); } } PK\9H##tests/privacy/provider_test.phpnu[. /** * PHPUnit tests for privacy provider. * * @package quizaccess_seb * @author Andrew Madden * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace quizaccess_seb\privacy; use core_privacy\local\request\approved_userlist; use core_privacy\local\request\userlist; use core_privacy\local\request\writer; use core_privacy\tests\request\approved_contextlist; use core_privacy\tests\provider_testcase; use quizaccess_seb\privacy\provider; use quizaccess_seb\seb_quiz_settings; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/../test_helper_trait.php'); /** * PHPUnit tests for privacy provider. * * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class provider_test extends provider_testcase { use \quizaccess_seb_test_helper_trait; /** * Setup the user, the quiz and ensure that the user is the last user to modify the SEB quiz settings. */ public function setup_test_data() { $this->resetAfterTest(); $this->setAdminUser(); $this->course = $this->getDataGenerator()->create_course(); $this->quiz = $this->create_test_quiz($this->course, \quizaccess_seb\settings_provider::USE_SEB_CONFIG_MANUALLY); $this->user = $this->getDataGenerator()->create_user(); $this->setUser($this->user); $template = $this->create_template(); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); // Modify settings so usermodified is updated. This is the user data we are testing for. $quizsettings->set('requiresafeexambrowser', \quizaccess_seb\settings_provider::USE_SEB_TEMPLATE); $quizsettings->set('templateid', $template->get('id')); $quizsettings->save(); } /** * Test that the module context for a user who last modified the module is retrieved. */ public function test_get_contexts_for_userid(): void { $this->setup_test_data(); $contexts = provider::get_contexts_for_userid($this->user->id); $contextids = $contexts->get_contextids(); $this->assertEquals(\context_module::instance($this->quiz->cmid)->id, reset($contextids)); } /** * That that no module context is found for a user who has not modified any quiz settings. */ public function test_get_no_contexts_for_userid(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $contexts = provider::get_contexts_for_userid($user->id); $contextids = $contexts->get_contextids(); $this->assertEmpty($contextids); } /** * Test that user data is exported in format expected. */ public function test_export_user_data(): void { $this->setup_test_data(); $context = \context_module::instance($this->quiz->cmid); // Add another course_module of a differenty type - doing this lets us // test that the data exporter is correctly limiting its selection to // the quiz and not anything with the same instance id. // (note this is only effective with databases not using fed (+1000) sequences // per table, like postgres and mysql do, rendering this useless. In any // case better to have the situation covered by some DBs, // like sqlsrv than by none). $this->getDataGenerator()->create_module('label', ['course' => $this->course->id]); $contextlist = provider::get_contexts_for_userid($this->user->id); $approvedcontextlist = new approved_contextlist( $this->user, 'quizaccess_seb', $contextlist->get_contextids() ); writer::reset(); /** @var \core_privacy\tests\request\content_writer $writer */ $writer = writer::with_context($context); $this->assertFalse($writer->has_any_data()); provider::export_user_data($approvedcontextlist); $index = '1'; // Get first data returned from the quizsettings table metadata. $data = $writer->get_data([ get_string('pluginname', 'quizaccess_seb'), seb_quiz_settings::TABLE, $index, ]); $this->assertNotEmpty($data); $index = '1'; // Get first data returned from the template table metadata. $data = $writer->get_data([ get_string('pluginname', 'quizaccess_seb'), \quizaccess_seb\template::TABLE, $index, ]); $this->assertNotEmpty($data); $index = '2'; // There should not be more than one instance with data. $data = $writer->get_data([ get_string('pluginname', 'quizaccess_seb'), seb_quiz_settings::TABLE, $index, ]); $this->assertEmpty($data); $index = '2'; // There should not be more than one instance with data. $data = $writer->get_data([ get_string('pluginname', 'quizaccess_seb'), \quizaccess_seb\template::TABLE, $index, ]); $this->assertEmpty($data); } /** * Test that a userlist with module context is populated by usermodified user. */ public function test_get_users_in_context(): void { $this->setup_test_data(); // Create empty userlist with quiz module context. $userlist = new userlist(\context_module::instance($this->quiz->cmid), 'quizaccess_seb'); // Test that the userlist is populated with expected user/s. provider::get_users_in_context($userlist); $this->assertEquals($this->user->id, $userlist->get_userids()[0]); } /** * Test that data is deleted for a list of users. */ public function test_delete_data_for_users(): void { $this->setup_test_data(); $approveduserlist = new approved_userlist(\context_module::instance($this->quiz->cmid), 'quizaccess_seb', [$this->user->id]); // Test data exists. $this->assertNotEmpty(seb_quiz_settings::get_record(['quizid' => $this->quiz->id])); // Test data is deleted. provider::delete_data_for_users($approveduserlist); $record = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertEmpty($record->get('usermodified')); $template = \quizaccess_seb\template::get_record(['id' => $record->get('templateid')]); $this->assertEmpty($template->get('usermodified')); } /** * Test that data is deleted for a list of contexts. */ public function test_delete_data_for_user(): void { $this->setup_test_data(); $context = \context_module::instance($this->quiz->cmid); $approvedcontextlist = new approved_contextlist($this->user, 'quizaccess_seb', [$context->id]); // Test data exists. $this->assertNotEmpty(seb_quiz_settings::get_record(['quizid' => $this->quiz->id])); // Test data is deleted. provider::delete_data_for_user($approvedcontextlist); $record = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertEmpty($record->get('usermodified')); $template = \quizaccess_seb\template::get_record(['id' => $record->get('templateid')]); $this->assertEmpty($template->get('usermodified')); } /** * Test that data is deleted for a single context. */ public function test_delete_data_for_all_users_in_context(): void { $this->setup_test_data(); $context = \context_module::instance($this->quiz->cmid); // Test data exists. $record = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $template = \quizaccess_seb\template::get_record(['id' => $record->get('templateid')]); $this->assertNotEmpty($record->get('usermodified')); $this->assertNotEmpty($template->get('usermodified')); // Test data is deleted. provider::delete_data_for_all_users_in_context($context); $record = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $template = \quizaccess_seb\template::get_record(['id' => $record->get('templateid')]); $this->assertEmpty($record->get('usermodified')); $this->assertEmpty($template->get('usermodified')); } } PK\5]5]tests/access_manager_test.phpnu[. namespace quizaccess_seb; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/test_helper_trait.php'); /** * PHPUnit tests for the access manager. * * @package quizaccess_seb * @author Andrew Madden * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \quizaccess_seb\seb_access_manager */ final class access_manager_test extends \advanced_testcase { use \quizaccess_seb_test_helper_trait; /** * Called before every test. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(); $this->setAdminUser(); $this->course = $this->getDataGenerator()->create_course(); } /** * Test access_manager private property quizsettings is null. */ public function test_access_manager_quizsettings_null(): void { $this->quiz = $this->create_test_quiz($this->course); $accessmanager = $this->get_access_manager(); $this->assertFalse($accessmanager->seb_required()); $reflection = new \ReflectionClass('\quizaccess_seb\seb_access_manager'); $property = $reflection->getProperty('quizsettings'); $this->assertFalse($property->getValue($accessmanager)); } /** * Test that SEB is not required. */ public function test_seb_required_false(): void { $this->quiz = $this->create_test_quiz($this->course); $accessmanager = $this->get_access_manager(); $this->assertFalse($accessmanager->seb_required()); } /** * Test that SEB is required. */ public function test_seb_required_true(): void { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $accessmanager = $this->get_access_manager(); $this->assertTrue($accessmanager->seb_required()); } /** * Test that user has capability to bypass SEB check. */ public function test_user_can_bypass_seb_check(): void { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Set the bypass SEB check capability to $USER. $this->assign_user_capability('quizaccess/seb:bypassseb', \context_module::instance($this->quiz->cmid)->id); $accessmanager = $this->get_access_manager(); $this->assertTrue($accessmanager->can_bypass_seb()); } /** * Test that user has capability to bypass SEB check. */ public function test_admin_user_can_bypass_seb_check(): void { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); // Test normal user cannot bypass check. $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $accessmanager = $this->get_access_manager(); $this->assertFalse($accessmanager->can_bypass_seb()); // Test with admin user. $this->setAdminUser(); $accessmanager = $this->get_access_manager(); $this->assertTrue($accessmanager->can_bypass_seb()); } /** * Test user does not have capability to bypass SEB check. */ public function test_user_cannot_bypass_seb_check(): void { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $accessmanager = $this->get_access_manager(); $this->assertFalse($accessmanager->can_bypass_seb()); } /** * Test we can detect SEB usage. */ public function test_is_using_seb(): void { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $accessmanager = $this->get_access_manager(); $this->assertFalse($accessmanager->is_using_seb()); $_SERVER['HTTP_USER_AGENT'] = 'Test'; $this->assertFalse($accessmanager->is_using_seb()); $_SERVER['HTTP_USER_AGENT'] = 'SEB'; $this->assertTrue($accessmanager->is_using_seb()); } /** * Test that the quiz Config Key matches the incoming request header. */ public function test_access_keys_validate_with_config_key(): void { global $FULLME; $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $accessmanager = $this->get_access_manager(); $configkey = seb_quiz_settings::get_record(['quizid' => $this->quiz->id])->get_config_key(); // Set up dummy request. $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4'; $expectedhash = hash('sha256', $FULLME . $configkey); $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = $expectedhash; $this->assertTrue($accessmanager->validate_config_key()); } /** * Test that the quiz Config Key matches a provided config key with no incoming request header. */ public function test_access_keys_validate_with_provided_config_key(): void { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $url = 'https://www.example.com/moodle'; $accessmanager = $this->get_access_manager(); $configkey = seb_quiz_settings::get_record(['quizid' => $this->quiz->id])->get_config_key(); $fullconfigkey = hash('sha256', $url . $configkey); $this->assertTrue($accessmanager->validate_config_key($fullconfigkey, $url)); } /** * Test that the quiz Config Key does not match the incoming request header. */ public function test_access_keys_fail_to_validate_with_config_key(): void { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $accessmanager = $this->get_access_manager(); $this->assertFalse($accessmanager->validate_config_key()); } /** * Test that config key is not checked when using client configuration with SEB. */ public function test_config_key_not_checked_if_client_requirement_is_selected(): void { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG); $accessmanager = $this->get_access_manager(); $this->assertFalse($accessmanager->should_validate_config_key()); } /** * Test that if there are no browser exam keys for quiz, check is skipped. */ public function test_no_browser_exam_keys_cause_check_to_be_successful(): void { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG); $settings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $settings->set('allowedbrowserexamkeys', ''); $settings->save(); $accessmanager = $this->get_access_manager(); $this->assertTrue($accessmanager->should_validate_browser_exam_key()); $this->assertTrue($accessmanager->validate_browser_exam_key()); } /** * Test that access fails if there is no hash in header. */ public function test_access_keys_fail_if_browser_exam_key_header_does_not_exist(): void { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG); $settings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $settings->set('allowedbrowserexamkeys', hash('sha256', 'one') . "\n" . hash('sha256', 'two')); $settings->save(); $accessmanager = $this->get_access_manager(); $this->assertFalse($accessmanager->validate_browser_exam_key()); } /** * Test that access fails if browser exam key doesn't match hash in header. */ public function test_access_keys_fail_if_browser_exam_key_header_does_not_match_provided_hash(): void { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG); $settings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $settings->set('allowedbrowserexamkeys', hash('sha256', 'one') . "\n" . hash('sha256', 'two')); $settings->save(); $accessmanager = $this->get_access_manager(); $_SERVER['HTTP_X_SAFEEXAMBROWSER_REQUESTHASH'] = hash('sha256', 'notwhatyouwereexpectinghuh'); $this->assertFalse($accessmanager->validate_browser_exam_key()); } /** * Test that browser exam key matches hash in header. */ public function test_browser_exam_keys_match_header_hash(): void { global $FULLME; $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG); $settings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $browserexamkey = hash('sha256', 'browserexamkey'); $settings->set('allowedbrowserexamkeys', $browserexamkey); // Add a hashed BEK. $settings->save(); $accessmanager = $this->get_access_manager(); // Set up dummy request. $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4'; $expectedhash = hash('sha256', $FULLME . $browserexamkey); $_SERVER['HTTP_X_SAFEEXAMBROWSER_REQUESTHASH'] = $expectedhash; $this->assertTrue($accessmanager->validate_browser_exam_key()); } /** * Test that browser exam key matches a provided browser exam key. */ public function test_browser_exam_keys_match_provided_browser_exam_key(): void { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG); $url = 'https://www.example.com/moodle'; $settings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $browserexamkey = hash('sha256', 'browserexamkey'); $fullbrowserexamkey = hash('sha256', $url . $browserexamkey); $settings->set('allowedbrowserexamkeys', $browserexamkey); // Add a hashed BEK. $settings->save(); $accessmanager = $this->get_access_manager(); $this->assertTrue($accessmanager->validate_browser_exam_key($fullbrowserexamkey, $url)); } /** * Test can get received config key. */ public function test_get_received_config_key(): void { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG); $accessmanager = $this->get_access_manager(); $this->assertNull($accessmanager->get_received_config_key()); $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = 'Test key'; $this->assertEquals('Test key', $accessmanager->get_received_config_key()); } /** * Test can get received browser key. */ public function get_received_browser_exam_key() { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG); $accessmanager = $this->get_access_manager(); $this->assertNull($accessmanager->get_received_browser_exam_key()); $_SERVER['HTTP_X_SAFEEXAMBROWSER_REQUESTHASH'] = 'Test browser key'; $this->assertEquals('Test browser key', $accessmanager->get_received_browser_exam_key()); } /** * Test can correctly get type of SEB usage for the quiz. */ public function test_get_seb_use_type(): void { // No SEB. $this->quiz = $this->create_test_quiz($this->course); $accessmanager = $this->get_access_manager(); $this->assertEquals(settings_provider::USE_SEB_NO, $accessmanager->get_seb_use_type()); // Manually. $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $accessmanager = $this->get_access_manager(); $this->assertEquals(settings_provider::USE_SEB_CONFIG_MANUALLY, $accessmanager->get_seb_use_type()); // Use template. $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $quizsettings->set('templateid', $this->create_template()->get('id')); $quizsettings->save(); $accessmanager = $this->get_access_manager(); $this->assertEquals(settings_provider::USE_SEB_TEMPLATE, $accessmanager->get_seb_use_type()); // Use uploaded config. $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); // Doesn't check basic header. $xml = file_get_contents(self::get_fixture_path(__NAMESPACE__, 'unencrypted.seb')); $this->create_module_test_file($xml, $this->quiz->cmid); $quizsettings->save(); $accessmanager = $this->get_access_manager(); $this->assertEquals(settings_provider::USE_SEB_UPLOAD_CONFIG, $accessmanager->get_seb_use_type()); // Use client config. $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG); $accessmanager = $this->get_access_manager(); $this->assertEquals(settings_provider::USE_SEB_CLIENT_CONFIG, $accessmanager->get_seb_use_type()); } /** * Data provider for self::test_should_validate_basic_header. * * @return array */ public static function should_validate_basic_header_data_provider(): array { return [ [settings_provider::USE_SEB_NO, false], [settings_provider::USE_SEB_CONFIG_MANUALLY, false], [settings_provider::USE_SEB_TEMPLATE, false], [settings_provider::USE_SEB_UPLOAD_CONFIG, false], [settings_provider::USE_SEB_CLIENT_CONFIG, true], ]; } /** * Test we know when we should validate basic header. * * @param int $type Type of SEB usage. * @param bool $expected Expected result. * * @dataProvider should_validate_basic_header_data_provider */ public function test_should_validate_basic_header($type, $expected): void { $accessmanager = $this->getMockBuilder(seb_access_manager::class) ->disableOriginalConstructor() ->onlyMethods(['get_seb_use_type']) ->getMock(); $accessmanager->method('get_seb_use_type')->willReturn($type); $this->assertEquals($expected, $accessmanager->should_validate_basic_header()); } /** * Data provider for self::test_should_validate_config_key. * * @return array */ public static function should_validate_config_key_data_provider(): array { return [ [settings_provider::USE_SEB_NO, false], [settings_provider::USE_SEB_CONFIG_MANUALLY, true], [settings_provider::USE_SEB_TEMPLATE, true], [settings_provider::USE_SEB_UPLOAD_CONFIG, true], [settings_provider::USE_SEB_CLIENT_CONFIG, false], ]; } /** * Test we know when we should validate config key. * * @param int $type Type of SEB usage. * @param bool $expected Expected result. * * @dataProvider should_validate_config_key_data_provider */ public function test_should_validate_config_key($type, $expected): void { $accessmanager = $this->getMockBuilder(seb_access_manager::class) ->disableOriginalConstructor() ->onlyMethods(['get_seb_use_type']) ->getMock(); $accessmanager->method('get_seb_use_type')->willReturn($type); $this->assertEquals($expected, $accessmanager->should_validate_config_key()); } /** * Data provider for self::test_should_validate_browser_exam_key. * * @return array */ public static function should_validate_browser_exam_key_data_provider(): array { return [ [settings_provider::USE_SEB_NO, false], [settings_provider::USE_SEB_CONFIG_MANUALLY, false], [settings_provider::USE_SEB_TEMPLATE, false], [settings_provider::USE_SEB_UPLOAD_CONFIG, true], [settings_provider::USE_SEB_CLIENT_CONFIG, true], ]; } /** * Test we know when we should browser exam key. * * @param int $type Type of SEB usage. * @param bool $expected Expected result. * * @dataProvider should_validate_browser_exam_key_data_provider */ public function test_should_validate_browser_exam_key($type, $expected): void { $accessmanager = $this->getMockBuilder(seb_access_manager::class) ->disableOriginalConstructor() ->onlyMethods(['get_seb_use_type']) ->getMock(); $accessmanager->method('get_seb_use_type')->willReturn($type); $this->assertEquals($expected, $accessmanager->should_validate_browser_exam_key()); } /** * Test that access manager uses cached Config Key. */ public function test_access_manager_uses_cached_config_key(): void { global $FULLME; $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $accessmanager = $this->get_access_manager(); $configkey = $accessmanager->get_valid_config_key(); // Set up dummy request. $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4'; $expectedhash = hash('sha256', $FULLME . $configkey); $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = $expectedhash; $this->assertTrue($accessmanager->validate_config_key()); // Change settings (but don't save) and check that still can validate config key. $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('showsebtaskbar', 0); $this->assertNotEquals($quizsettings->get_config_key(), $configkey); $this->assertTrue($accessmanager->validate_config_key()); // Now save settings which should purge caches but access manager still has config key. $quizsettings->save(); $this->assertNotEquals($quizsettings->get_config_key(), $configkey); $this->assertTrue($accessmanager->validate_config_key()); // Initialise a new access manager. Now validation should fail. $accessmanager = $this->get_access_manager(); $this->assertFalse($accessmanager->validate_config_key()); } /** * Check that valid SEB config key is null if quiz doesn't have SEB settings. */ public function test_valid_config_key_is_null_if_no_settings(): void { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_NO); $accessmanager = $this->get_access_manager(); $this->assertEmpty(seb_quiz_settings::get_record(['quizid' => $this->quiz->id])); $this->assertNull($accessmanager->get_valid_config_key()); } /** * Test if config key should not be validated. */ public function test_if_config_key_should_not_be_validated(): void { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_NO); $accessmanager = $this->get_access_manager(); $this->assertTrue($accessmanager->validate_config_key()); } /** * Test if browser exam key should not be validated. */ public function test_if_browser_exam_key_should_not_be_validated(): void { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $accessmanager = $this->get_access_manager(); $this->assertTrue($accessmanager->validate_browser_exam_key()); } /** * Test that access is set correctly in Moodle session. */ public function test_set_session_access(): void { global $SESSION; $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG); $accessmanager = $this->get_access_manager(); $this->assertTrue(empty($SESSION->quizaccess_seb_access[$this->quiz->cmid])); $accessmanager->set_session_access(true); $this->assertTrue($SESSION->quizaccess_seb_access[$this->quiz->cmid]); } /** * Test that access is set in Moodle session for only course module associated with access manager. */ public function test_session_access_set_for_specific_course_module(): void { global $SESSION; $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG); $quiz2 = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG); $accessmanager = $this->get_access_manager(); $accessmanager->set_session_access(true); $this->assertCount(1, $SESSION->quizaccess_seb_access); $this->assertTrue($SESSION->quizaccess_seb_access[$this->quiz->cmid]); $this->assertTrue(empty($SESSION->quizaccess_seb_access[$quiz2->cmid])); } /** * Test that access state can be retrieved from Moodle session. */ public function test_validate_session_access(): void { global $SESSION; $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG); $accessmanager = $this->get_access_manager(); $this->assertEmpty($accessmanager->validate_session_access()); $SESSION->quizaccess_seb_access[$this->quiz->cmid] = true; $this->assertTrue($accessmanager->validate_session_access()); } /** * Test that access can be cleared from Moodle session. */ public function test_clear_session_access(): void { global $SESSION; $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG); $accessmanager = $this->get_access_manager(); $SESSION->quizaccess_seb_access[$this->quiz->cmid] = true; $accessmanager->clear_session_access(); $this->assertTrue(empty($SESSION->quizaccess_seb_access[$this->quiz->cmid])); } /** * Test we can decide if need to redirect to SEB config link. */ public function test_should_redirect_to_seb_config_link(): void { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $accessmanager = $this->get_access_manager(); set_config('autoreconfigureseb', '1', 'quizaccess_seb'); $_SERVER['HTTP_USER_AGENT'] = 'SEB'; $this->assertFalse($accessmanager->should_redirect_to_seb_config_link()); set_config('autoreconfigureseb', '1', 'quizaccess_seb'); $_SERVER['HTTP_USER_AGENT'] = 'SEB'; $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = hash('sha256', 'configkey'); $this->assertTrue($accessmanager->should_redirect_to_seb_config_link()); } } PK\$$tests/template_test.phpnu[. namespace quizaccess_seb; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/test_helper_trait.php'); /** * PHPUnit tests for template class. * * @package quizaccess_seb * @author Dmitrii Metelkin * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class template_test extends \advanced_testcase { use \quizaccess_seb_test_helper_trait; /** * Called before every test. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(); } /** * Test that template saved with valid content. */ public function test_template_is_saved(): void { global $DB; $data = new \stdClass(); $data->name = 'Test name'; $data->description = 'Test description'; $data->enabled = 1; $data->content = " showTaskBarallowWlanshowReloadButton" . "showTimeshowInputLanguageallowQuit" . "quitURLConfirmaudioControlEnabledaudioMute" . "browserMediaCaptureCamerabrowserMediaCaptureMicrophone" . "allowSpellCheckbrowserWindowAllowReloadURLFilterEnable" . "URLFilterEnableContentFilterhashedQuitPassword" . "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08URLFilterRules" . "action1activeexpression" . "test.comregex" . "sendBrowserExamKey\n"; $template = new template(0, $data); $template->save(); $actual = $DB->get_record(template::TABLE, ['id' => $template->get('id')]); $this->assertEquals($data->name, $actual->name); $this->assertEquals($data->description, $actual->description); $this->assertEquals($data->enabled, $actual->enabled); $this->assertEquals($data->content, $actual->content); $this->assertTrue($template->can_delete()); } /** * Test that template is not saved with invalid content. */ public function test_template_is_not_saved_with_invalid_content(): void { $this->expectException(\core\invalid_persistent_exception::class); $this->expectExceptionMessage('Invalid SEB config template'); $data = new \stdClass(); $data->name = 'Test name'; $data->description = 'Test description'; $data->enabled = 1; $data->content = "Invalid content"; $template = new template(0, $data); $template->save(); } /** * Test that a template cannot be deleted when assigned to a quiz. */ public function test_cannot_delete_template_when_assigned_to_quiz(): void { global $DB; $data = new \stdClass(); $data->name = 'Test name'; $data->description = 'Test description'; $data->enabled = 1; $data->content = " showTaskBarallowWlanshowReloadButton" . "showTimeshowInputLanguageallowQuit" . "quitURLConfirmaudioControlEnabledaudioMute" . "browserMediaCaptureCamerabrowserMediaCaptureMicrophone" . "allowSpellCheckbrowserWindowAllowReloadURLFilterEnable" . "URLFilterEnableContentFilterhashedQuitPassword" . "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08URLFilterRules" . "action1activeexpression" . "test.comregex" . "sendBrowserExamKey\n"; $template = new template(0, $data); $template->save(); $this->assertTrue($template->can_delete()); $DB->insert_record(seb_quiz_settings::TABLE, (object) [ 'quizid' => 1, 'cmid' => 1, 'templateid' => $template->get('id'), 'requiresafeexambrowser' => '1', 'sebconfigfile' => '373552893', 'showsebtaskbar' => '1', 'showwificontrol' => '0', 'showreloadbutton' => '1', 'showtime' => '0', 'showkeyboardlayout' => '1', 'allowuserquitseb' => '1', 'quitpassword' => 'test', 'linkquitseb' => '', 'userconfirmquit' => '1', 'enableaudiocontrol' => '1', 'allowcapturecamera' => '1', 'allowcapturemicrophone' => '1', 'muteonstartup' => '0', 'allowspellchecking' => '0', 'allowreloadinexam' => '1', 'activateurlfiltering' => '1', 'filterembeddedcontent' => '0', 'expressionsallowed' => 'test.com', 'regexallowed' => '', 'expressionsblocked' => '', 'regexblocked' => '', 'showsebdownloadlink' => '1', 'config' => '', ]); $this->assertFalse($template->can_delete()); } /** * Test that a disabled template no longer shows up in quiz SEB settings other than quizzes already using it. * * @covers \quizaccess_seb\seb_quiz_settings::get_record * @covers \quizaccess_seb\settings_provider::get_requiresafeexambrowser_options */ public function test_disabled_template_quiz_setting_options(): void { // Create quiz and fetch standard SEB requirement options. $this->setAdminUser(); $this->course = $this->getDataGenerator()->create_course(); $templateoptionstr = get_string('seb_use_template', 'quizaccess_seb'); // Create a quiz. $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $context = \context_module::instance($this->quiz->cmid); // Check there is no template option (as there aren't any). $options = settings_provider::get_requiresafeexambrowser_options($context); $this->assertNotContainsEquals($templateoptionstr, $options); // Create a template. $data = new \stdClass(); $data->name = 'Test name'; $data->description = 'Test description'; $data->enabled = 1; $data->content = file_get_contents(self::get_fixture_path(__NAMESPACE__, 'unencrypted.seb')); $template = new template(0, $data); $template->save(); // Check options now include template option. $options = settings_provider::get_requiresafeexambrowser_options($context); $this->assertContainsEquals($templateoptionstr, $options); // Set SEB setting to use template for quiz. $settings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $settings->set('templateid', $template->get('id')); $settings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $settings->save(); // Disable template. $template->set('enabled', 0); $template->save(); // Check option still exists on current quiz. $options = settings_provider::get_requiresafeexambrowser_options($context); $this->assertContainsEquals($templateoptionstr, $options); // Create a new quiz. $newquiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $context = \context_module::instance($newquiz->cmid); // Check there is no template option (as the template is now disabled). $options = settings_provider::get_requiresafeexambrowser_options($context); $this->assertNotContainsEquals($templateoptionstr, $options); } } PK\>>tests/quiz_settings_test.phpnu[. namespace quizaccess_seb; use context_module; use moodle_url; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/test_helper_trait.php'); /** * PHPUnit tests for seb_quiz_settings class. * * @package quizaccess_seb * @author Andrew Madden * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class quiz_settings_test extends \advanced_testcase { use \quizaccess_seb_test_helper_trait; /** @var context_module $context Test context. */ protected $context; /** @var moodle_url $url Test quiz URL. */ protected $url; /** * Called before every test. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(); $this->setAdminUser(); $this->course = $this->getDataGenerator()->create_course(); $this->quiz = $this->getDataGenerator()->create_module('quiz', [ 'course' => $this->course->id, 'seb_requiresafeexambrowser' => settings_provider::USE_SEB_CONFIG_MANUALLY, ]); $this->context = \context_module::instance($this->quiz->cmid); $this->url = new \moodle_url("/mod/quiz/view.php", ['id' => $this->quiz->cmid]); } /** * Test that config is generated immediately prior to saving quiz settings. */ public function test_config_is_created_from_quiz_settings(): void { // Test settings to populate the in the object. $settings = $this->get_test_settings([ 'quizid' => $this->quiz->id, 'cmid' => $this->quiz->cmid, ]); // Obtain the existing record that is created when using a generator. $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); // Update the settings with values from the test function. $quizsettings->from_record($settings); $quizsettings->save(); $config = $quizsettings->get_config(); $this->assertEquals( " showTaskBarallowWlanshowReloadButton" . "showTimeshowInputLanguageallowQuit" . "quitURLConfirmaudioControlEnabledaudioMute" . "browserMediaCaptureCamerabrowserMediaCaptureMicrophone" . "allowSpellCheckbrowserWindowAllowReloadURLFilterEnable" . "URLFilterEnableContentFilterhashedQuitPassword" . "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08URLFilterRules" . "action1activeexpression" . "test.comregexstartURL$this->url" . "sendBrowserExamKeybrowserWindowWebView3" . "examSessionClearCookiesOnStartallowPreferencesWindow\n", $config); } /** * Test that config string gets updated from quiz settings. */ public function test_config_is_updated_from_quiz_settings(): void { // Test settings to populate the in the object. $settings = $this->get_test_settings([ 'quizid' => $this->quiz->id, 'cmid' => $this->quiz->cmid, ]); // Obtain the existing record that is created when using a generator. $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); // Update the settings with values from the test function. $quizsettings->from_record($settings); $quizsettings->save(); $config = $quizsettings->get_config(); $this->assertEquals(" showTaskBarallowWlanshowReloadButton" . "showTimeshowInputLanguageallowQuit" . "quitURLConfirmaudioControlEnabledaudioMute" . "browserMediaCaptureCamerabrowserMediaCaptureMicrophone" . "allowSpellCheckbrowserWindowAllowReloadURLFilterEnable" . "URLFilterEnableContentFilterhashedQuitPassword" . "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08URLFilterRules" . "action1activeexpression" . "test.comregexstartURL$this->url" . "sendBrowserExamKeybrowserWindowWebView3" . "examSessionClearCookiesOnStart" . "allowPreferencesWindow\n", $config); $quizsettings->set('filterembeddedcontent', 1); // Alter the settings. $quizsettings->save(); $config = $quizsettings->get_config(); $this->assertEquals(" showTaskBarallowWlanshowReloadButton" . "showTimeshowInputLanguageallowQuit" . "quitURLConfirmaudioControlEnabledaudioMute" . "browserMediaCaptureCamerabrowserMediaCaptureMicrophone" . "allowSpellCheckbrowserWindowAllowReloadURLFilterEnable" . "URLFilterEnableContentFilterhashedQuitPassword" . "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08URLFilterRules" . "action1activeexpression" . "test.comregexstartURL$this->url" . "sendBrowserExamKeybrowserWindowWebView3" . "examSessionClearCookiesOnStart" . "allowPreferencesWindow\n", $config); } /** * Test that config key is generated immediately prior to saving quiz settings. */ public function test_config_key_is_created_from_quiz_settings(): void { $settings = $this->get_test_settings(); $quizsettings = new seb_quiz_settings(0, $settings); $configkey = $quizsettings->get_config_key(); $this->assertEquals("24712dcecc69a2ca8ad19f4f8928375038f087e13ce9f504cfc4496846aaaf27", $configkey ); } /** * Test that config key is generated immediately prior to saving quiz settings. */ public function test_config_key_is_updated_from_quiz_settings(): void { $settings = $this->get_test_settings(); $quizsettings = new seb_quiz_settings(0, $settings); $configkey = $quizsettings->get_config_key(); $this->assertEquals("24712dcecc69a2ca8ad19f4f8928375038f087e13ce9f504cfc4496846aaaf27", $configkey); $quizsettings->set('filterembeddedcontent', 1); // Alter the settings. $configkey = $quizsettings->get_config_key(); $this->assertEquals("e2f6dc2a6ddb9bdfa7530dc8afbae996499c34a954e1c635c4fed894e47c1abc", $configkey); } /** * Test that different URL filter expressions are turned into config XML. * * @param \stdClass $settings Quiz settings * @param string $expectedxml SEB Config XML. * * @dataProvider filter_rules_provider */ public function test_filter_rules_added_to_config(\stdClass $settings, string $expectedxml): void { $quizsettings = new seb_quiz_settings(0, $settings); $config = $quizsettings->get_config(); $this->assertEquals($expectedxml, $config); } /** * Test that browser keys are validated and retrieved as an array instead of string. */ public function test_browser_exam_keys_are_retrieved_as_array(): void { $quizsettings = new seb_quiz_settings(); $quizsettings->set('allowedbrowserexamkeys', "one two,three\nfour"); $retrievedkeys = $quizsettings->get('allowedbrowserexamkeys'); $this->assertEquals(['one', 'two', 'three', 'four'], $retrievedkeys); } /** * Test validation of Browser Exam Keys. * * @param string $bek Browser Exam Key. * @param string $expectederrorstring Expected error. * * @dataProvider bad_browser_exam_key_provider */ public function test_browser_exam_keys_validation_errors($bek, $expectederrorstring): void { $quizsettings = new seb_quiz_settings(); $quizsettings->set('allowedbrowserexamkeys', $bek); $quizsettings->validate(); $errors = $quizsettings->get_errors(); $this->assertContainsEquals($expectederrorstring, $errors); } /** * Test that uploaded seb file gets converted to config string. */ public function test_config_file_uploaded_converted_to_config(): void { $url = new \moodle_url("/mod/quiz/view.php", ['id' => $this->quiz->cmid]); $xml = "\n" . "\n" . "hashedQuitPasswordhashedpassword" . "allowWlanstartURL$url" . "sendBrowserExamKeybrowserWindowWebView3\n"; $itemid = $this->create_module_test_file($xml, $this->quiz->cmid); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); $quizsettings->save(); $config = $quizsettings->get_config(); $this->assertEquals($xml, $config); } /** * Test test_no_config_file_uploaded */ public function test_no_config_file_uploaded(): void { $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); $cmid = $quizsettings->get('cmid'); $this->expectException(\moodle_exception::class); $this->expectExceptionMessage("No uploaded SEB config file could be found for quiz with cmid: {$cmid}"); $quizsettings->get_config(); } /** * A helper function to build a config file. * * @param mixed $allowuserquitseb Required allowQuit setting. * @param mixed $quitpassword Required hashedQuitPassword setting. * * @return string */ protected function get_config_xml($allowuserquitseb = null, $quitpassword = null) { $xml = "\n" . "\n" . "allowWlanstartURL" . "https://safeexambrowser.org/start" . "sendBrowserExamKey"; if (!is_null($allowuserquitseb)) { $allowuserquitseb = empty($allowuserquitseb) ? 'false' : 'true'; $xml .= "allowQuit<{$allowuserquitseb}/>"; } if (!is_null($quitpassword)) { $xml .= "hashedQuitPassword{$quitpassword}"; } $xml .= "\n"; return $xml; } /** * Test using USE_SEB_TEMPLATE and have it override settings from the template when they are set. */ public function test_using_seb_template_override_settings_when_they_set_in_template(): void { $xml = $this->get_config_xml(true, 'password'); $template = $this->create_template($xml); $this->assertStringContainsString("startURLhttps://safeexambrowser.org/start", $template->get('content')); $this->assertStringContainsString("allowQuit", $template->get('content')); $this->assertStringContainsString("hashedQuitPasswordpassword", $template->get('content')); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $quizsettings->set('templateid', $template->get('id')); $quizsettings->set('allowuserquitseb', 1); $quizsettings->save(); $this->assertStringContainsString( "startURLhttps://www.example.com/moodle/mod/quiz/view.php?id={$this->quiz->cmid}", $quizsettings->get_config() ); $this->assertStringContainsString("allowQuit", $quizsettings->get_config()); $this->assertStringNotContainsString("hashedQuitPassword", $quizsettings->get_config()); $quizsettings->set('quitpassword', 'new password'); $quizsettings->save(); $hashedpassword = hash('SHA256', 'new password'); $this->assertStringContainsString("allowQuit", $quizsettings->get_config()); $this->assertStringNotContainsString("hashedQuitPasswordpassword", $quizsettings->get_config()); $this->assertStringContainsString("hashedQuitPassword{$hashedpassword}", $quizsettings->get_config()); $quizsettings->set('allowuserquitseb', 0); $quizsettings->set('quitpassword', ''); $quizsettings->save(); $this->assertStringContainsString("allowQuit", $quizsettings->get_config()); $this->assertStringNotContainsString("hashedQuitPassword", $quizsettings->get_config()); } /** * Test using USE_SEB_TEMPLATE and have it override settings from the template when they are not set. */ public function test_using_seb_template_override_settings_when_not_set_in_template(): void { $xml = $this->get_config_xml(); $template = $this->create_template($xml); $this->assertStringContainsString("startURLhttps://safeexambrowser.org/start", $template->get('content')); $this->assertStringNotContainsString("allowQuit", $template->get('content')); $this->assertStringNotContainsString("hashedQuitPasswordpassword", $template->get('content')); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $quizsettings->set('templateid', $template->get('id')); $quizsettings->set('allowuserquitseb', 1); $quizsettings->save(); $this->assertStringContainsString("allowQuit", $quizsettings->get_config()); $this->assertStringNotContainsString("hashedQuitPassword", $quizsettings->get_config()); $quizsettings->set('quitpassword', 'new password'); $quizsettings->save(); $hashedpassword = hash('SHA256', 'new password'); $this->assertStringContainsString("allowQuit", $quizsettings->get_config()); $this->assertStringContainsString("hashedQuitPassword{$hashedpassword}", $quizsettings->get_config()); $quizsettings->set('allowuserquitseb', 0); $quizsettings->set('quitpassword', ''); $quizsettings->save(); $this->assertStringContainsString("allowQuit", $quizsettings->get_config()); $this->assertStringNotContainsString("hashedQuitPassword", $quizsettings->get_config()); } /** * Test using USE_SEB_UPLOAD_CONFIG and use settings from the file if they are set. */ public function test_using_own_config_settings_are_not_overridden_if_set(): void { $xml = $this->get_config_xml(true, 'password'); $this->create_module_test_file($xml, $this->quiz->cmid); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); $quizsettings->set('allowuserquitseb', 0); $quizsettings->set('quitpassword', ''); $quizsettings->save(); $this->assertStringContainsString( "startURLhttps://www.example.com/moodle/mod/quiz/view.php?id={$this->quiz->cmid}", $quizsettings->get_config() ); $this->assertStringContainsString("allowQuit", $quizsettings->get_config()); $this->assertStringContainsString("hashedQuitPasswordpassword", $quizsettings->get_config()); $quizsettings->set('quitpassword', 'new password'); $quizsettings->save(); $hashedpassword = hash('SHA256', 'new password'); $this->assertStringNotContainsString("hashedQuitPassword{$hashedpassword}", $quizsettings->get_config()); $this->assertStringContainsString("allowQuit", $quizsettings->get_config()); $this->assertStringContainsString("hashedQuitPasswordpassword", $quizsettings->get_config()); $quizsettings->set('allowuserquitseb', 0); $quizsettings->set('quitpassword', ''); $quizsettings->save(); $this->assertStringContainsString("allowQuit", $quizsettings->get_config()); $this->assertStringContainsString("hashedQuitPasswordpassword", $quizsettings->get_config()); } /** * Test using USE_SEB_UPLOAD_CONFIG and use settings from the file if they are not set. */ public function test_using_own_config_settings_are_not_overridden_if_not_set(): void { $xml = $this->get_config_xml(); $this->create_module_test_file($xml, $this->quiz->cmid); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); $quizsettings->set('allowuserquitseb', 1); $quizsettings->set('quitpassword', ''); $quizsettings->save(); $this->assertStringContainsString( "startURLhttps://www.example.com/moodle/mod/quiz/view.php?id={$this->quiz->cmid}", $quizsettings->get_config() ); $this->assertStringNotContainsString("allowQuit", $quizsettings->get_config()); $this->assertStringNotContainsString("hashedQuitPassword", $quizsettings->get_config()); $quizsettings->set('quitpassword', 'new password'); $quizsettings->save(); $this->assertStringNotContainsString("allowQuit", $quizsettings->get_config()); $this->assertStringNotContainsString("hashedQuitPassword", $quizsettings->get_config()); $quizsettings->set('allowuserquitseb', 0); $quizsettings->set('quitpassword', ''); $quizsettings->save(); $this->assertStringNotContainsString("allowQuit", $quizsettings->get_config()); $this->assertStringNotContainsString("hashedQuitPassword", $quizsettings->get_config()); } /** * Test using USE_SEB_TEMPLATE populates the linkquitseb setting if a quitURL is found. */ public function test_template_has_quit_url_set(): void { $xml = "\n" . "\n" . "hashedQuitPasswordhashedpassword" . "allowWlanquitURLhttp://seb.quit.url" . "sendBrowserExamKey\n"; $template = $this->create_template($xml); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $quizsettings->set('templateid', $template->get('id')); $this->assertEmpty($quizsettings->get('linkquitseb')); $quizsettings->save(); $this->assertNotEmpty($quizsettings->get('linkquitseb')); $this->assertEquals('http://seb.quit.url', $quizsettings->get('linkquitseb')); } /** * Test using USE_SEB_UPLOAD_CONFIG populates the linkquitseb setting if a quitURL is found. */ public function test_config_file_uploaded_has_quit_url_set(): void { $xml = "\n" . "\n" . "hashedQuitPasswordhashedpassword" . "allowWlanquitURLhttp://seb.quit.url" . "sendBrowserExamKey\n"; $itemid = $this->create_module_test_file($xml, $this->quiz->cmid); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); $this->assertEmpty($quizsettings->get('linkquitseb')); $quizsettings->save(); $this->assertNotEmpty($quizsettings->get('linkquitseb')); $this->assertEquals('http://seb.quit.url', $quizsettings->get('linkquitseb')); } /** * Test template id set correctly. */ public function test_templateid_set_correctly_when_save_settings(): void { $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertEquals(0, $quizsettings->get('templateid')); $template = $this->create_template(); $templateid = $template->get('id'); // Initially set to USE_SEB_TEMPLATE with a template id. $this->save_settings_with_optional_template($quizsettings, settings_provider::USE_SEB_TEMPLATE, $templateid); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertEquals($templateid, $quizsettings->get('templateid')); // Case for USE_SEB_NO, ensure template id reverts to 0. $this->save_settings_with_optional_template($quizsettings, settings_provider::USE_SEB_NO); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertEquals(0, $quizsettings->get('templateid')); // Reverting back to USE_SEB_TEMPLATE. $this->save_settings_with_optional_template($quizsettings, settings_provider::USE_SEB_TEMPLATE, $templateid); // Case for USE_SEB_CONFIG_MANUALLY, ensure template id reverts to 0. $this->save_settings_with_optional_template($quizsettings, settings_provider::USE_SEB_CONFIG_MANUALLY); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertEquals(0, $quizsettings->get('templateid')); // Reverting back to USE_SEB_TEMPLATE. $this->save_settings_with_optional_template($quizsettings, settings_provider::USE_SEB_TEMPLATE, $templateid); // Case for USE_SEB_CLIENT_CONFIG, ensure template id reverts to 0. $this->save_settings_with_optional_template($quizsettings, settings_provider::USE_SEB_CLIENT_CONFIG); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertEquals(0, $quizsettings->get('templateid')); // Reverting back to USE_SEB_TEMPLATE. $this->save_settings_with_optional_template($quizsettings, settings_provider::USE_SEB_TEMPLATE, $templateid); // Case for USE_SEB_UPLOAD_CONFIG, ensure template id reverts to 0. $xml = file_get_contents(self::get_fixture_path(__NAMESPACE__, 'unencrypted.seb')); $this->create_module_test_file($xml, $this->quiz->cmid); $this->save_settings_with_optional_template($quizsettings, settings_provider::USE_SEB_UPLOAD_CONFIG); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertEquals(0, $quizsettings->get('templateid')); // Case for USE_SEB_TEMPLATE, ensure template id is correct. $this->save_settings_with_optional_template($quizsettings, settings_provider::USE_SEB_TEMPLATE, $templateid); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertEquals($templateid, $quizsettings->get('templateid')); } /** * Helper function in tests to set USE_SEB_TEMPLATE and a template id on the quiz settings. * * @param seb_quiz_settings $quizsettings Given quiz settings instance. * @param int $savetype Type of SEB usage. * @param int $templateid Template ID. */ public function save_settings_with_optional_template($quizsettings, $savetype, $templateid = 0) { $quizsettings->set('requiresafeexambrowser', $savetype); if (!empty($templateid)) { $quizsettings->set('templateid', $templateid); } $quizsettings->save(); } /** * Bad browser exam key data provider. * * @return array */ public static function bad_browser_exam_key_provider(): array { return [ 'Short string' => ['fdsf434r', 'A key should be a 64-character hex string.'], 'Non hex string' => ['aadf6799aadf6789aadf6789aadf6789aadf6789aadf6789aadf6789aadf678!', 'A key should be a 64-character hex string.'], 'Non unique' => ["aadf6799aadf6789aadf6789aadf6789aadf6789aadf6789aadf6789aadf6789" . "\naadf6799aadf6789aadf6789aadf6789aadf6789aadf6789aadf6789aadf6789", 'The keys must all be different.'], ]; } /** * Provide settings for different filter rules. * * @return array Test data. */ public static function filter_rules_provider(): array { return [ 'enabled simple expessions' => [ (object) [ 'requiresafeexambrowser' => settings_provider::USE_SEB_CONFIG_MANUALLY, 'quizid' => 1, 'cmid' => 1, 'expressionsallowed' => "test.com\r\nsecond.hello", 'regexallowed' => '', 'expressionsblocked' => '', 'regexblocked' => '', ], "\n" . "\n" . "showTaskBar" . "allowWlanshowReloadButton" . "showTimeshowInputLanguageallowQuit" . "quitURLConfirmaudioControlEnabledaudioMute" . "browserMediaCaptureCamerabrowserMediaCaptureMicrophone" . "allowSpellCheckbrowserWindowAllowReloadURLFilterEnable" . "URLFilterEnableContentFilterURLFilterRules" . "action1active" . "expressiontest.com" . "regexaction1" . "activeexpression" . "second.helloregex" . "startURLhttps://www.example.com/moodle/mod/quiz/view.php?id=1" . "sendBrowserExamKeybrowserWindowWebView3" . "examSessionClearCookiesOnStart" . "allowPreferencesWindow\n", ], 'blocked simple expessions' => [ (object) [ 'requiresafeexambrowser' => settings_provider::USE_SEB_CONFIG_MANUALLY, 'quizid' => 1, 'cmid' => 1, 'expressionsallowed' => '', 'regexallowed' => '', 'expressionsblocked' => "test.com\r\nsecond.hello", 'regexblocked' => '', ], "\n" . "\n" . "showTaskBar" . "allowWlanshowReloadButton" . "showTimeshowInputLanguageallowQuit" . "quitURLConfirmaudioControlEnabledaudioMute" . "browserMediaCaptureCamerabrowserMediaCaptureMicrophone" . "allowSpellCheckbrowserWindowAllowReloadURLFilterEnable" . "URLFilterEnableContentFilterURLFilterRules" . "action0active" . "expressiontest.com" . "regexaction0" . "activeexpression" . "second.helloregex" . "startURLhttps://www.example.com/moodle/mod/quiz/view.php?id=1" . "sendBrowserExamKeybrowserWindowWebView3" . "examSessionClearCookiesOnStart" . "allowPreferencesWindow\n", ], 'enabled regex expessions' => [ (object) [ 'requiresafeexambrowser' => settings_provider::USE_SEB_CONFIG_MANUALLY, 'quizid' => 1, 'cmid' => 1, 'expressionsallowed' => '', 'regexallowed' => "test.com\r\nsecond.hello", 'expressionsblocked' => '', 'regexblocked' => '', ], "\n" . "\n" . "showTaskBar" . "allowWlanshowReloadButton" . "showTimeshowInputLanguageallowQuit" . "quitURLConfirmaudioControlEnabledaudioMute" . "browserMediaCaptureCamerabrowserMediaCaptureMicrophone" . "allowSpellCheckbrowserWindowAllowReloadURLFilterEnable" . "URLFilterEnableContentFilterURLFilterRules" . "action1active" . "expressiontest.com" . "regexaction1" . "activeexpression" . "second.helloregex" . "startURLhttps://www.example.com/moodle/mod/quiz/view.php?id=1" . "sendBrowserExamKeybrowserWindowWebView3" . "examSessionClearCookiesOnStart" . "allowPreferencesWindow\n", ], 'blocked regex expessions' => [ (object) [ 'requiresafeexambrowser' => settings_provider::USE_SEB_CONFIG_MANUALLY, 'quizid' => 1, 'cmid' => 1, 'expressionsallowed' => '', 'regexallowed' => '', 'expressionsblocked' => '', 'regexblocked' => "test.com\r\nsecond.hello", ], "\n" . "\n" . "showTaskBar" . "allowWlanshowReloadButton" . "showTimeshowInputLanguageallowQuit" . "quitURLConfirmaudioControlEnabledaudioMute" . "browserMediaCaptureCamerabrowserMediaCaptureMicrophone" . "allowSpellCheckbrowserWindowAllowReloadURLFilterEnable" . "URLFilterEnableContentFilterURLFilterRules" . "action0active" . "expressiontest.com" . "regexaction0" . "activeexpression" . "second.helloregex" . "startURLhttps://www.example.com/moodle/mod/quiz/view.php?id=1" . "sendBrowserExamKeybrowserWindowWebView3" . "examSessionClearCookiesOnStart" . "allowPreferencesWindow\n", ], 'multiple simple expessions' => [ (object) [ 'requiresafeexambrowser' => settings_provider::USE_SEB_CONFIG_MANUALLY, 'quizid' => 1, 'cmid' => 1, 'expressionsallowed' => "*", 'regexallowed' => '', 'expressionsblocked' => '', 'regexblocked' => "test.com\r\nsecond.hello", ], "\n" . "\n" . "showTaskBar" . "allowWlanshowReloadButton" . "showTimeshowInputLanguageallowQuit" . "quitURLConfirmaudioControlEnabledaudioMute" . "browserMediaCaptureCamerabrowserMediaCaptureMicrophone" . "allowSpellCheckbrowserWindowAllowReloadURLFilterEnable" . "URLFilterEnableContentFilterURLFilterRulesaction" . "1activeexpression*" . "regex" . "action0active" . "expressiontest.com" . "regexaction0" . "activeexpression" . "second.helloregex" . "startURLhttps://www.example.com/moodle/mod/quiz/view.php?id=1" . "sendBrowserExamKeybrowserWindowWebView3" . "examSessionClearCookiesOnStart" . "allowPreferencesWindow\n", ], ]; } /** * Test that config and config key are null when expected. */ public function test_generates_config_values_as_null_when_expected(): void { $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertNotNull($quizsettings->get_config()); $this->assertNotNull($quizsettings->get_config_key()); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_NO); $quizsettings->save(); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertNull($quizsettings->get_config()); $this->assertNull($quizsettings->get_config()); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); $xml = file_get_contents(self::get_fixture_path(__NAMESPACE__, 'unencrypted.seb')); $this->create_module_test_file($xml, $this->quiz->cmid); $quizsettings->save(); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertNotNull($quizsettings->get_config()); $this->assertNotNull($quizsettings->get_config_key()); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_CLIENT_CONFIG); $quizsettings->save(); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertNull($quizsettings->get_config()); $this->assertNull($quizsettings->get_config_key()); $template = $this->create_template(); $templateid = $template->get('id'); $this->save_settings_with_optional_template($quizsettings, settings_provider::USE_SEB_TEMPLATE, $templateid); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertNotNull($quizsettings->get_config()); $this->assertNotNull($quizsettings->get_config_key()); } /** * Test that quizsettings cache exists after creation. */ public function test_quizsettings_cache_exists_after_creation(): void { $expected = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertEquals($expected->to_record(), \cache::make('quizaccess_seb', 'quizsettings')->get($this->quiz->id)); } /** * Test that quizsettings cache gets deleted after deletion. */ public function test_quizsettings_cache_purged_after_deletion(): void { $this->assertNotEmpty(\cache::make('quizaccess_seb', 'quizsettings')->get($this->quiz->id)); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->delete(); $this->assertFalse(\cache::make('quizaccess_seb', 'quizsettings')->get($this->quiz->id)); } /** * Test that we can get seb_quiz_settings by quiz id. */ public function test_get_quiz_settings_by_quiz_id(): void { $expected = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertEquals($expected->to_record(), seb_quiz_settings::get_by_quiz_id($this->quiz->id)->to_record()); // Check that data is getting from cache. $expected->set('showsebtaskbar', 0); $this->assertNotEquals($expected->to_record(), seb_quiz_settings::get_by_quiz_id($this->quiz->id)->to_record()); // Now save and check that cached as been updated. $expected->save(); $this->assertEquals($expected->to_record(), seb_quiz_settings::get_by_quiz_id($this->quiz->id)->to_record()); // Returns false for non existing quiz. $this->assertFalse(seb_quiz_settings::get_by_quiz_id(7777777)); } /** * Test that SEB config cache exists after creation of the quiz. */ public function test_config_cache_exists_after_creation(): void { $this->assertNotEmpty(\cache::make('quizaccess_seb', 'config')->get($this->quiz->id)); } /** * Test that SEB config cache gets deleted after deletion. */ public function test_config_cache_purged_after_deletion(): void { $this->assertNotEmpty(\cache::make('quizaccess_seb', 'config')->get($this->quiz->id)); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->delete(); $this->assertFalse(\cache::make('quizaccess_seb', 'config')->get($this->quiz->id)); } /** * Test that we can get SEB config by quiz id. */ public function test_get_config_by_quiz_id(): void { $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $expected = $quizsettings->get_config(); $this->assertEquals($expected, seb_quiz_settings::get_config_by_quiz_id($this->quiz->id)); // Check that data is getting from cache. $quizsettings->set('showsebtaskbar', 0); $this->assertNotEquals($quizsettings->get_config(), seb_quiz_settings::get_config_by_quiz_id($this->quiz->id)); // Now save and check that cached as been updated. $quizsettings->save(); $this->assertEquals($quizsettings->get_config(), seb_quiz_settings::get_config_by_quiz_id($this->quiz->id)); // Returns null for non existing quiz. $this->assertNull(seb_quiz_settings::get_config_by_quiz_id(7777777)); } /** * Test that SEB config key cache exists after creation of the quiz. */ public function test_config_key_cache_exists_after_creation(): void { $this->assertNotEmpty(\cache::make('quizaccess_seb', 'configkey')->get($this->quiz->id)); } /** * Test that SEB config key cache gets deleted after deletion. */ public function test_config_key_cache_purged_after_deletion(): void { $this->assertNotEmpty(\cache::make('quizaccess_seb', 'configkey')->get($this->quiz->id)); $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->delete(); $this->assertFalse(\cache::make('quizaccess_seb', 'configkey')->get($this->quiz->id)); } /** * Test that we can get SEB config key by quiz id. */ public function test_get_config_key_by_quiz_id(): void { $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $expected = $quizsettings->get_config_key(); $this->assertEquals($expected, seb_quiz_settings::get_config_key_by_quiz_id($this->quiz->id)); // Check that data is getting from cache. $quizsettings->set('showsebtaskbar', 0); $this->assertNotEquals($quizsettings->get_config_key(), seb_quiz_settings::get_config_key_by_quiz_id($this->quiz->id)); // Now save and check that cached as been updated. $quizsettings->save(); $this->assertEquals($quizsettings->get_config_key(), seb_quiz_settings::get_config_key_by_quiz_id($this->quiz->id)); // Returns null for non existing quiz. $this->assertNull(seb_quiz_settings::get_config_key_by_quiz_id(7777777)); } } PK\$PP tests/settings_provider_test.phpnu[. namespace quizaccess_seb; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/test_helper_trait.php'); /** * PHPUnit tests for settings_provider. * * @package quizaccess_seb * @author Andrew Madden * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class settings_provider_test extends \advanced_testcase { use \quizaccess_seb_test_helper_trait; /** * Mocked quiz form instance. * @var \mod_quiz_mod_form */ protected $mockedquizform; /** * Test moodle form. * @var \MoodleQuickForm */ protected $mockedform; /** * Context for testing. * @var \context */ protected $context; /** * Test user. * @var \stdClass */ protected $user; /** * Test role ID. * @var int */ protected $roleid; /** * Helper method to set up form mocks. */ protected function set_up_form_mocks() { if (empty($this->context)) { $this->context = \context_module::instance($this->quiz->cmid); } $this->mockedquizform = $this->createMock('mod_quiz_mod_form'); $this->mockedquizform->method('get_context')->willReturn($this->context); $this->mockedquizform->method('get_instance')->willReturn($this->quiz->id); $this->mockedform = new \MoodleQuickForm('test', 'post', ''); $this->mockedform->addElement('static', 'security'); } /** * Helper method to set up user and role for testing. */ protected function set_up_user_and_role() { $this->user = $this->getDataGenerator()->create_user(); $this->setUser($this->user); $this->roleid = $this->getDataGenerator()->create_role(); $this->getDataGenerator()->role_assign($this->roleid, $this->user->id, $this->context->id); } /** * Capability data for testing. * * @return array */ public static function settings_capability_data_provider(): array { $data = []; // Build first level SEB config settings. Any of this setting let us use SEB manual config. foreach (settings_provider::get_seb_settings_map()[settings_provider::USE_SEB_CONFIG_MANUALLY] as $name => $children) { if (key_exists($name, settings_provider::get_seb_config_elements())) { $cap = settings_provider::build_setting_capability_name($name); $data[] = [$cap]; } } return $data; } /** * Test that settings types to be added to quiz settings, are part of quiz_settings persistent class. */ public function test_setting_elements_are_part_of_quiz_settings_table(): void { $dbsettings = (array) (new seb_quiz_settings())->to_record(); $settingelements = settings_provider::get_seb_config_elements(); $settingelements = (array) $this->strip_all_prefixes((object) $settingelements); // Get all elements to be added to form, that are not in the persistent quiz_settings class. $diffelements = array_diff_key($settingelements, $dbsettings); $this->assertEmpty($diffelements); } /** * Make sure that all SEB settings have related capabilities. */ public function test_that_all_seb_settings_have_capabilities(): void { foreach (settings_provider::get_seb_config_elements() as $name => $notused) { $this->assertNotEmpty(get_capability_info(settings_provider::build_setting_capability_name($name))); } } /** * Test that setting defaults only refer to settings defined in setting types. */ public function test_setting_defaults_are_part_of_file_types(): void { $settingelements = settings_provider::get_seb_config_elements(); $settingdefaults = settings_provider::get_seb_config_element_defaults(); // Get all defaults that have no matching element in settings types. $diffelements = array_diff_key($settingdefaults, $settingelements); $this->assertEmpty($diffelements); } /** * Test that setting types only refer to settings defined in setting types. */ public function test_setting_types_are_part_of_file_types(): void { $settingelements = settings_provider::get_seb_config_elements(); $settingtypes = settings_provider::get_seb_config_element_types(); // Get all defaults that have no matching element in settings types. $diffelements = array_diff_key($settingtypes, $settingelements); $this->assertEmpty($diffelements); } /** * Helper method to assert hide if element. * @param hideif_rule $hideif Rule to check. * @param string $element Expected element. * @param string $dependantname Expected dependant element name. * @param string $condition Expected condition. * @param mixed $value Expected value. */ protected function assert_hide_if(hideif_rule $hideif, $element, $dependantname, $condition, $value) { $this->assertEquals($element, $hideif->get_element()); $this->assertEquals($dependantname, $hideif->get_dependantname()); $this->assertEquals($condition, $hideif->get_condition()); $this->assertEquals($value, $hideif->get_dependantvalue()); } /** * Test hideif rules. */ public function test_hideifs(): void { $settinghideifs = settings_provider::get_quiz_hideifs(); $this->assertCount(25, $settinghideifs); $this->assertArrayHasKey('seb_templateid', $settinghideifs); $this->assertCount(1, $settinghideifs['seb_templateid']); $this->assert_hide_if( $settinghideifs['seb_templateid'][0], 'seb_templateid', 'seb_requiresafeexambrowser', 'noteq', settings_provider::USE_SEB_TEMPLATE ); $this->assertArrayHasKey('filemanager_sebconfigfile', $settinghideifs); $this->assertCount(1, $settinghideifs['filemanager_sebconfigfile']); $this->assert_hide_if( $settinghideifs['filemanager_sebconfigfile'][0], 'filemanager_sebconfigfile', 'seb_requiresafeexambrowser', 'noteq', settings_provider::USE_SEB_UPLOAD_CONFIG ); $this->assertArrayHasKey('seb_showsebtaskbar', $settinghideifs); $this->assertCount(1, $settinghideifs['seb_showsebtaskbar']); $this->assert_hide_if( $settinghideifs['seb_showsebtaskbar'][0], 'seb_showsebtaskbar', 'seb_requiresafeexambrowser', 'noteq', settings_provider::USE_SEB_CONFIG_MANUALLY ); $this->assertArrayHasKey('seb_showwificontrol', $settinghideifs); $this->assertCount(2, $settinghideifs['seb_showwificontrol']); $this->assert_hide_if( $settinghideifs['seb_showwificontrol'][0], 'seb_showwificontrol', 'seb_requiresafeexambrowser', 'noteq', settings_provider::USE_SEB_CONFIG_MANUALLY ); $this->assert_hide_if( $settinghideifs['seb_showwificontrol'][1], 'seb_showwificontrol', 'seb_showsebtaskbar', 'eq', 0 ); $this->assertArrayHasKey('seb_showreloadbutton', $settinghideifs); $this->assertCount(2, $settinghideifs['seb_showreloadbutton']); $this->assert_hide_if( $settinghideifs['seb_showreloadbutton'][0], 'seb_showreloadbutton', 'seb_requiresafeexambrowser', 'noteq', settings_provider::USE_SEB_CONFIG_MANUALLY ); $this->assert_hide_if( $settinghideifs['seb_showreloadbutton'][1], 'seb_showreloadbutton', 'seb_showsebtaskbar', 'eq', 0 ); $this->assertArrayHasKey('seb_showtime', $settinghideifs); $this->assertCount(2, $settinghideifs['seb_showtime']); $this->assert_hide_if( $settinghideifs['seb_showtime'][0], 'seb_showtime', 'seb_requiresafeexambrowser', 'noteq', settings_provider::USE_SEB_CONFIG_MANUALLY ); $this->assert_hide_if( $settinghideifs['seb_showtime'][1], 'seb_showtime', 'seb_showsebtaskbar', 'eq', 0 ); $this->assertArrayHasKey('seb_showkeyboardlayout', $settinghideifs); $this->assertCount(2, $settinghideifs['seb_showkeyboardlayout']); $this->assert_hide_if( $settinghideifs['seb_showkeyboardlayout'][0], 'seb_showkeyboardlayout', 'seb_requiresafeexambrowser', 'noteq', settings_provider::USE_SEB_CONFIG_MANUALLY ); $this->assert_hide_if( $settinghideifs['seb_showkeyboardlayout'][1], 'seb_showkeyboardlayout', 'seb_showsebtaskbar', 'eq', 0 ); $this->assertArrayHasKey('seb_allowuserquitseb', $settinghideifs); $this->assertCount(3, $settinghideifs['seb_allowuserquitseb']); $this->assert_hide_if( $settinghideifs['seb_allowuserquitseb'][0], 'seb_allowuserquitseb', 'seb_requiresafeexambrowser', 'eq', settings_provider::USE_SEB_NO ); $this->assert_hide_if( $settinghideifs['seb_allowuserquitseb'][1], 'seb_allowuserquitseb', 'seb_requiresafeexambrowser', 'eq', settings_provider::USE_SEB_CLIENT_CONFIG ); $this->assert_hide_if( $settinghideifs['seb_allowuserquitseb'][2], 'seb_allowuserquitseb', 'seb_requiresafeexambrowser', 'eq', settings_provider::USE_SEB_UPLOAD_CONFIG ); $this->assertArrayHasKey('seb_quitpassword', $settinghideifs); $this->assertCount(4, $settinghideifs['seb_quitpassword']); $this->assert_hide_if( $settinghideifs['seb_quitpassword'][0], 'seb_quitpassword', 'seb_requiresafeexambrowser', 'eq', settings_provider::USE_SEB_NO ); $this->assert_hide_if( $settinghideifs['seb_quitpassword'][1], 'seb_quitpassword', 'seb_requiresafeexambrowser', 'eq', settings_provider::USE_SEB_CLIENT_CONFIG ); $this->assert_hide_if( $settinghideifs['seb_quitpassword'][2], 'seb_quitpassword', 'seb_requiresafeexambrowser', 'eq', settings_provider::USE_SEB_UPLOAD_CONFIG ); $this->assert_hide_if( $settinghideifs['seb_quitpassword'][3], 'seb_quitpassword', 'seb_allowuserquitseb', 'eq', 0 ); $this->assertArrayHasKey('seb_linkquitseb', $settinghideifs); $this->assertCount(1, $settinghideifs['seb_linkquitseb']); $this->assert_hide_if( $settinghideifs['seb_linkquitseb'][0], 'seb_linkquitseb', 'seb_requiresafeexambrowser', 'noteq', settings_provider::USE_SEB_CONFIG_MANUALLY ); $this->assertArrayHasKey('seb_userconfirmquit', $settinghideifs); $this->assertCount(1, $settinghideifs['seb_userconfirmquit']); $this->assert_hide_if( $settinghideifs['seb_userconfirmquit'][0], 'seb_userconfirmquit', 'seb_requiresafeexambrowser', 'noteq', settings_provider::USE_SEB_CONFIG_MANUALLY ); $this->assertArrayHasKey('seb_enableaudiocontrol', $settinghideifs); $this->assertCount(1, $settinghideifs['seb_enableaudiocontrol']); $this->assert_hide_if( $settinghideifs['seb_enableaudiocontrol'][0], 'seb_enableaudiocontrol', 'seb_requiresafeexambrowser', 'noteq', settings_provider::USE_SEB_CONFIG_MANUALLY ); $this->assertArrayHasKey('seb_allowcapturecamera', $settinghideifs); $this->assertCount(1, $settinghideifs['seb_allowcapturecamera']); $this->assert_hide_if( $settinghideifs['seb_allowcapturecamera'][0], 'seb_allowcapturecamera', 'seb_requiresafeexambrowser', 'noteq', settings_provider::USE_SEB_CONFIG_MANUALLY ); $this->assertArrayHasKey('seb_allowcapturemicrophone', $settinghideifs); $this->assertCount(1, $settinghideifs['seb_allowcapturemicrophone']); $this->assert_hide_if( $settinghideifs['seb_allowcapturemicrophone'][0], 'seb_allowcapturemicrophone', 'seb_requiresafeexambrowser', 'noteq', settings_provider::USE_SEB_CONFIG_MANUALLY ); $this->assertArrayHasKey('seb_muteonstartup', $settinghideifs); $this->assertCount(2, $settinghideifs['seb_muteonstartup']); $this->assert_hide_if( $settinghideifs['seb_muteonstartup'][0], 'seb_muteonstartup', 'seb_requiresafeexambrowser', 'noteq', settings_provider::USE_SEB_CONFIG_MANUALLY ); $this->assert_hide_if( $settinghideifs['seb_muteonstartup'][1], 'seb_muteonstartup', 'seb_enableaudiocontrol', 'eq', 0 ); $this->assertArrayHasKey('seb_allowspellchecking', $settinghideifs); $this->assertCount(1, $settinghideifs['seb_allowspellchecking']); $this->assert_hide_if( $settinghideifs['seb_allowspellchecking'][0], 'seb_allowspellchecking', 'seb_requiresafeexambrowser', 'noteq', settings_provider::USE_SEB_CONFIG_MANUALLY ); $this->assertArrayHasKey('seb_allowreloadinexam', $settinghideifs); $this->assertCount(1, $settinghideifs['seb_allowreloadinexam']); $this->assert_hide_if( $settinghideifs['seb_allowreloadinexam'][0], 'seb_allowreloadinexam', 'seb_requiresafeexambrowser', 'noteq', settings_provider::USE_SEB_CONFIG_MANUALLY ); $this->assertArrayHasKey('seb_activateurlfiltering', $settinghideifs); $this->assertCount(1, $settinghideifs['seb_activateurlfiltering']); $this->assert_hide_if( $settinghideifs['seb_activateurlfiltering'][0], 'seb_activateurlfiltering', 'seb_requiresafeexambrowser', 'noteq', settings_provider::USE_SEB_CONFIG_MANUALLY ); $this->assertArrayHasKey('seb_filterembeddedcontent', $settinghideifs); $this->assertCount(2, $settinghideifs['seb_filterembeddedcontent']); $this->assert_hide_if( $settinghideifs['seb_filterembeddedcontent'][0], 'seb_filterembeddedcontent', 'seb_requiresafeexambrowser', 'noteq', settings_provider::USE_SEB_CONFIG_MANUALLY ); $this->assert_hide_if( $settinghideifs['seb_filterembeddedcontent'][1], 'seb_filterembeddedcontent', 'seb_activateurlfiltering', 'eq', 0 ); $this->assertArrayHasKey('seb_expressionsallowed', $settinghideifs); $this->assertCount(2, $settinghideifs['seb_expressionsallowed']); $this->assert_hide_if( $settinghideifs['seb_expressionsallowed'][0], 'seb_expressionsallowed', 'seb_requiresafeexambrowser', 'noteq', settings_provider::USE_SEB_CONFIG_MANUALLY ); $this->assert_hide_if( $settinghideifs['seb_expressionsallowed'][1], 'seb_expressionsallowed', 'seb_activateurlfiltering', 'eq', 0 ); $this->assertArrayHasKey('seb_regexallowed', $settinghideifs); $this->assertCount(2, $settinghideifs['seb_regexallowed']); $this->assert_hide_if( $settinghideifs['seb_regexallowed'][0], 'seb_regexallowed', 'seb_requiresafeexambrowser', 'noteq', settings_provider::USE_SEB_CONFIG_MANUALLY ); $this->assert_hide_if( $settinghideifs['seb_regexallowed'][1], 'seb_regexallowed', 'seb_activateurlfiltering', 'eq', 0 ); $this->assertArrayHasKey('seb_expressionsblocked', $settinghideifs); $this->assertCount(2, $settinghideifs['seb_expressionsblocked']); $this->assert_hide_if( $settinghideifs['seb_expressionsblocked'][0], 'seb_expressionsblocked', 'seb_requiresafeexambrowser', 'noteq', settings_provider::USE_SEB_CONFIG_MANUALLY ); $this->assert_hide_if( $settinghideifs['seb_expressionsblocked'][1], 'seb_expressionsblocked', 'seb_activateurlfiltering', 'eq', 0 ); $this->assertArrayHasKey('seb_regexblocked', $settinghideifs); $this->assertCount(2, $settinghideifs['seb_regexblocked']); $this->assert_hide_if( $settinghideifs['seb_regexblocked'][0], 'seb_regexblocked', 'seb_requiresafeexambrowser', 'noteq', settings_provider::USE_SEB_CONFIG_MANUALLY ); $this->assert_hide_if( $settinghideifs['seb_regexblocked'][1], 'seb_regexblocked', 'seb_activateurlfiltering', 'eq', 0 ); $this->assertArrayHasKey('seb_showsebdownloadlink', $settinghideifs); $this->assertCount(1, $settinghideifs['seb_showsebdownloadlink']); $this->assert_hide_if( $settinghideifs['seb_showsebdownloadlink'][0], 'seb_showsebdownloadlink', 'seb_requiresafeexambrowser', 'eq', settings_provider::USE_SEB_NO ); $this->assertArrayHasKey('seb_allowedbrowserexamkeys', $settinghideifs); $this->assertCount(3, $settinghideifs['seb_allowedbrowserexamkeys']); $this->assert_hide_if( $settinghideifs['seb_allowedbrowserexamkeys'][0], 'seb_allowedbrowserexamkeys', 'seb_requiresafeexambrowser', 'eq', settings_provider::USE_SEB_NO ); $this->assert_hide_if( $settinghideifs['seb_allowedbrowserexamkeys'][1], 'seb_allowedbrowserexamkeys', 'seb_requiresafeexambrowser', 'eq', settings_provider::USE_SEB_CONFIG_MANUALLY ); $this->assert_hide_if( $settinghideifs['seb_allowedbrowserexamkeys'][2], 'seb_allowedbrowserexamkeys', 'seb_requiresafeexambrowser', 'eq', settings_provider::USE_SEB_TEMPLATE ); } /** * Test that setting hideif rules only refer to settings defined in setting types, including the conditions. */ public function test_setting_hideifs_are_part_of_file_types(): void { $settingelements = settings_provider::get_seb_config_elements(); $settinghideifs = settings_provider::get_quiz_hideifs(); // Add known additional elements. $settingelements['seb_templateid'] = ''; $settingelements['filemanager_sebconfigfile'] = ''; $settingelements['seb_showsebdownloadlink'] = ''; $settingelements['seb_allowedbrowserexamkeys'] = ''; // Get all defaults that have no matching element in settings types. $diffelements = array_diff_key($settinghideifs, $settingelements); // Check no diff for elements to hide. $this->assertEmpty($diffelements); // Check each element's to hide conditions that each condition refers to element in settings types. foreach ($settinghideifs as $conditions) { foreach ($conditions as $condition) { $this->assertTrue(array_key_exists($condition->get_element(), $settingelements)); } } } /** * Test that exception thrown if we try to build capability name from the incorrect setting name. */ public function test_build_setting_capability_name_incorrect_setting(): void { $this->expectException(\coding_exception::class); $this->expectExceptionMessage('Incorrect SEB quiz setting broken'); $broken = settings_provider::build_setting_capability_name('broken'); } /** * Test we can build capability name from the the setting name. */ public function test_build_setting_capability_name_correct_setting(): void { foreach (settings_provider::get_seb_config_elements() as $name => $type) { $expected = 'quizaccess/seb:manage_' . $name; $actual = settings_provider::build_setting_capability_name($name); $this->assertSame($expected, $actual); } } /** * Test can check if can manage SEB settings respecting settings structure. */ public function test_can_manage_seb_config_setting(): void { $this->resetAfterTest(); $this->setAdminUser(); $this->course = $this->getDataGenerator()->create_course(); $this->quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $this->course->id]); $this->context = \context_module::instance($this->quiz->cmid); $this->set_up_user_and_role(); foreach (settings_provider::get_seb_settings_map()[settings_provider::USE_SEB_CONFIG_MANUALLY] as $setting => $children) { // Skip not SEB setting. if ($setting == 'seb_showsebdownloadlink') { continue; } $this->assertFalse(settings_provider::can_manage_seb_config_setting($setting, $this->context)); foreach ($children as $child => $empty) { $this->assertFalse(settings_provider::can_manage_seb_config_setting($child, $this->context)); // Assign child capability without having parent one. Should not have access to manage child. $childcap = settings_provider::build_setting_capability_name($child); assign_capability($childcap, CAP_ALLOW, $this->roleid, $this->context->id); $this->assertFalse(settings_provider::can_manage_seb_config_setting($child, $this->context)); } // Assign parent capability. Should be able to manage children now. $parentcap = settings_provider::build_setting_capability_name($setting); assign_capability($parentcap, CAP_ALLOW, $this->roleid, $this->context->id); $this->assertTrue(settings_provider::can_manage_seb_config_setting($setting, $this->context)); foreach ($children as $child => $empty) { $this->assertTrue(settings_provider::can_manage_seb_config_setting($child, $this->context)); } } } /** * Test SEB usage options. * * @param string $settingcapability Setting capability to check options against. * * @dataProvider settings_capability_data_provider */ public function test_get_requiresafeexambrowser_options($settingcapability): void { $this->resetAfterTest(); $this->setAdminUser(); $this->course = $this->getDataGenerator()->create_course(); $this->quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $this->course->id]); $this->context = \context_module::instance($this->quiz->cmid); $options = settings_provider::get_requiresafeexambrowser_options($this->context); $this->assertCount(4, $options); $this->assertTrue(array_key_exists(settings_provider::USE_SEB_NO, $options)); $this->assertTrue(array_key_exists(settings_provider::USE_SEB_CONFIG_MANUALLY, $options)); $this->assertFalse(array_key_exists(settings_provider::USE_SEB_TEMPLATE, $options)); $this->assertTrue(array_key_exists(settings_provider::USE_SEB_UPLOAD_CONFIG, $options)); $this->assertTrue(array_key_exists(settings_provider::USE_SEB_CLIENT_CONFIG, $options)); // Create a template. $this->create_template(); // The template options should be visible now. $options = settings_provider::get_requiresafeexambrowser_options($this->context); $this->assertCount(5, $options); $this->assertTrue(array_key_exists(settings_provider::USE_SEB_TEMPLATE, $options)); // A new user does not have the capability to use the file manager and template. $this->set_up_user_and_role(); $options = settings_provider::get_requiresafeexambrowser_options($this->context); $this->assertCount(1, $options); $this->assertFalse(array_key_exists(settings_provider::USE_SEB_CONFIG_MANUALLY, $options)); $this->assertFalse(array_key_exists(settings_provider::USE_SEB_TEMPLATE, $options)); $this->assertFalse(array_key_exists(settings_provider::USE_SEB_UPLOAD_CONFIG, $options)); $this->assertFalse(array_key_exists(settings_provider::USE_SEB_CLIENT_CONFIG, $options)); $this->assertTrue(array_key_exists(settings_provider::USE_SEB_NO, $options)); assign_capability($settingcapability, CAP_ALLOW, $this->roleid, $this->context->id); $options = settings_provider::get_requiresafeexambrowser_options($this->context); $this->assertCount(1, $options); $this->assertFalse(array_key_exists(settings_provider::USE_SEB_CONFIG_MANUALLY, $options)); $this->assertFalse(array_key_exists(settings_provider::USE_SEB_TEMPLATE, $options)); $this->assertFalse(array_key_exists(settings_provider::USE_SEB_UPLOAD_CONFIG, $options)); $this->assertFalse(array_key_exists(settings_provider::USE_SEB_CLIENT_CONFIG, $options)); $this->assertTrue(array_key_exists(settings_provider::USE_SEB_NO, $options)); assign_capability('quizaccess/seb:manage_seb_configuremanually', CAP_ALLOW, $this->roleid, $this->context->id); $options = settings_provider::get_requiresafeexambrowser_options($this->context); $this->assertCount(2, $options); $this->assertTrue(array_key_exists(settings_provider::USE_SEB_CONFIG_MANUALLY, $options)); $this->assertFalse(array_key_exists(settings_provider::USE_SEB_TEMPLATE, $options)); $this->assertFalse(array_key_exists(settings_provider::USE_SEB_UPLOAD_CONFIG, $options)); $this->assertFalse(array_key_exists(settings_provider::USE_SEB_CLIENT_CONFIG, $options)); $this->assertTrue(array_key_exists(settings_provider::USE_SEB_NO, $options)); assign_capability('quizaccess/seb:manage_seb_usesebclientconfig', CAP_ALLOW, $this->roleid, $this->context->id); $options = settings_provider::get_requiresafeexambrowser_options($this->context); $this->assertCount(3, $options); $this->assertTrue(array_key_exists(settings_provider::USE_SEB_CONFIG_MANUALLY, $options)); $this->assertFalse(array_key_exists(settings_provider::USE_SEB_TEMPLATE, $options)); $this->assertFalse(array_key_exists(settings_provider::USE_SEB_UPLOAD_CONFIG, $options)); $this->assertTrue(array_key_exists(settings_provider::USE_SEB_CLIENT_CONFIG, $options)); $this->assertTrue(array_key_exists(settings_provider::USE_SEB_NO, $options)); assign_capability('quizaccess/seb:manage_seb_templateid', CAP_ALLOW, $this->roleid, $this->context->id); $options = settings_provider::get_requiresafeexambrowser_options($this->context); $this->assertCount(4, $options); $this->assertTrue(array_key_exists(settings_provider::USE_SEB_CONFIG_MANUALLY, $options)); $this->assertTrue(array_key_exists(settings_provider::USE_SEB_TEMPLATE, $options)); $this->assertFalse(array_key_exists(settings_provider::USE_SEB_UPLOAD_CONFIG, $options)); $this->assertTrue(array_key_exists(settings_provider::USE_SEB_CLIENT_CONFIG, $options)); $this->assertTrue(array_key_exists(settings_provider::USE_SEB_NO, $options)); assign_capability('quizaccess/seb:manage_filemanager_sebconfigfile', CAP_ALLOW, $this->roleid, $this->context->id); $options = settings_provider::get_requiresafeexambrowser_options($this->context); $this->assertCount(5, $options); $this->assertTrue(array_key_exists(settings_provider::USE_SEB_CONFIG_MANUALLY, $options)); $this->assertTrue(array_key_exists(settings_provider::USE_SEB_TEMPLATE, $options)); $this->assertTrue(array_key_exists(settings_provider::USE_SEB_UPLOAD_CONFIG, $options)); $this->assertTrue(array_key_exists(settings_provider::USE_SEB_CLIENT_CONFIG, $options)); $this->assertTrue(array_key_exists(settings_provider::USE_SEB_NO, $options)); } /** * Test SEB usage options with conflicting permissions. */ public function test_get_requiresafeexambrowser_options_with_conflicting_permissions(): void { $this->resetAfterTest(); $this->setAdminUser(); $this->course = $this->getDataGenerator()->create_course(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $this->context = \context_module::instance($this->quiz->cmid); $template = $this->create_template(); $settings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $settings->set('templateid', $template->get('id')); $settings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $settings->save(); $this->set_up_user_and_role(); $options = settings_provider::get_requiresafeexambrowser_options($this->context); // If there is nay conflict we return full list of options. $this->assertCount(5, $options); $this->assertTrue(array_key_exists(settings_provider::USE_SEB_TEMPLATE, $options)); } /** * Test that SEB options and templates are frozen if conflicting permissions. */ public function test_form_elements_are_frozen_if_conflicting_permissions(): void { $this->resetAfterTest(); $this->setAdminUser(); $this->course = $this->getDataGenerator()->create_course(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $this->context = \context_module::instance($this->quiz->cmid); // Setup conflicting permissions. $template = $this->create_template(); $settings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $settings->set('templateid', $template->get('id')); $settings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $settings->save(); $this->set_up_user_and_role(); assign_capability('quizaccess/seb:manage_seb_requiresafeexambrowser', CAP_ALLOW, $this->roleid, $this->context->id); assign_capability('quizaccess/seb:manage_seb_showsebdownloadlink', CAP_ALLOW, $this->roleid, $this->context->id); assign_capability('quizaccess/seb:manage_seb_allowedbrowserexamkeys', CAP_ALLOW, $this->roleid, $this->context->id); $this->set_up_form_mocks(); settings_provider::add_seb_settings_fields($this->mockedquizform, $this->mockedform); $this->assertTrue($this->mockedform->isElementFrozen('seb_requiresafeexambrowser')); $this->assertTrue($this->mockedform->isElementFrozen('seb_templateid')); $this->assertTrue($this->mockedform->isElementFrozen('seb_showsebdownloadlink')); $this->assertTrue($this->mockedform->isElementFrozen('seb_allowedbrowserexamkeys')); } /** * Test that All settings are frozen if quiz was attempted and use seb with manual settings. */ public function test_form_elements_are_locked_when_quiz_attempted_manual(): void { $this->resetAfterTest(); $this->course = $this->getDataGenerator()->create_course(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $this->context = \context_module::instance($this->quiz->cmid); $user = $this->getDataGenerator()->create_user(); $this->attempt_quiz($this->quiz, $user); $this->setAdminUser(); $this->set_up_form_mocks(); settings_provider::add_seb_settings_fields($this->mockedquizform, $this->mockedform); $this->assertTrue($this->mockedform->isElementFrozen('seb_requiresafeexambrowser')); $this->assertTrue($this->mockedform->elementExists('filemanager_sebconfigfile')); $this->assertFalse($this->mockedform->elementExists('seb_templateid')); $this->assertTrue($this->mockedform->isElementFrozen('seb_showsebdownloadlink')); $this->assertTrue($this->mockedform->isElementFrozen('seb_allowedbrowserexamkeys')); foreach (settings_provider::get_seb_config_elements() as $name => $type) { $this->assertTrue($this->mockedform->isElementFrozen($name)); } } /** * Test that All settings are frozen if a quiz was attempted and use template. */ public function test_form_elements_are_locked_when_quiz_attempted_template(): void { $this->resetAfterTest(); $this->setAdminUser(); $this->course = $this->getDataGenerator()->create_course(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $this->context = \context_module::instance($this->quiz->cmid); $template = $this->create_template(); $settings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $settings->set('templateid', $template->get('id')); $settings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $settings->save(); $user = $this->getDataGenerator()->create_user(); $this->attempt_quiz($this->quiz, $user); $this->setAdminUser(); $this->set_up_form_mocks(); settings_provider::add_seb_settings_fields($this->mockedquizform, $this->mockedform); $this->assertTrue($this->mockedform->isElementFrozen('seb_requiresafeexambrowser')); $this->assertTrue($this->mockedform->elementExists('filemanager_sebconfigfile')); $this->assertTrue($this->mockedform->isElementFrozen('seb_templateid')); $this->assertTrue($this->mockedform->isElementFrozen('seb_showsebdownloadlink')); $this->assertTrue($this->mockedform->isElementFrozen('seb_allowedbrowserexamkeys')); foreach (settings_provider::get_seb_config_elements() as $name => $type) { $this->assertTrue($this->mockedform->isElementFrozen($name)); } } /** * Test Show Safe Exam Browser download button setting in the form. */ public function test_showsebdownloadlink_in_form(): void { $this->resetAfterTest(); $this->setAdminUser(); $this->course = $this->getDataGenerator()->create_course(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $this->context = \context_module::instance($this->quiz->cmid); $this->set_up_user_and_role(); assign_capability('quizaccess/seb:manage_seb_requiresafeexambrowser', CAP_ALLOW, $this->roleid, $this->context->id); $this->set_up_form_mocks(); // Shouldn't be in the form if no permissions. settings_provider::add_seb_settings_fields($this->mockedquizform, $this->mockedform); $this->assertFalse($this->mockedform->elementExists('seb_showsebdownloadlink')); // Should be in the form if we grant require permissions. assign_capability('quizaccess/seb:manage_seb_showsebdownloadlink', CAP_ALLOW, $this->roleid, $this->context->id); settings_provider::add_seb_settings_fields($this->mockedquizform, $this->mockedform); $this->assertTrue($this->mockedform->elementExists('seb_showsebdownloadlink')); } /** * Test Allowed Browser Exam Keys setting in the form. */ public function test_allowedbrowserexamkeys_in_form(): void { $this->resetAfterTest(); $this->setAdminUser(); $this->course = $this->getDataGenerator()->create_course(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG); $this->context = \context_module::instance($this->quiz->cmid); $this->set_up_user_and_role(); assign_capability('quizaccess/seb:manage_seb_requiresafeexambrowser', CAP_ALLOW, $this->roleid, $this->context->id); $this->set_up_form_mocks(); // Shouldn't be in the form if no permissions. settings_provider::add_seb_settings_fields($this->mockedquizform, $this->mockedform); $this->assertFalse($this->mockedform->elementExists('seb_allowedbrowserexamkeys')); // Should be in the form if we grant require permissions. assign_capability('quizaccess/seb:manage_seb_allowedbrowserexamkeys', CAP_ALLOW, $this->roleid, $this->context->id); settings_provider::add_seb_settings_fields($this->mockedquizform, $this->mockedform); $this->assertTrue($this->mockedform->elementExists('seb_allowedbrowserexamkeys')); } /** * Test the validation of a seb config file. */ public function test_validate_draftarea_configfile_success(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $xml = "\n" . "\n" . "hashedQuitPasswordhashedpassword" . "allowWlan\n"; $itemid = $this->create_test_draftarea_file($xml); $errors = settings_provider::validate_draftarea_configfile($itemid); $this->assertEmpty($errors); } /** * Test the validation of a missing seb config file. */ public function test_validate_draftarea_configfile_failure(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $xml = "This is not a config file."; $itemid = $this->create_test_draftarea_file($xml); $errors = settings_provider::validate_draftarea_configfile($itemid); $this->assertEquals($errors, new \lang_string('fileparsefailed', 'quizaccess_seb')); } /** * Test obtaining the draftarea content. */ public function test_get_current_user_draft_file(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $xml = file_get_contents(self::get_fixture_path(__NAMESPACE__, 'unencrypted.seb')); $itemid = $this->create_test_draftarea_file($xml); $file = settings_provider::get_current_user_draft_file($itemid); $content = $file->get_content(); $this->assertEquals($xml, $content); } /** * Test saving files from the user draft area into the quiz context area storage. */ public function test_save_filemanager_sebconfigfile_draftarea(): void { $this->resetAfterTest(); $this->course = $this->getDataGenerator()->create_course(); $this->quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $this->course->id]); $this->context = \context_module::instance($this->quiz->cmid); $this->set_up_user_and_role(); $xml = file_get_contents(self::get_fixture_path(__NAMESPACE__, 'unencrypted.seb')); $draftitemid = $this->create_test_draftarea_file($xml); settings_provider::save_filemanager_sebconfigfile_draftarea($draftitemid, $this->quiz->cmid); $fs = get_file_storage(); $files = $fs->get_area_files($this->context->id, 'quizaccess_seb', 'filemanager_sebconfigfile'); $this->assertCount(2, $files); } /** * Test deleting the $this->quiz->cmid itemid from the file area. */ public function test_delete_uploaded_config_file(): void { $this->resetAfterTest(); $this->course = $this->getDataGenerator()->create_course(); $this->quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $this->course->id]); $this->context = \context_module::instance($this->quiz->cmid); $this->set_up_user_and_role(); $xml = file_get_contents(self::get_fixture_path(__NAMESPACE__, 'unencrypted.seb')); $draftitemid = $this->create_test_draftarea_file($xml); settings_provider::save_filemanager_sebconfigfile_draftarea($draftitemid, $this->quiz->cmid); $fs = get_file_storage(); $files = $fs->get_area_files($this->context->id, 'quizaccess_seb', 'filemanager_sebconfigfile'); $this->assertCount(2, $files); settings_provider::delete_uploaded_config_file($this->quiz->cmid); $files = $fs->get_area_files($this->context->id, 'quizaccess_seb', 'filemanager_sebconfigfile'); // The '.' directory. $this->assertCount(1, $files); } /** * Test getting the file from the context module id file area. */ public function test_get_module_context_sebconfig_file(): void { $this->resetAfterTest(); $this->setAdminUser(); $this->course = $this->getDataGenerator()->create_course(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $this->context = \context_module::instance($this->quiz->cmid); $this->set_up_user_and_role(); $xml = file_get_contents(self::get_fixture_path(__NAMESPACE__, 'unencrypted.seb')); $draftitemid = $this->create_test_draftarea_file($xml); $fs = get_file_storage(); $files = $fs->get_area_files($this->context->id, 'quizaccess_seb', 'filemanager_sebconfigfile'); $this->assertCount(0, $files); settings_provider::save_filemanager_sebconfigfile_draftarea($draftitemid, $this->quiz->cmid); $settings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $settings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); $settings->save(); $file = settings_provider::get_module_context_sebconfig_file($this->quiz->cmid); $this->assertSame($file->get_content(), $xml); } /** * Test file manager options. */ public function test_get_filemanager_options(): void { $expected = [ 'subdirs' => 0, 'maxfiles' => 1, 'accepted_types' => ['.seb'] ]; $this->assertSame($expected, settings_provider::get_filemanager_options()); } /** * Test that users can or can not configure seb settings. */ public function test_can_configure_seb(): void { $this->resetAfterTest(); $this->course = $this->getDataGenerator()->create_course(); $this->quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $this->course->id]); $this->context = \context_module::instance($this->quiz->cmid); $this->setAdminUser(); $this->assertTrue(settings_provider::can_configure_seb($this->context)); $this->set_up_user_and_role(); $this->assertFalse(settings_provider::can_configure_seb($this->context)); assign_capability('quizaccess/seb:manage_seb_requiresafeexambrowser', CAP_ALLOW, $this->roleid, $this->context->id); $this->assertTrue(settings_provider::can_configure_seb($this->context)); } /** * Test that users can or can not use seb template. */ public function test_can_use_seb_template(): void { $this->resetAfterTest(); $this->course = $this->getDataGenerator()->create_course(); $this->quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $this->course->id]); $this->context = \context_module::instance($this->quiz->cmid); $this->setAdminUser(); $this->assertTrue(settings_provider::can_use_seb_template($this->context)); $this->set_up_user_and_role(); $this->assertFalse(settings_provider::can_use_seb_template($this->context)); assign_capability('quizaccess/seb:manage_seb_templateid', CAP_ALLOW, $this->roleid, $this->context->id); $this->assertTrue(settings_provider::can_use_seb_template($this->context)); } /** * Test that users can or can not upload seb config file. */ public function test_can_upload_seb_file(): void { $this->resetAfterTest(); $this->course = $this->getDataGenerator()->create_course(); $this->quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $this->course->id]); $this->context = \context_module::instance($this->quiz->cmid); $this->setAdminUser(); $this->assertTrue(settings_provider::can_upload_seb_file($this->context)); $this->set_up_user_and_role(); $this->assertFalse(settings_provider::can_upload_seb_file($this->context)); assign_capability('quizaccess/seb:manage_filemanager_sebconfigfile', CAP_ALLOW, $this->roleid, $this->context->id); $this->assertTrue(settings_provider::can_upload_seb_file($this->context)); } /** * Test that users can or can not change Show Safe Exam Browser download button setting. */ public function test_can_change_seb_showsebdownloadlink(): void { $this->resetAfterTest(); $this->course = $this->getDataGenerator()->create_course(); $this->quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $this->course->id]); $this->context = \context_module::instance($this->quiz->cmid); $this->setAdminUser(); $this->assertTrue(settings_provider::can_change_seb_showsebdownloadlink($this->context)); $this->set_up_user_and_role(); $this->assertFalse(settings_provider::can_change_seb_showsebdownloadlink($this->context)); assign_capability('quizaccess/seb:manage_seb_showsebdownloadlink', CAP_ALLOW, $this->roleid, $this->context->id); $this->assertTrue(settings_provider::can_change_seb_showsebdownloadlink($this->context)); } /** * Test that users can or can not change Allowed Browser Exam Keys setting. */ public function test_can_change_seb_allowedbrowserexamkeys(): void { $this->resetAfterTest(); $this->course = $this->getDataGenerator()->create_course(); $this->quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $this->course->id]); $this->context = \context_module::instance($this->quiz->cmid); $this->setAdminUser(); $this->assertTrue(settings_provider::can_change_seb_allowedbrowserexamkeys($this->context)); $this->set_up_user_and_role(); $this->assertFalse(settings_provider::can_change_seb_allowedbrowserexamkeys($this->context)); assign_capability('quizaccess/seb:manage_seb_allowedbrowserexamkeys', CAP_ALLOW, $this->roleid, $this->context->id); $this->assertTrue(settings_provider::can_change_seb_allowedbrowserexamkeys($this->context)); } /** * Test that users can or can not Configure SEb manually * * @param string $settingcapability Setting capability to check manual option against. * * @dataProvider settings_capability_data_provider */ public function test_can_configure_manually($settingcapability): void { $this->resetAfterTest(); $this->course = $this->getDataGenerator()->create_course(); $this->quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $this->course->id]); $this->context = \context_module::instance($this->quiz->cmid); $this->setAdminUser(); $this->assertTrue(settings_provider::can_configure_manually($this->context)); $this->set_up_user_and_role(); $this->assertFalse(settings_provider::can_configure_manually($this->context)); assign_capability('quizaccess/seb:manage_seb_configuremanually', CAP_ALLOW, $this->roleid, $this->context->id); $this->assertFalse(settings_provider::can_configure_manually($this->context)); assign_capability($settingcapability, CAP_ALLOW, $this->roleid, $this->context->id); $this->assertTrue(settings_provider::can_configure_manually($this->context)); } /** * Test that we can check if the seb settings are locked. */ public function test_is_seb_settings_locked(): void { $this->resetAfterTest(); $this->course = $this->getDataGenerator()->create_course(); $this->quiz = $this->create_test_quiz($this->course); $user = $this->getDataGenerator()->create_user(); $this->assertFalse(settings_provider::is_seb_settings_locked($this->quiz->id)); $this->attempt_quiz($this->quiz, $user); $this->assertTrue(settings_provider::is_seb_settings_locked($this->quiz->id)); } /** * Test that we can check identify conflicting permissions if set to use template. */ public function test_is_conflicting_permissions_for_manage_templates(): void { $this->resetAfterTest(); $this->setAdminUser(); $this->course = $this->getDataGenerator()->create_course(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $this->context = \context_module::instance($this->quiz->cmid); // Create a template. $template = $this->create_template(); $settings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $settings->set('templateid', $template->get('id')); $settings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $settings->save(); $this->assertFalse(settings_provider::is_conflicting_permissions($this->context)); $this->set_up_user_and_role(); $this->assertTrue(settings_provider::is_conflicting_permissions($this->context)); assign_capability('quizaccess/seb:manage_seb_templateid', CAP_ALLOW, $this->roleid, $this->context->id); $this->assertFalse(settings_provider::is_conflicting_permissions($this->context)); } /** * Test that we can check identify conflicting permissions if set to use own seb file. */ public function test_is_conflicting_permissions_for_upload_seb_file(): void { $this->resetAfterTest(); $this->setAdminUser(); $this->course = $this->getDataGenerator()->create_course(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $this->context = \context_module::instance($this->quiz->cmid); // Save file. $xml = file_get_contents(self::get_fixture_path(__NAMESPACE__, 'unencrypted.seb')); $draftitemid = $this->create_test_draftarea_file($xml); settings_provider::save_filemanager_sebconfigfile_draftarea($draftitemid, $this->quiz->cmid); $settings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $settings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); $settings->save(); $this->assertFalse(settings_provider::is_conflicting_permissions($this->context)); $this->set_up_user_and_role(); assign_capability('quizaccess/seb:manage_filemanager_sebconfigfile', CAP_ALLOW, $this->roleid, $this->context->id); $this->assertFalse(settings_provider::is_conflicting_permissions($this->context)); } /** * Test that we can check identify conflicting permissions if set to use own configure manually. * * @param string $settingcapability Setting capability to check manual option against. * * @dataProvider settings_capability_data_provider */ public function test_is_conflicting_permissions_for_configure_manually($settingcapability): void { $this->resetAfterTest(); $this->setAdminUser(); $this->course = $this->getDataGenerator()->create_course(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); $this->context = \context_module::instance($this->quiz->cmid); $this->assertFalse(settings_provider::is_conflicting_permissions($this->context)); $this->set_up_user_and_role(); $this->assertTrue(settings_provider::is_conflicting_permissions($this->context)); assign_capability('quizaccess/seb:manage_seb_configuremanually', CAP_ALLOW, $this->roleid, $this->context->id); $this->assertTrue(settings_provider::is_conflicting_permissions($this->context)); assign_capability($settingcapability, CAP_ALLOW, $this->roleid, $this->context->id); $this->assertFalse(settings_provider::is_conflicting_permissions($this->context)); } /** * Test add_prefix helper method. */ public function test_add_prefix(): void { $this->assertEquals('seb_one', settings_provider::add_prefix('one')); $this->assertEquals('seb_two', settings_provider::add_prefix('seb_two')); $this->assertEquals('seb_seb_three', settings_provider::add_prefix('seb_seb_three')); $this->assertEquals('seb_', settings_provider::add_prefix('seb_')); $this->assertEquals('seb_', settings_provider::add_prefix('')); $this->assertEquals('seb_one_seb', settings_provider::add_prefix('one_seb')); } /** * Test filter_plugin_settings helper method. */ public function test_filter_plugin_settings(): void { $test = new \stdClass(); $test->one = 'one'; $test->seb_two = 'two'; $test->seb_seb_three = 'three'; $test->four = 'four'; $newsettings = (array)settings_provider::filter_plugin_settings($test); $this->assertFalse(key_exists('one', $newsettings)); $this->assertFalse(key_exists('four', $newsettings)); $this->assertCount(2, $newsettings); $this->assertEquals('two', $newsettings['two']); $this->assertEquals('three', $newsettings['seb_three']); } /** * Helper method to get a list of settings. * * @return \stdClass */ protected function get_settings() { $allsettings = new \stdClass(); $allsettings->seb_showsebdownloadlink = 0; $allsettings->seb_linkquitseb = 2; $allsettings->seb_userconfirmquit = 3; $allsettings->seb_allowuserquitseb = 4; $allsettings->seb_quitpassword = 5; $allsettings->seb_allowreloadinexam = 6; $allsettings->seb_showsebtaskbar = 7; $allsettings->seb_showreloadbutton = 8; $allsettings->seb_showtime = 9; $allsettings->seb_showkeyboardlayout = 10; $allsettings->seb_showwificontrol = 11; $allsettings->seb_enableaudiocontrol = 12; $allsettings->seb_muteonstartup = 13; $allsettings->seb_allowspellchecking = 14; $allsettings->seb_activateurlfiltering = 15; $allsettings->seb_filterembeddedcontent = 16; $allsettings->seb_expressionsallowed = 17; $allsettings->seb_regexallowed = 18; $allsettings->seb_expressionsblocked = 19; $allsettings->seb_regexblocked = 20; $allsettings->seb_templateid = 21; $allsettings->seb_allowedbrowserexamkeys = 22; $allsettings->seb_allowcapturecamera = 23; $allsettings->seb_allowcapturemicrophone = 24; return $allsettings; } /** * Helper method to assert results of filter_plugin_settings * * @param int $type Type of SEB usage. * @param array $notnulls A list of expected not null settings. */ protected function assert_filter_plugin_settings(int $type, array $notnulls) { $allsettings = $this->get_settings(); $allsettings->seb_requiresafeexambrowser = $type; $actual = settings_provider::filter_plugin_settings($allsettings); $expected = (array)$allsettings; foreach ($actual as $name => $value) { if (in_array($name, $notnulls)) { $this->assertEquals($expected['seb_' . $name], $value); } else { $this->assertNull($value); } } } /** * Test filter_plugin_settings method for no SEB case. */ public function test_filter_plugin_settings_for_no_seb(): void { $notnulls = ['requiresafeexambrowser']; $this->assert_filter_plugin_settings(settings_provider::USE_SEB_NO, $notnulls); } /** * Test filter_plugin_settings method for using uploaded config. */ public function test_filter_plugin_settings_for_uploaded_config(): void { $notnulls = ['requiresafeexambrowser', 'showsebdownloadlink', 'allowedbrowserexamkeys']; $this->assert_filter_plugin_settings(settings_provider::USE_SEB_UPLOAD_CONFIG, $notnulls); } /** * Test filter_plugin_settings method for using template. */ public function test_filter_plugin_settings_for_template(): void { $notnulls = ['requiresafeexambrowser', 'showsebdownloadlink', 'allowuserquitseb', 'quitpassword', 'templateid']; $this->assert_filter_plugin_settings(settings_provider::USE_SEB_TEMPLATE, $notnulls); } /** * Test filter_plugin_settings method for using client config. */ public function test_filter_plugin_settings_for_client_config(): void { $notnulls = ['requiresafeexambrowser', 'showsebdownloadlink', 'allowedbrowserexamkeys']; $this->assert_filter_plugin_settings(settings_provider::USE_SEB_CLIENT_CONFIG, $notnulls); } /** * Test filter_plugin_settings method for manually configured SEB. */ public function test_filter_plugin_settings_for_configure_manually(): void { $allsettings = $this->get_settings(); $allsettings->seb_requiresafeexambrowser = settings_provider::USE_SEB_CONFIG_MANUALLY; $actual = settings_provider::filter_plugin_settings($allsettings); // For manual it's easier to check nulls, as most of the settings are not null. $nulls = ['templateid', 'allowedbrowserexamkeys']; $expected = (array)$allsettings; foreach ($actual as $name => $value) { if (in_array($name, $nulls)) { $this->assertNull($value); } else { $this->assertEquals($expected['seb_' . $name], $value); } } } /** * Test settings map. */ public function test_get_seb_settings_map(): void { $expected = [ settings_provider::USE_SEB_NO => [ ], settings_provider::USE_SEB_CONFIG_MANUALLY => [ 'seb_showsebdownloadlink' => [], 'seb_linkquitseb' => [], 'seb_userconfirmquit' => [], 'seb_allowuserquitseb' => [ 'seb_quitpassword' => [] ], 'seb_allowreloadinexam' => [], 'seb_showsebtaskbar' => [ 'seb_showreloadbutton' => [], 'seb_showtime' => [], 'seb_showkeyboardlayout' => [], 'seb_showwificontrol' => [], ], 'seb_enableaudiocontrol' => [ 'seb_muteonstartup' => [], ], 'seb_allowcapturecamera' => [], 'seb_allowcapturemicrophone' => [], 'seb_allowspellchecking' => [], 'seb_activateurlfiltering' => [ 'seb_filterembeddedcontent' => [], 'seb_expressionsallowed' => [], 'seb_regexallowed' => [], 'seb_expressionsblocked' => [], 'seb_regexblocked' => [], ], ], settings_provider::USE_SEB_TEMPLATE => [ 'seb_templateid' => [], 'seb_showsebdownloadlink' => [], 'seb_allowuserquitseb' => [ 'seb_quitpassword' => [], ], ], settings_provider::USE_SEB_UPLOAD_CONFIG => [ 'filemanager_sebconfigfile' => [], 'seb_showsebdownloadlink' => [], 'seb_allowedbrowserexamkeys' => [], ], settings_provider::USE_SEB_CLIENT_CONFIG => [ 'seb_showsebdownloadlink' => [], 'seb_allowedbrowserexamkeys' => [], ], ]; $this->assertEquals($expected, settings_provider::get_seb_settings_map()); } } PK\YdIItests/config_key_test.phpnu[. namespace quizaccess_seb; /** * PHPUnit Tests for config_key class. * * @package quizaccess_seb * @author Andrew Madden * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \quizaccess_seb\config_key */ final class config_key_test extends \advanced_testcase { /** * Test that trying to generate the hash key with bad xml will result in an error. */ public function test_config_key_not_generated_with_bad_xml(): void { $this->expectException(\invalid_parameter_exception::class); $this->expectExceptionMessage("Invalid a PList XML string, representing SEB config"); config_key::generate("get_hash(); $this->assertEquals('4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945', $hash); } /** * Test config key hash is derived correctly by Moodle. * * @param string $config The SEB config file name. * @param string $hash The correct config key hash for this file. * * @dataProvider real_ck_hash_provider */ public function test_config_key_hash_is_derived_correctly($config, $hash): void { $xml = file_get_contents(self::get_fixture_path(__NAMESPACE__, $config)); $derivedhash = config_key::generate($xml)->get_hash(); $this->assertEquals($hash, $derivedhash); } /** * Check that the Config Key hash is not altered if the originatorVersion is present in the XML or not. */ public function test_presence_of_originator_version_does_not_effect_hash(): void { $xmlwithoriginatorversion = file_get_contents(self::get_fixture_path(__NAMESPACE__, 'simpleunencrypted.seb')); $xmlwithoutoriginatorversion = file_get_contents(self::get_fixture_path(__NAMESPACE__, 'simpleunencryptedwithoutoriginator.seb')); $hashwithorigver = config_key::generate($xmlwithoriginatorversion)->get_hash(); $hashwithoutorigver = config_key::generate($xmlwithoutoriginatorversion)->get_hash(); $this->assertEquals($hashwithorigver, $hashwithoutorigver); } /** * Provide a seb file, the expected Config Key and a password if encrypted. * * @return array */ public static function real_ck_hash_provider(): array { return [ 'unencrypted_mac2.1.4' => ['unencrypted_mac_001.seb', '4fa9af8ec8759eb7c680752ef4ee5eaf1a860628608fccae2715d519849f9292', ''], 'unencrypted_win2.2.3' => ['unencrypted_win_223.seb', '2534e4e9f3188f9f9133bf7cf7b4c5d898292bbd7e8d0230f39d1176636a1431', ''], ]; } } PK\:s s tests/link_generator_test.phpnu[. namespace quizaccess_seb; /** * PHPUnit tests for link_generator. * * @package quizaccess_seb * @author Andrew Madden * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class link_generator_test extends \advanced_testcase { /** * Called before every test. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(); } /** * Test that a http link is generated correctly. */ public function test_http_link_generated(): void { $course = $this->getDataGenerator()->create_course(); $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]); $this->assertEquals( "http://www.example.com/moodle/mod/quiz/accessrule/seb/config.php?cmid=$quiz->cmid", link_generator::get_link($quiz->cmid, false, false)); } /** * Test that a http link is generated correctly. */ public function test_https_link_generated(): void { $course = $this->getDataGenerator()->create_course(); $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]); $this->assertEquals( "https://www.example.com/moodle/mod/quiz/accessrule/seb/config.php?cmid=$quiz->cmid", link_generator::get_link($quiz->cmid, false)); } /** * Test that a seb link is generated correctly. */ public function test_seb_link_generated(): void { $course = $this->getDataGenerator()->create_course(); $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]); $this->assertEquals( "seb://www.example.com/moodle/mod/quiz/accessrule/seb/config.php?cmid=$quiz->cmid", link_generator::get_link($quiz->cmid, true, false)); } /** * Test that a sebs link is generated correctly. */ public function test_sebs_link_generated(): void { $course = $this->getDataGenerator()->create_course(); $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]); $this->assertEquals( "sebs://www.example.com/moodle/mod/quiz/accessrule/seb/config.php?cmid=$quiz->cmid", link_generator::get_link($quiz->cmid, true)); } /** * Test that link_generator can't not be instantiated with fake course module. */ public function test_course_module_does_not_exist(): void { $this->expectException(\dml_exception::class); $this->expectExceptionMessageMatches("/^Can't find data record in database.*/"); $generator = link_generator::get_link(123456, false); } } PK\Kp*uPuPtests/property_list_test.phpnu[. namespace quizaccess_seb; /** * PHPUnit for property_list class. * * @package quizaccess_seb * @author Andrew Madden * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class property_list_test extends \advanced_testcase { /** * Test that an empty PList with a root dictionary is created. */ public function test_create_empty_plist(): void { $emptyplist = new property_list(); $xml = trim($emptyplist->to_xml()); $this->assertEquals(' ', $xml); } /** * Test that a Plist is constructed from an XML string. */ public function test_construct_plist_from_xml(): void { $xml = $this->get_plist_xml_header() . "testKey" . "testValue" . $this->get_plist_xml_footer(); $plist = new property_list($xml); $generatedxml = trim($plist->to_xml()); $this->assertEquals(" testKeytestValue", $generatedxml); } /** * Test that an element can be added to the root dictionary. */ public function test_add_element_to_root(): void { $plist = new property_list(); $newelement = new \CFPropertyList\CFString('testValue'); $plist->add_element_to_root('testKey', $newelement); $generatedxml = trim($plist->to_xml()); $this->assertEquals(" testKeytestValue", $generatedxml); } /** * Test that an element's value can be retrieved. */ public function test_get_element_value(): void { $xml = $this->get_plist_xml_header() . "testKey" . "testValue" . $this->get_plist_xml_footer(); $plist = new property_list($xml); $this->assertEquals('testValue', $plist->get_element_value('testKey')); } /** * Test that an element's value can be retrieved. */ public function test_get_element_value_if_not_exists(): void { $plist = new property_list(); $this->assertEmpty($plist->get_element_value('testKey')); } /** * Test an element's value can be retrieved if it is an array. */ public function test_get_element_value_if_array(): void { $xml = $this->get_plist_xml_header() . "testDict" . "" . "testKey" . "testValue" . "" . $this->get_plist_xml_footer(); $plist = new property_list($xml); $this->assertEquals(['testKey' => 'testValue'], $plist->get_element_value('testDict')); } /** * Test that a element's value can be updated that is not an array or dictionary. * * @param string $xml XML to create PList. * @param string $key Key of element to try and update. * @param mixed $value Value to try to update with. * * @dataProvider good_update_data_provider */ public function test_updating_element_value($xml, $key, $value): void { $xml = $this->get_plist_xml_header() . $xml . $this->get_plist_xml_footer(); $plist = new property_list($xml); $plist->update_element_value($key, $value); $this->assertEquals($value, $plist->get_element_value($key)); } /** * Test that a element's value can be updated that is not an array or dictionary. * * @param string $xml XML to create PList. * @param string $key Key of element to try and update. * @param mixed $value Bad value to try to update with. * @param mixed $expected Expected value of element after update is called. * @param string $exceptionmessage Message of exception expected to be thrown. * @dataProvider bad_update_data_provider */ public function test_updating_element_value_with_bad_data(string $xml, string $key, $value, $expected, $exceptionmessage): void { $xml = $this->get_plist_xml_header() . $xml . $this->get_plist_xml_footer(); $plist = new property_list($xml); $this->expectException(\invalid_parameter_exception::class); $this->expectExceptionMessage($exceptionmessage); $plist->update_element_value($key, $value); $plistarray = json_decode($plist->to_json()); // Export elements. $this->assertEquals($expected, $plistarray->$key); } /** * Test that a dictionary can have it's value (array) updated. */ public function test_updating_element_array_if_dictionary(): void { $xml = $this->get_plist_xml_header() . "testDict" . "" . "testKey" . "testValue" . "" . $this->get_plist_xml_footer(); $plist = new property_list($xml); $plist->update_element_array('testDict', ['newKey' => new \CFPropertyList\CFString('newValue')]); $this->assertEquals(['newKey' => 'newValue'], $plist->get_element_value('testDict')); } /** * Test that a dictionary can have it's value (array) updated. */ public function test_updating_element_array_if_dictionary_with_bad_data(): void { $xml = $this->get_plist_xml_header() . "testDict" . "" . "testKey" . "testValue" . "" . $this->get_plist_xml_footer(); $plist = new property_list($xml); $this->expectException(\invalid_parameter_exception::class); $this->expectExceptionMessage('New array must only contain CFType objects.'); $plist->update_element_array('testDict', [false]); $this->assertEquals(['testKey' => 'testValue'], $plist->get_element_value('testDict')); $this->assertDebuggingCalled('property_list: If updating an array in PList, it must only contain CFType objects.', DEBUG_DEVELOPER); } /** * Test that an element can be deleted. */ public function test_delete_element(): void { $xml = $this->get_plist_xml_header() . "testKey" . "testValue" . $this->get_plist_xml_footer(); $plist = new property_list($xml); $plist->delete_element('testKey'); $generatedxml = trim($plist->to_xml()); $this->assertEquals(" ", $generatedxml); } /** * Test that an element can be deleted. */ public function test_delete_element_if_not_exists(): void { $xml = $this->get_plist_xml_header() . "testKey" . "testValue" . $this->get_plist_xml_footer(); $plist = new property_list($xml); $plist->delete_element('nonExistentKey'); $generatedxml = trim($plist->to_xml()); // The xml should be unaltered. $this->assertEquals(" testKeytestValue", $generatedxml); } /** * Test that json is exported correctly according to SEB Config Key requirements. * * @param string $xml PList XML used to generate CFPropertyList. * @param string $expectedjson Expected JSON output. * * @dataProvider json_data_provider */ public function test_export_to_json($xml, $expectedjson): void { $xml = $this->get_plist_xml_header() . $xml . $this->get_plist_xml_footer(); $plist = new property_list($xml); $generatedjson = $plist->to_json(); $this->assertEquals($expectedjson, $generatedjson); } /** * Test that the xml is exported to JSON from a real SEB config file. Expected JSON extracted from SEB logs. */ public function test_export_to_json_full_file(): void { $xml = file_get_contents(self::get_fixture_path(__NAMESPACE__, 'unencrypted_mac_001.seb')); $plist = new property_list($xml); $plist->delete_element('originatorVersion'); // JSON should not contain originatorVersion key. $generatedjson = $plist->to_json(); $json = trim(file_get_contents(self::get_fixture_path(__NAMESPACE__, 'JSON_unencrypted_mac_001.txt'))); $this->assertEquals($json, $generatedjson); } /** * Test the set_or_update_value function. */ public function test_set_or_update_value(): void { $plist = new property_list(); $this->assertEmpty($plist->get_element_value('string')); $this->assertEmpty($plist->get_element_value('bool')); $this->assertEmpty($plist->get_element_value('number')); // Setting values. $plist->set_or_update_value('string', new \CFPropertyList\CFString('initial string')); $plist->set_or_update_value('bool', new \CFPropertyList\CFBoolean(true)); $plist->set_or_update_value('number', new \CFPropertyList\CFNumber('10')); $this->assertEquals('initial string', $plist->get_element_value('string')); $this->assertEquals(true, $plist->get_element_value('bool')); $this->assertEquals(10, $plist->get_element_value('number')); // Updating values. $plist->set_or_update_value('string', new \CFPropertyList\CFString('new string')); $plist->set_or_update_value('bool', new \CFPropertyList\CFBoolean(false)); $plist->set_or_update_value('number', new \CFPropertyList\CFNumber('42')); $this->assertEquals('new string', $plist->get_element_value('string')); $this->assertEquals(false, $plist->get_element_value('bool')); $this->assertEquals(42, $plist->get_element_value('number')); // Type exception. $this->expectException(\TypeError::class); $plist->set_or_update_value('someKey', 'We really need to pass in CFTypes here'); } /** * Get a valid PList header. Must also use footer. * * @return string */ private function get_plist_xml_header(): string { return "\n" . "\n" . "\n" . " "; } /** * Get a valid PList footer. Must also use header. * * @return string */ private function get_plist_xml_footer(): string { return " \n" . ""; } /** * Data provider for good data on update. * * @return array Array with test data. */ public static function good_update_data_provider(): array { return [ 'Update string' => ['testKeytestValue', 'testKey', 'newValue'], 'Update bool' => ['testKey', 'testKey', false], 'Update number' => ['testKey888', 'testKey', 123.4], ]; } /** * Data provider for bad data on update. * * @return array Array with test data. */ public static function bad_update_data_provider(): array { return [ 'Update string with bool' => ['testKeytestValue', 'testKey', true, 'testValue', 'Invalid parameter value detected (Only string, number and boolean elements can be updated, ' . 'or value type does not match element type: CFPropertyList\CFString'], 'Update string with number' => ['testKeytestValue', 'testKey', 999, 'testValue', 'Invalid parameter value detected (Only string, number and boolean elements can be updated, ' . 'or value type does not match element type: CFPropertyList\CFString'], 'Update string with null' => ['testKeytestValue', 'testKey', null, 'testValue', 'Invalid parameter value detected (Only string, number and boolean elements can be updated, ' . 'or value type does not match element type: CFPropertyList\CFString'], 'Update string with array' => ['testKeytestValue', 'testKey', ['arrayValue'], 'testValue', 'Use update_element_array to update a collection.'], 'Update bool with string' => ['testKey', 'testKey', 'testValue', true, 'Invalid parameter value detected (Only string, number and boolean elements can be updated, ' . 'or value type does not match element type: CFPropertyList\CFBool'], 'Update bool with number' => ['testKey', 'testKey', 999, true, 'Invalid parameter value detected (Only string, number and boolean elements can be updated, ' . 'or value type does not match element type: CFPropertyList\CFBool'], 'Update bool with null' => ['testKey', 'testKey', null, true, 'Invalid parameter value detected (Only string, number and boolean elements can be updated, ' . 'or value type does not match element type: CFPropertyList\CFBool'], 'Update bool with array' => ['testKey', 'testKey', ['testValue'], true, 'Use update_element_array to update a collection.'], 'Update number with string' => ['testKey888', 'testKey', 'string', 888, 'Invalid parameter value detected (Only string, number and boolean elements can be updated, ' . 'or value type does not match element type: CFPropertyList\CFNumber'], 'Update number with bool' => ['testKey888', 'testKey', true, 888, 'Invalid parameter value detected (Only string, number and boolean elements can be updated, ' . 'or value type does not match element type: CFPropertyList\CFNumber'], 'Update number with null' => ['testKey888', 'testKey', null, 888, 'Invalid parameter value detected (Only string, number and boolean elements can be updated, ' . 'or value type does not match element type: CFPropertyList\CFNumber'], 'Update number with array' => ['testKey888', 'testKey', ['testValue'], 888, 'Use update_element_array to update a collection.'], 'Update date with string' => ['testKey1940-10-09T22:13:56Z', 'testKey', 'string', '1940-10-10T06:13:56+08:00', 'Invalid parameter value detected (Only string, number and boolean elements can be updated, ' . 'or value type does not match element type: CFPropertyList\CFDate'], 'Update data with number' => ['testKeytestData', 'testKey', 789, 'testData', 'Invalid parameter value detected (Only string, number and boolean elements can be updated, ' . 'or value type does not match element type: CFPropertyList\CFData'], ]; } /** * Data provider for expected JSON from PList. * * Examples extracted from requirements listed in SEB Config Key documents. * https://safeexambrowser.org/developer/seb-config-key.html * * 1. Date should be in ISO 8601 format. * 2. Data should be base 64 encoded. * 3. String should be UTF-8 encoded. * 4, 5, 6, 7. No requirements for bools, arrays or dicts. * 8. Empty dicts should not be included. * 9. JSON key ordering should be case insensitive, and use string ordering. * 10. URL forward slashes should not be escaped. * * @return array */ public static function json_data_provider(): array { $data = "blahblah"; $base64data = base64_encode($data); return [ 'date' => ["date1940-10-09T22:13:56Z", "{\"date\":\"1940-10-09T22:13:56+00:00\"}"], 'data' => ["data$base64data", "{\"data\":\"$base64data\"}"], 'string' => ["stringhello wörld", "{\"string\":\"hello wörld\"}"], 'string with 1 backslash' => ["stringws:\localhost", "{\"string\":\"ws:\localhost\"}"], 'string with 2 backslashes' => ["stringws:\\localhost", '{"string":"ws:\\localhost"}'], 'string with 3 backslashes' => ["stringws:\\\localhost", '{"string":"ws:\\\localhost"}'], 'string with 4 backslashes' => ["stringws:\\\\localhost", '{"string":"ws:\\\\localhost"}'], 'string with 5 backslashes' => ["stringws:\\\\\localhost", '{"string":"ws:\\\\\localhost"}'], 'bool' => ["bool", "{\"bool\":true}"], 'array' => ["arrayarrayboolarraybool2" , "{\"array\":[false,true]}"], 'empty array' => ["boolarray" , "{\"array\":[],\"bool\":true}"], 'dict' => ["dictdictbooldictbool2" , "{\"dict\":{\"dictbool\":false,\"dictbool2\":true}}"], 'empty dict' => ["boolemptydict", "{\"bool\":true}"], 'unordered elements' => ["testKey" . "testValue" . "allowWLAN" . "testValue2" . "allowWlan" . "testValue3" , "{\"allowWlan\":\"testValue3\",\"allowWLAN\":\"testValue2\",\"testKey\":\"testValue\"}"], 'url' => ["urlhttp://test.com", "{\"url\":\"http://test.com\"}"], 'assoc dict' => ["dictbananaapple", "{\"dict\":{\"apple\":true,\"banana\":false}}"], 'seq array' => ["array12 3456 78910", "{\"array\":[false,true,true,true,true,true,true,true,true,true]}"], ]; } } PK\էDwtests/event/events_test.phpnu[. namespace quizaccess_seb\event; use mod_quiz\quiz_settings; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/..//test_helper_trait.php'); /** * PHPUnit tests for all plugin events. * * @package quizaccess_seb * @author Andrew Madden * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class events_test extends \advanced_testcase { use \quizaccess_seb_test_helper_trait; /** * Called before every test. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(); $this->course = $this->getDataGenerator()->create_course(); } /** * Test creating the access_prevented event. * * @covers \quizaccess_seb\event\access_prevented */ public function test_event_access_prevented(): void { $this->resetAfterTest(); $this->setAdminUser(); $quiz = $this->create_test_quiz($this->course, \quizaccess_seb\settings_provider::USE_SEB_CONFIG_MANUALLY); $accessmanager = new \quizaccess_seb\seb_access_manager(new quiz_settings($quiz, get_coursemodule_from_id('quiz', $quiz->cmid), $this->course)); // Set up event with data. $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = 'configkey'; $_SERVER['HTTP_X_SAFEEXAMBROWSER_REQUESTHASH'] = 'browserexamkey'; $event = \quizaccess_seb\event\access_prevented::create_strict($accessmanager, 'Because I said so.'); // Create an event sink, trigger event and retrieve event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertEquals(1, count($events)); $event = reset($events); $expectedconfigkey = $accessmanager->get_valid_config_key(); // Test that the event data is as expected. $this->assertInstanceOf('\quizaccess_seb\event\access_prevented', $event); $this->assertEquals('Quiz access was prevented', $event->get_name()); $this->assertEquals( "The user with id '$user->id' has been prevented from accessing quiz with id '$quiz->id' by the " . "Safe Exam Browser access plugin. The reason was 'Because I said so.'. " . "Expected config key: '$expectedconfigkey'. " . "Received config key: 'configkey'. Received browser exam key: 'browserexamkey'.", $event->get_description()); $this->assertEquals(\context_module::instance($quiz->cmid), $event->get_context()); $this->assertEquals($user->id, $event->userid); $this->assertEquals($quiz->id, $event->objectid); $this->assertEquals($this->course->id, $event->courseid); $this->assertEquals('Because I said so.', $event->other['reason']); $this->assertEquals($expectedconfigkey, $event->other['savedconfigkey']); $this->assertEquals('configkey', $event->other['receivedconfigkey']); $this->assertEquals('browserexamkey', $event->other['receivedbrowserexamkey']); } /** * Test creating the access_prevented event with provided SEB keys. * * @covers \quizaccess_seb\event\access_prevented */ public function test_event_access_prevented_with_keys(): void { $this->resetAfterTest(); $this->setAdminUser(); $quiz = $this->create_test_quiz($this->course, \quizaccess_seb\settings_provider::USE_SEB_CONFIG_MANUALLY); $accessmanager = new \quizaccess_seb\seb_access_manager(new quiz_settings($quiz, get_coursemodule_from_id('quiz', $quiz->cmid), $this->course)); // Set up event with data. $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $event = \quizaccess_seb\event\access_prevented::create_strict($accessmanager, 'Because I said so.', 'configkey', 'browserexamkey'); // Create an event sink, trigger event and retrieve event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertEquals(1, count($events)); $event = reset($events); $expectedconfigkey = $accessmanager->get_valid_config_key(); // Test that the event data is as expected. $this->assertInstanceOf('\quizaccess_seb\event\access_prevented', $event); $this->assertEquals('Quiz access was prevented', $event->get_name()); $this->assertEquals( "The user with id '$user->id' has been prevented from accessing quiz with id '$quiz->id' by the " . "Safe Exam Browser access plugin. The reason was 'Because I said so.'. " . "Expected config key: '$expectedconfigkey'. " . "Received config key: 'configkey'. Received browser exam key: 'browserexamkey'.", $event->get_description()); $this->assertEquals(\context_module::instance($quiz->cmid), $event->get_context()); $this->assertEquals($user->id, $event->userid); $this->assertEquals($quiz->id, $event->objectid); $this->assertEquals($this->course->id, $event->courseid); $this->assertEquals('Because I said so.', $event->other['reason']); $this->assertEquals($expectedconfigkey, $event->other['savedconfigkey']); $this->assertEquals('configkey', $event->other['receivedconfigkey']); $this->assertEquals('browserexamkey', $event->other['receivedbrowserexamkey']); } /** * Test creating the template_created event. * * @covers \quizaccess_seb\event\template_created */ public function test_event_create_template(): void { $this->resetAfterTest(); // Set up event with data. $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $template = $this->create_template(); $event = \quizaccess_seb\event\template_created::create_strict( $template, \context_system::instance()); // Create an event sink, trigger event and retrieve event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertEquals(1, count($events)); $event = reset($events); // Test that the event data is as expected. $this->assertInstanceOf('\quizaccess_seb\event\template_created', $event); $this->assertEquals('SEB template was created', $event->get_name()); $this->assertEquals( "The user with id '$user->id' has created a template with id '{$template->get('id')}'.", $event->get_description() ); $this->assertEquals(\context_system::instance(), $event->get_context()); $this->assertEquals($user->id, $event->userid); $this->assertEquals($template->get('id'), $event->objectid); } } PK\E; tests/behat/seb_settings.featurenu[@mod_quiz @quizaccess @quizaccess_seb Feature: SEB settings in quiz access rule Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | Scenario Outline: Require the use of Safe Exam Browser set to Yes – Configure manually shows message to students Given the following "activities" exist: | activity | course | name | seb_requiresafeexambrowser | idnumber | | quiz | C1 | Quiz 1 | | quiz1 | And the following "question categories" exist: | contextlevel | reference | name | | Activity module | quiz1 | Test questions | And the following "questions" exist: | questioncategory | qtype | name | questiontext | | Test questions | truefalse | Reading | Can you read this? | And quiz "Quiz 1" contains the following questions: | question | page | | Reading | 1 | When I am on the "Quiz 1" "quiz activity" page logged in as student1 Then I see "This quiz has been configured so that students may only attempt it using the Safe Exam Browser." And "Attempt quiz" "button" exist Examples: | sebsetting | messagevisibility | attemptbuttonvisibility | | 0 | should not | should | | 1 | should | should not | Scenario Outline: Show Safe Exam Browser download button setting controls visibility of download link Given the following "activities" exist: | activity | course | name | idnumber | seb_requiresafeexambrowser | seb_showsebdownloadlink | | quiz | C1 | Quiz 1 | quiz1 | 1 | | And the following "question categories" exist: | contextlevel | reference | name | | Activity module | quiz1 | Test questions | And the following "questions" exist: | questioncategory | qtype | name | questiontext | | Test questions | truefalse | Reading | Can you read this? | And quiz "Quiz 1" contains the following questions: | question | page | | Reading | 1 | When I am on the "Quiz 1" "quiz activity" page logged in as student1 Then "Download Safe Exam Browser" "link" exist And I should see "This quiz has been configured so that students may only attempt it using the Safe Exam Browser." Examples: | seb_showsebdownloadlink | downloadseblinkvisibility | | 0 | should not | | 1 | should | PK\2tests/behat/secure_layout_activity_heading.featurenu[@mod_quiz @quizaccess @quizaccess_seb Feature: View the activity header when Safe Exam Browser is required In order to correctly identify the quiz when Safe Exam Browser is required As a student I need to be able to see the quiz information in the activity header Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Sam1 | Student1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | And the following "question categories" exist: | contextlevel | reference | name | | Course | C1 | Test questions | And the following "questions" exist: | questioncategory | qtype | name | questiontext | | Test questions | truefalse | TF1 | TF question | And the following "activity" exists: | activity | quiz | | course | C1 | | idnumber | 00001 | | name | Test quiz name | | intro | Test quiz description | | seb_requiresafeexambrowser | 1 | | grade | 10 | And quiz "Test quiz name" contains the following questions: | question | page | | TF1 | 1 | Scenario: Quiz description is displayed when Safe Exam Browser is required When I am on the "Test quiz name" "quiz activity" page logged in as student1 Then I should see "Launch Safe Exam Browser" And "Test quiz name" "heading" should exist And I should see "Test quiz description" PK\277tests/behat/edit_form.featurenu[@javascript @mod_quiz @quizaccess @quizaccess_seb Feature: Safe Exam Browser settings in quiz edit form Background: Given the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "activities" exist: | activity | course | section | name | | quiz | C1 | 1 | Quiz 1 | | quiz | C1 | 1 | Quiz 2 | Scenario: Quiz setting "Require the use of Safe Exam Browser" has all types, except "Use an existing template". When I am on the "Quiz 1" "quiz activity editing" page logged in as admin And I expand all fieldsets And the "Require the use of Safe Exam Browser" select box should contain "Yes – Configure manually" And the "Require the use of Safe Exam Browser" select box should not contain "Yes – Use an existing template" And the "Require the use of Safe Exam Browser" select box should contain "Yes – Upload my own config" And the "Require the use of Safe Exam Browser" select box should contain "Yes – Use SEB client config" And the field "Require the use of Safe Exam Browser" matches value "No" Scenario: Quiz setting "Require the use of Safe Exam Browser" has all types if at least one template has been added. Given the following "quizaccess_seb > seb templates" exist: | name | | Template 1 | When I am on the "Quiz 1" "quiz activity editing" page logged in as admin And I expand all fieldsets And the "Require the use of Safe Exam Browser" select box should contain "Yes – Configure manually" And the "Require the use of Safe Exam Browser" select box should contain "Yes – Use an existing template" And the "Require the use of Safe Exam Browser" select box should contain "Yes – Upload my own config" And the "Require the use of Safe Exam Browser" select box should contain "Yes – Use SEB client config" And the field "Require the use of Safe Exam Browser" matches value "No" Scenario: Quiz can be edited without capability to select SEB template Given the following "permission override" exists: | role | editingteacher | | capability | quizaccess/seb:manage_seb_templateid | | permission | Prevent | | contextlevel | System | | reference | | And the following "user" exists: | username | teacher | | firstname | Teacher | | lastname | One | And the following "course enrolment" exists: | user | teacher | | course | C1 | | role | editingteacher | And I log in as "teacher" # Create the quiz. When I add a quiz activity to course "Course 1" section "0" and I fill the form with: | Name | My quiz | Then I should not see "New Quiz" # Edit the quiz. And I am on the "My quiz" "quiz activity editing" page And I set the field "Name" to "My quiz edited" And I press "Save and return to course" And I should not see "Edit settings" Scenario: SEB settings if using No SEB Given the following "quizaccess_seb > seb templates" exist: | name | | Template 1 | And I am on the "Quiz 1" "quiz activity editing" page logged in as admin And I expand all fieldsets And I set the field "Require the use of Safe Exam Browser" to "No" Then I should not see "Upload Safe Exam Browser config file" Then I should not see "Safe Exam Browser config template" Then I should not see "Template 1" Then I should not see "Show Safe Exam Browser download button" Then I should not see "Enable quitting of SEB" Then I should not see "Quit password" Then I should not see "Allowed browser exam keys" Then I should not see "Show Exit Safe Exam Browser button, configured with this quit link" Then I should not see "Ask user to confirm quitting" Then I should not see "Enable reload in exam" Then I should not see "Show SEB task bar" Then I should not see "Show reload button" Then I should not see "Show time" Then I should not see "Show keyboard layout" Then I should not see "Show Wi-Fi control" Then I should not see "Enable audio controls" Then I should not see "Mute on startup" Then I should not see "Allow browser access to camera" Then I should not see "Allow browser access to microphone" Then I should not see "Enable spell checking" Then I should not see "Enable URL filtering" Then I should not see "Filter also embedded content" Then I should not see "Expressions allowed" Then I should not see "Regex allowed" Then I should not see "Expressions blocked" Then I should not see "Regex blocked" Scenario: SEB settings if using Use SEB client config Given the following "quizaccess_seb > seb templates" exist: | name | | Template 1 | And I am on the "Quiz 1" "quiz activity editing" page logged in as admin And I expand all fieldsets And I set the field "Require the use of Safe Exam Browser" to "Yes – Use SEB client config" Then I should see "Show Safe Exam Browser download button" Then I should see "Allowed browser exam keys" Then I should not see "Upload Safe Exam Browser config file" Then I should not see "Safe Exam Browser config template" Then I should not see "Template 1" Then I should not see "Enable quitting of SEB" Then I should not see "Quit password" Then I should not see "Show Exit Safe Exam Browser button, configured with this quit link" Then I should not see "Ask user to confirm quitting" Then I should not see "Enable reload in exam" Then I should not see "Show SEB task bar" Then I should not see "Show reload button" Then I should not see "Show time" Then I should not see "Show keyboard layout" Then I should not see "Show Wi-Fi control" Then I should not see "Enable audio controls" Then I should not see "Mute on startup" Then I should not see "Allow browser access to camera" Then I should not see "Allow browser access to microphone" Then I should not see "Enable spell checking" Then I should not see "Enable URL filtering" Then I should not see "Filter also embedded content" Then I should not see "Expressions allowed" Then I should not see "Regex allowed" Then I should not see "Expressions blocked" Then I should not see "Regex blocked" Scenario: SEB settings if using Upload my own config Given the following "quizaccess_seb > seb templates" exist: | name | | Template 1 | And I am on the "Quiz 1" "quiz activity editing" page logged in as admin And I expand all fieldsets And I set the field "Require the use of Safe Exam Browser" to "Yes – Upload my own config" Then I should see "Upload Safe Exam Browser config file" Then I should see "Show Safe Exam Browser download button" Then I should not see "Enable quitting of SEB" Then I should not see "Quit password" Then I should see "Allowed browser exam keys" Then I should not see "Show Exit Safe Exam Browser button, configured with this quit link" Then I should not see "Ask user to confirm quitting" Then I should not see "Enable reload in exam" Then I should not see "Show SEB task bar" Then I should not see "Show reload button" Then I should not see "Show time" Then I should not see "Show keyboard layout" Then I should not see "Show Wi-Fi control" Then I should not see "Enable audio controls" Then I should not see "Mute on startup" Then I should not see "Allow browser access to camera" Then I should not see "Allow browser access to microphone" Then I should not see "Enable spell checking" Then I should not see "Enable URL filtering" Then I should not see "Filter also embedded content" Then I should not see "Expressions allowed" Then I should not see "Regex allowed" Then I should not see "Expressions blocked" Then I should not see "Regex blocked" Then I should not see "Safe Exam Browser config template" Then I should not see "Template 1" Scenario: SEB settings if using Use an existing template Given the following "quizaccess_seb > seb templates" exist: | name | | Template 1 | And I am on the "Quiz 1" "quiz activity editing" page logged in as admin And I expand all fieldsets And I set the field "Require the use of Safe Exam Browser" to "Yes – Use an existing template" Then I should see "Safe Exam Browser config template" Then I should see "Template 1" Then I should see "Show Safe Exam Browser download button" Then I should see "Enable quitting of SEB" Then I should see "Quit password" Then I should not see "Allowed browser exam keys" Then I should not see "Upload Safe Exam Browser config file" Then I should not see "Show Exit Safe Exam Browser button, configured with this quit link" Then I should not see "Ask user to confirm quitting" Then I should not see "Enable reload in exam" Then I should not see "Show SEB task bar" Then I should not see "Show reload button" Then I should not see "Show time" Then I should not see "Show keyboard layout" Then I should not see "Show Wi-Fi control" Then I should not see "Enable audio controls" Then I should not see "Mute on startup" Then I should not see "Allow browser access to camera" Then I should not see "Allow browser access to microphone" Then I should not see "Enable spell checking" Then I should not see "Enable URL filtering" Then I should not see "Filter also embedded content" Then I should not see "Expressions allowed" Then I should not see "Regex allowed" Then I should not see "Expressions blocked" Then I should not see "Regex blocked" And I set the field "Enable quitting of SEB" to "No" Then I should not see "Quit password" Scenario: SEB settings if using Configure manually Given the following "quizaccess_seb > seb templates" exist: | name | | Template 1 | And I am on the "Quiz 1" "quiz activity editing" page logged in as admin And I expand all fieldsets And I set the field "Require the use of Safe Exam Browser" to "Yes – Configure manually" Then I should see "Show Safe Exam Browser download button" Then I should see "Enable quitting of SEB" Then I should see "Quit password" Then I should see "Show Exit Safe Exam Browser button, configured with this quit link" Then I should see "Ask user to confirm quitting" Then I should see "Enable reload in exam" Then I should see "Show SEB task bar" Then I should see "Show reload button" Then I should see "Show time" Then I should see "Show keyboard layout" Then I should see "Show Wi-Fi control" Then I should see "Enable audio controls" Then I should not see "Mute on startup" Then I should see "Allow browser access to camera" Then I should see "Allow browser access to microphone" Then I should see "Enable spell checking" Then I should see "Enable URL filtering" Then I should not see "Filter also embedded content" Then I should not see "Expressions allowed" Then I should not see "Regex allowed" Then I should not see "Expressions blocked" Then I should not see "Regex blocked" And I set the field "Enable quitting of SEB" to "No" Then I should not see "Quit password" And I set the field "Show SEB task bar" to "No" Then I should not see "Show reload button" Then I should not see "Show time" Then I should not see "Show keyboard layout" Then I should not see "Show Wi-Fi control" And I set the field "Enable audio controls" to "Yes" Then I should see "Mute on startup" And I set the field "Enable URL filtering" to "Yes" Then I should see "Filter also embedded content" Then I should see "Expressions allowed" Then I should see "Regex allowed" Then I should see "Expressions blocked" Then I should see "Regex blocked" Then I should not see "Upload Safe Exam Browser config file" Then I should not see "Allowed browser exam keys" Then I should not see "Safe Exam Browser config template" Then I should not see "Template 1" Scenario: Disable templates that are already in use and seeing their visibility in settings Given the following "quizaccess_seb > seb templates" exist: | name | enabled | | Template 1 | 1 | | Template 2 | 0 | # Set Quiz 1 template to Template 1 When I am on the "Quiz 1" "quiz activity editing" page logged in as admin And I expand all fieldsets And I set the field "Require the use of Safe Exam Browser" to "Yes – Use an existing template" Then I should see "Safe Exam Browser config template" And the "Safe Exam Browser config template" select box should contain "Template 1" And the "Safe Exam Browser config template" select box should not contain "Template 2" And I set the field "Safe Exam Browser config template" to "Template 1" And I press "Save and return to course" # Disable Template 1 And I navigate to "Plugins > Activity modules > Quiz > Safe Exam Browser templates" in site administration And I click on "Edit" "link" in the "Template 1" "table_row" And I set the field "Enabled" to "No" And I press "Save changes" # Check Quiz 1 is still using Template 1 When I am on the "Quiz 1" "quiz activity editing" page logged in as admin And I expand all fieldsets And the field "Require the use of Safe Exam Browser" matches value "Yes – Use an existing template" Then I should see "Template 1" And the "Safe Exam Browser config template" select box should contain "Template 1" And the "Safe Exam Browser config template" select box should not contain "Template 2" # Check Quiz 3 cannot use any templates as they're all disabled When I am on the "Quiz 2" "quiz activity editing" page logged in as admin And I expand all fieldsets And the "Require the use of Safe Exam Browser" select box should not contain "Yes – Use an existing template" PK\WA tests/behat/attempt_quiz.featurenu[@javascript @mod_quiz @quizaccess @quizaccess_seb Feature: Quiz start with Safe Exam Browser access rule Background: Given the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | course | section | name | | quiz | C1 | 1 | Quiz 1 | And the following "question categories" exist: | contextlevel | reference | name | | Course | C1 | Test questions | And the following "questions" exist: | questioncategory | qtype | name | questiontext | | Test questions | truefalse | TF1 | First question | And quiz "Quiz 1" contains the following questions: | question | page | maxmark | | TF1 | 1 | | Scenario: Start a quiz as student with Safe Exam Browser quiz access rule When I am on the "Quiz 1" "quiz activity editing" page logged in as teacher1 And I expand all fieldsets And I set the following fields to these values: | seb_requiresafeexambrowser | 1 | And I press "Save and return to course" And I am on the "Quiz 1" "mod_quiz > View" page logged in as "student1" Then the "target" attribute of "//a[text()='Download Safe Exam Browser']" "xpath_element" should contain "_blank" PK\Utests/hideif_rule_test.phpnu[. namespace quizaccess_seb; /** * PHPUnit tests for hideif_rule. * * @package quizaccess_seb * @author Dmitrii Metelkin * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class hideif_rule_test extends \advanced_testcase { /** * Test that can get rule data. */ public function test_can_get_what_set_in_constructor(): void { $rule = new hideif_rule('Element', 'Dependant', 'eq', 'Value'); $this->assertEquals('Element', $rule->get_element()); $this->assertEquals('Dependant', $rule->get_dependantname()); $this->assertEquals('eq', $rule->get_condition()); $this->assertEquals('Value', $rule->get_dependantvalue()); } } PK\=M)M)tests/test_helper_trait.phpnu[. /** * A test helper trait. * * @package quizaccess_seb * @author Andrew Madden * @copyright 2019 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use mod_quiz\local\access_rule_base; use mod_quiz\quiz_attempt; use quizaccess_seb\seb_access_manager; use quizaccess_seb\settings_provider; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . "/mod/quiz/accessrule/seb/rule.php"); // Include plugin rule class. require_once($CFG->dirroot . "/mod/quiz/mod_form.php"); // Include plugin rule class. /** * A test helper trait. It has some common helper methods. * * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ trait quizaccess_seb_test_helper_trait { /** @var \stdClass $course Test course to contain quiz. */ protected $course; /** @var \stdClass $quiz A test quiz. */ protected $quiz; /** @var \stdClass $user A test logged-in user. */ protected $user; /** * Assign a capability to $USER * The function creates a student $USER if $USER->id is empty * * @param string $capability Capability name. * @param int $contextid Context ID. * @param int $roleid Role ID. * @return int The role id - mainly returned for creation, so calling function can reuse it. */ protected function assign_user_capability($capability, $contextid, $roleid = null) { global $USER; // Create a new student $USER if $USER doesn't exist. if (empty($USER->id)) { $user = $this->getDataGenerator()->create_user(); $this->setUser($user); } if (empty($roleid)) { $roleid = \create_role('Dummy role', 'dummyrole', 'dummy role description'); } \assign_capability($capability, CAP_ALLOW, $roleid, $contextid); \role_assign($roleid, $USER->id, $contextid); \accesslib_clear_all_caches_for_unit_testing(); return $roleid; } /** * Strip the seb_ prefix from each setting key. * * @param \stdClass $settings Object containing settings. * @return \stdClass The modified settings object. */ protected function strip_all_prefixes(\stdClass $settings): \stdClass { $newsettings = new \stdClass(); foreach ($settings as $name => $setting) { $newname = preg_replace("/^seb_/", "", $name); $newsettings->$newname = $setting; // Add new key. } return $newsettings; } /** * Creates a file in the user draft area. * * @param string $xml * @return int The user draftarea id */ protected function create_test_draftarea_file(string $xml): int { global $USER; $itemid = 0; $usercontext = \context_user::instance($USER->id); $filerecord = [ 'contextid' => \context_user::instance($USER->id)->id, 'component' => 'user', 'filearea' => 'draft', 'itemid' => $itemid, 'filepath' => '/', 'filename' => 'test.xml' ]; $fs = get_file_storage(); $fs->create_file_from_string($filerecord, $xml); $draftitemid = 0; file_prepare_draft_area($draftitemid, $usercontext->id, 'user', 'draft', 0); return $draftitemid; } /** * Create a file in a modules filearea. * * @param string $xml XML content of the file. * @param string $cmid Course module id. * @return int Item ID of file. */ protected function create_module_test_file(string $xml, string $cmid): int { $itemid = 0; $fs = get_file_storage(); $filerecord = [ 'contextid' => \context_module::instance($cmid)->id, 'component' => 'quizaccess_seb', 'filearea' => 'filemanager_sebconfigfile', 'itemid' => $itemid, 'filepath' => '/', 'filename' => 'test.xml' ]; $fs->create_file_from_string($filerecord, $xml); return $itemid; } /** * Create a test quiz for the specified course. * * @param \stdClass $course * @param int $requiresafeexambrowser How to use SEB for this quiz? * @return array */ protected function create_test_quiz($course, $requiresafeexambrowser = settings_provider::USE_SEB_NO) { $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); $quiz = $quizgenerator->create_instance([ 'course' => $course->id, 'questionsperpage' => 0, 'grade' => 100.0, 'sumgrades' => 2, 'seb_requiresafeexambrowser' => $requiresafeexambrowser, ]); $quiz->seb_showsebdownloadlink = 1; $quiz->coursemodule = $quiz->cmid; // Create a question bank. $qbank = self::getDataGenerator()->create_module('qbank', ['course' => $course->id]); $qbankcontext = context_module::instance($qbank->cmid); // Create a couple of questions. $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat = $questiongenerator->create_question_category(['contextid' => $qbankcontext->id]); $saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]); quiz_add_quiz_question($saq->id, $quiz); $numq = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]); quiz_add_quiz_question($numq->id, $quiz); return $quiz; } /** * Answer questions for a quiz + user. * * @param \stdClass $quiz Quiz to attempt. * @param \stdClass $user A user to attempt the quiz. * @return array */ protected function attempt_quiz($quiz, $user) { $this->setUser($user); $starttime = time(); $quizobj = mod_quiz\quiz_settings::create($quiz->id, $user->id); $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); // Start the attempt. $attempt = quiz_create_attempt($quizobj, 1, false, $starttime, false, $user->id); quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $starttime); quiz_attempt_save_started($quizobj, $quba, $attempt); // Answer the questions. $attemptobj = quiz_attempt::create($attempt->id); $tosubmit = [ 1 => ['answer' => 'frog'], 2 => ['answer' => '3.14'], ]; $attemptobj->process_submitted_actions($starttime, false, $tosubmit); // Finish the attempt. $attemptobj = quiz_attempt::create($attempt->id); $attemptobj->process_submit($starttime, false); $attemptobj->process_grade_submission($starttime); $this->setUser(); return [$quizobj, $quba, $attemptobj]; } /** * Create test template. * * @param string|null $xml Template content. * @return \quizaccess_seb\template Just created template. */ public function create_template(?string $xml = null) { $data = []; if (!is_null($xml)) { $data['content'] = $xml; } return $this->getDataGenerator()->get_plugin_generator('quizaccess_seb')->create_template($data); } /** * Get access manager for testing. * * @return \quizaccess_seb\seb_access_manager */ protected function get_access_manager() { return new seb_access_manager(new mod_quiz\quiz_settings($this->quiz, get_coursemodule_from_id('quiz', $this->quiz->cmid), $this->course)); } /** * A helper method to make the rule form the currently created quiz and course. * * @return access_rule_base|null */ protected function make_rule() { return \quizaccess_seb::make( new mod_quiz\quiz_settings($this->quiz, get_coursemodule_from_id('quiz', $this->quiz->cmid), $this->course), 0, true ); } /** * A helper method to set up quiz view page. */ protected function set_up_quiz_view_page() { global $PAGE; $page = new \moodle_page(); $page->set_context(\context_module::instance($this->quiz->cmid)); $page->set_course($this->course); $page->set_pagelayout('standard'); $page->set_pagetype("mod-quiz-view"); $page->set_url('/mod/quiz/view.php?id=' . $this->quiz->cmid); $PAGE = $page; } /** * Get a test object containing mock test settings. * * @return \stdClass Settings. */ protected function get_test_settings(array $settings = []): \stdClass { return (object) array_merge([ 'quizid' => 1, 'cmid' => 1, 'requiresafeexambrowser' => '1', 'showsebtaskbar' => '1', 'showwificontrol' => '0', 'showreloadbutton' => '1', 'showtime' => '0', 'showkeyboardlayout' => '1', 'allowuserquitseb' => '1', 'quitpassword' => 'test', 'linkquitseb' => '', 'userconfirmquit' => '1', 'enableaudiocontrol' => '1', 'muteonstartup' => '0', 'allowcapturecamera' => '1', 'allowcapturemicrophone' => '1', 'allowspellchecking' => '0', 'allowreloadinexam' => '1', 'activateurlfiltering' => '1', 'filterembeddedcontent' => '0', 'expressionsallowed' => 'test.com', 'regexallowed' => '', 'expressionsblocked' => '', 'regexblocked' => '', 'showsebdownloadlink' => '1', ], $settings); } } PK\`3K޺ template.phpnu[. /** * Page to manipulate templates. * * @package quizaccess_seb * @author Dmitrii Metelkin * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../../../../config.php'); require_once($CFG->libdir . '/adminlib.php'); $action = optional_param('action', 'view', PARAM_ALPHANUMEXT); $PAGE->set_context(context_system::instance()); $manager = new \quizaccess_seb\template_controller(); $manager->execute($action); PK\)lpT T settings.phpnu[. /** * Global configuration settings for the quizaccess_seb plugin. * * @package quizaccess_seb * @author Andrew Madden * @author Dmitrii Metelkin * @copyright 2019 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; global $ADMIN; if ($hassiteconfig) { $settings->add(new admin_setting_heading( 'quizaccess_seb/supportedversions', '', $OUTPUT->notification(get_string('setting:supportedversions', 'quizaccess_seb'), 'warning'))); $settings->add(new admin_setting_configcheckbox('quizaccess_seb/autoreconfigureseb', get_string('setting:autoreconfigureseb', 'quizaccess_seb'), get_string('setting:autoreconfigureseb_desc', 'quizaccess_seb'), '1')); $links = [ 'seb' => get_string('setting:showseblink', 'quizaccess_seb'), 'http' => get_string('setting:showhttplink', 'quizaccess_seb') ]; $settings->add(new admin_setting_configmulticheckbox('quizaccess_seb/showseblinks', get_string('setting:showseblinks', 'quizaccess_seb'), get_string('setting:showseblinks_desc', 'quizaccess_seb'), $links, $links)); $settings->add(new admin_setting_configtext('quizaccess_seb/downloadlink', get_string('setting:downloadlink', 'quizaccess_seb'), get_string('setting:downloadlink_desc', 'quizaccess_seb'), 'https://safeexambrowser.org/download_en.html', PARAM_URL)); $settings->add(new admin_setting_configcheckbox('quizaccess_seb/quizpasswordrequired', get_string('setting:quizpasswordrequired', 'quizaccess_seb'), get_string('setting:quizpasswordrequired_desc', 'quizaccess_seb'), '0')); $settings->add(new admin_setting_configcheckbox('quizaccess_seb/displayblocksbeforestart', get_string('setting:displayblocksbeforestart', 'quizaccess_seb'), get_string('setting:displayblocksbeforestart_desc', 'quizaccess_seb'), '0')); $settings->add(new admin_setting_configcheckbox('quizaccess_seb/displayblockswhenfinished', get_string('setting:displayblockswhenfinished', 'quizaccess_seb'), get_string('setting:displayblockswhenfinished_desc', 'quizaccess_seb'), '1')); } if (has_capability('quizaccess/seb:managetemplates', context_system::instance())) { $ADMIN->add('modsettingsquizcat', new admin_externalpage( 'quizaccess_seb/template', get_string('manage_templates', 'quizaccess_seb'), new moodle_url('/mod/quiz/accessrule/seb/template.php'), 'quizaccess/seb:managetemplates' ) ); } PK\cq,db/services.phpnu[PK\YFXdb/install.phpnu[PK\X'"'" }db/access.phpnu[PK\Yg' ' 6db/upgrade.phpnu[PK\yәFBdb/install.xmlnu[PK\wo P [db/caches.phpnu[PK\MJJ=alang/en/quizaccess_seb.phpnu[PK\k9|backup/moodle2/restore_quizaccess_seb_subplugin.class.phpnu[PK\%Y'8wbackup/moodle2/backup_quizaccess_seb_subplugin.class.phpnu[PK\4 config.phpnu[PK\*#{M M plib.phpnu[PK\y+#gg version.phpnu[PK\{i'classes/external/validate_quiz_keys.phpnu[PK\|4..pclasses/privacy/provider.phpnu[PK\;u@,,q'classes/local/form/template.phpnu[PK\M%%6classes/local/table/template_list.phpnu[PK\aD+(+(Jclasses/template_controller.phpnu[PK\d qsclasses/hideif_rule.phpnu[PK\nb(WWclasses/helper.phpnu[PK\FTjcc#classes/link_generator.phpnu[PK\8VN N Йclasses/config_key.phpnu[PK\2D``dclasses/settings_provider.phpnu[PK\qU22Tclasses/seb_access_manager.phpnu[PK\"classes/event/template_created.phpnu[PK\/DJ#ӕclasses/event/template_disabled.phpnu[PK\ @"¤classes/event/template_enabled.phpnu[PK\"W "classes/event/template_deleted.phpnu[PK\UU"classes/event/access_prevented.phpnu[PK\{HΗ"classes/event/template_updated.phpnu[PK\aV5Z5Zmclasses/seb_quiz_settings.phpnu[PK\m@@=classes/property_list.phpnu[PK\ohYY~classes/template.phpnu[PK\2BFpkk[templates/loading.mustachenu[PK\7K amd/src/view.jsnu[PK\amd/src/validate_quiz_access.jsnu[PK\oN"amd/src/managetemplates.jsnu[PK\HD $amd/build/managetemplates.min.js.mapnu[PK\i)qamd/build/validate_quiz_access.min.js.mapnu[PK\h) amd/build/managetemplates.min.jsnu[PK\JUm amd/build/view.min.js.mapnu[PK\k^ ^ %amd/build/validate_quiz_access.min.jsnu[PK\! 7 7 amd/build/view.min.jsnu[PK\g2!]!]Nrule.phpnu[PK\O##^tests/helper_test.phpnu[PK\D+tests/fixtures/JSON_unencrypted_mac_001.txtnu[PK\ x{$tests/fixtures/simpleunencrypted.sebnu[PK\ؙp1717&Dtests/fixtures/unencrypted_mac_001.sebnu[PK\צ:= = tests/fixtures/encrypted.sebnu[PK\+OOTtests/fixtures/unencrypted.sebnu[PK\1!P!P&C7tests/fixtures/unencrypted_win_223.sebnu[PK\755tests/fixtures/simpleunencryptedwithoutoriginator.sebnu[PK\~22tests/backup_restore_test.phpnu[PK\y++оtests/rule_test.phpnu[PK\ ^^>tests/generator/lib.phpnu[PK\o\\2tests/generator/behat_quizaccess_seb_generator.phpnu[PK\np'p',tests/external/validate_quiz_access_test.phpnu[PK\9H##mtests/privacy/provider_test.phpnu[PK\5]5]_tests/access_manager_test.phpnu[PK\$$Stests/template_test.phpnu[PK\>>xtests/quiz_settings_test.phpnu[PK\$PP Z.tests/settings_provider_test.phpnu[PK\YdII tests/config_key_test.phpnu[PK\:s s / tests/link_generator_test.phpnu[PK\Kp*uPuPL= tests/property_list_test.phpnu[PK\էDw tests/event/events_test.phpnu[PK\E; Z tests/behat/seb_settings.featurenu[PK\2 tests/behat/secure_layout_activity_heading.featurenu[PK\277{ tests/behat/edit_form.featurenu[PK\WA  tests/behat/attempt_quiz.featurenu[PK\U tests/hideif_rule_test.phpnu[PK\=M)M) tests/test_helper_trait.phpnu[PK\`3K޺ / template.phpnu[PK\)lpT T u4 settings.phpnu[PKIIQB