ÿØÿà 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\Qr! styles.cssnu[@charset "UTF-8"; /* bigbluebuttonbn resources */ .recording-thumbnail { border: 1px solid #ddd; border-radius: 4px; padding: 0 0 0 0; transition: transform .2s; /* Animation */ width: 113px; height: 64px; } .recording-thumbnail:hover { box-shadow: 0 0 2px 1px rgba(0, 140, 186, 0.5); transform: scale(2.0); /* (200% zoom - Note: if the zoom is too large, it will go outside of the viewport) */ -moz-transform: scale(2.0); /* Firefox */ -webkit-transform: scale(2.0); /* Safari and Chrome */ -o-transform: scale(2.0); /* Opera */ position: relative; display: block; z-index: 999; } .fa-disabled { cursor: not-allowed; opacity: 0.2; } .fa-invisible { cursor: not-allowed; visibility: hidden; } .bbb_index_form { display: inline-block; } .bigbluebuttonbn_icon_btn i.icon { margin-right: auto; } /* Disable gray background for text input */ input.form-control[readonly][name="guestpassword"], input.form-control[readonly][name="guestjoinurl"] { background: initial; }PK\wZZ 11db/services.phpnu[. /** * URL external functions and service definitions. * * @package mod_bigbluebuttonbn * @category external * @copyright 2018 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) */ defined('MOODLE_INTERNAL') || die; $functions = [ 'mod_bigbluebuttonbn_can_join' => [ 'classname' => 'mod_bigbluebuttonbn\external\can_join', 'methodname' => 'execute', 'description' => 'Returns information if the current user can join or not.', 'type' => 'read', 'capabilities' => 'mod/bigbluebuttonbn:view', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], ], 'mod_bigbluebuttonbn_get_recordings' => [ 'classname' => 'mod_bigbluebuttonbn\external\get_recordings', 'methodname' => 'execute', 'description' => 'Returns a list of recordings ready to be processed by a datatable.', 'type' => 'read', 'ajax' => true, 'capabilities' => 'mod/bigbluebuttonbn:view', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], ], 'mod_bigbluebuttonbn_get_recordings_to_import' => [ 'classname' => 'mod_bigbluebuttonbn\external\get_recordings_to_import', 'methodname' => 'execute', 'description' => 'Returns a list of recordings ready to import to be processed by a datatable.', 'type' => 'read', 'ajax' => true, 'capabilities' => 'mod/bigbluebuttonbn:importrecordings', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], ], 'mod_bigbluebuttonbn_update_recording' => [ 'classname' => 'mod_bigbluebuttonbn\external\update_recording', 'methodname' => 'execute', 'description' => 'Update a single recording', 'type' => 'write', 'ajax' => true, 'capabilities' => 'mod/bigbluebuttonbn:managerecordings', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], ], 'mod_bigbluebuttonbn_end_meeting' => [ 'classname' => 'mod_bigbluebuttonbn\external\end_meeting', 'methodname' => 'execute', 'description' => 'End a meeting', 'type' => 'write', 'ajax' => true, 'capabilities' => 'mod/bigbluebuttonbn:join', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], ], 'mod_bigbluebuttonbn_completion_validate' => [ 'classname' => 'mod_bigbluebuttonbn\external\completion_validate', 'methodname' => 'execute', 'description' => 'Validate completion', 'type' => 'write', 'ajax' => true, 'capabilities' => 'mod/bigbluebuttonbn:view', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], ], 'mod_bigbluebuttonbn_meeting_info' => [ 'classname' => 'mod_bigbluebuttonbn\external\meeting_info', 'methodname' => 'execute', 'description' => 'Get displayable information on the meeting', 'type' => 'read', 'ajax' => true, 'capabilities' => 'mod/bigbluebuttonbn:view', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], ], 'mod_bigbluebuttonbn_get_bigbluebuttonbns_by_courses' => [ 'classname' => 'mod_bigbluebuttonbn\external\get_bigbluebuttonbns_by_courses', 'methodname' => 'execute', 'description' => 'Returns a list of bigbluebuttonbns in a provided list of courses, if no list is provided all bigbluebuttonbns that the user can view will be returned.', 'type' => 'read', 'capabilities' => 'mod/bigbluebuttonbn:view', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], ], 'mod_bigbluebuttonbn_view_bigbluebuttonbn' => [ 'classname' => 'mod_bigbluebuttonbn\external\view_bigbluebuttonbn', 'methodname' => 'execute', 'description' => 'Trigger the course module viewed event and update the module completion status.', 'type' => 'write', 'capabilities' => 'mod/bigbluebuttonbn:view', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE] ], 'mod_bigbluebuttonbn_get_join_url' => [ 'classname' => 'mod_bigbluebuttonbn\external\get_join_url', 'methodname' => 'execute', 'description' => 'Get the join URL for the meeting and create if it does not exist.', 'type' => 'write', 'capabilities' => 'mod/bigbluebuttonbn:join', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], ], ]; PK\~~db/subplugins.jsonnu[{ "subplugintypes": { "bbbext": "extension" }, "plugintypes": { "bbbext": "mod/bigbluebuttonbn/extension" } } PK\Ahzdb/install.phpnu[. /** * Install script for mod_bigbluebuttonbn. * * @package mod_bigbluebuttonbn * @copyright 2022 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Perform the post-install procedures. */ function xmldb_bigbluebuttonbn_install() { global $DB; // Disable the BigBlueButton activity module on new installs by default. $DB->set_field('modules', 'visible', 0, ['name' => 'bigbluebuttonbn']); } PK\.} db/log.phpnu[. /** * Definition of log events. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) */ defined('MOODLE_INTERNAL') || die(); global $DB; $logs = [ ['module' => 'bigbluebuttonbn', 'action' => 'add', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'], ['module' => 'bigbluebuttonbn', 'action' => 'update', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'], ['module' => 'bigbluebuttonbn', 'action' => 'view', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'], ['module' => 'bigbluebuttonbn', 'action' => 'view all', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'], ['module' => 'bigbluebuttonbn', 'action' => 'create', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'], ['module' => 'bigbluebuttonbn', 'action' => 'end', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'], ['module' => 'bigbluebuttonbn', 'action' => 'join', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'], ['module' => 'bigbluebuttonbn', 'action' => 'left', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'], ['module' => 'bigbluebuttonbn', 'action' => 'publish', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'], ['module' => 'bigbluebuttonbn', 'action' => 'unpublish', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'], ['module' => 'bigbluebuttonbn', 'action' => 'delete', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'], ]; PK\)Һ db/tasks.phpnu[. /** * Definition of scheduled tasks for mod_bigbluebuttonbn. * * @package mod_bigbluebuttonbn * @copyright 2021 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $tasks = [ [ 'classname' => 'mod_bigbluebuttonbn\task\check_pending_recordings', 'blocking' => 0, 'minute' => '*/5', 'hour' => '*', 'day' => '*', 'month' => '*', 'dayofweek' => '*' ], [ 'classname' => 'mod_bigbluebuttonbn\task\check_dismissed_recordings', 'blocking' => 0, 'minute' => '*', 'hour' => '*', 'day' => '*/10', // Every 10 days. 'month' => '*', 'dayofweek' => '*' ], ]; PK\Eqq db/access.phpnu[. /** * Capabilities for BigBlueButton. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) * @author Darko Miletic (darko.miletic@gmail.com) */ defined('MOODLE_INTERNAL') || die(); $capabilities = [ // Ability to add a new bigbluebuttonbn instance. 'mod/bigbluebuttonbn:addinstance' => [ 'riskbitmask' => RISK_XSS, 'captype' => 'write', 'contextlevel' => CONTEXT_COURSE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, ], 'clonepermissionsfrom' => 'moodle/course:manageactivities', ], // Ability to create instances with live meeting capabilities. 'mod/bigbluebuttonbn:addinstancewithmeeting' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, ], 'clonepermissionsfrom' => 'moodle/course:manageactivities', ], // Ability to create instances with recording capabilities. 'mod/bigbluebuttonbn:addinstancewithrecording' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, ], 'clonepermissionsfrom' => 'moodle/course:manageactivities', ], // Ability to join a meeting. 'mod/bigbluebuttonbn:join' => [ 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'student' => CAP_ALLOW, 'guest' => CAP_ALLOW, ], ], // Ability to access instances, regardless of the type. 'mod/bigbluebuttonbn:view' => [ 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'student' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ] ], // Ability to manage recordings. 'mod/bigbluebuttonbn:managerecordings' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, ], ], // Ability to view all recordings formats (if not user can only see the formats // defined in bigbluebuttonbn_recording_safe_formats). 'mod/bigbluebuttonbn:viewallrecordingformats' => [ 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, ], ], // Ability to publish recordings. 'mod/bigbluebuttonbn:publishrecordings' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, ], ], // Ability to unpublish recordings. 'mod/bigbluebuttonbn:unpublishrecordings' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, ], ], // Ability to protect recordings. 'mod/bigbluebuttonbn:protectrecordings' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, ], ], // Ability to unprotect recordings. 'mod/bigbluebuttonbn:unprotectrecordings' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, ], ], // Ability to delete recordings. 'mod/bigbluebuttonbn:deleterecordings' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, ], ], // Ability to import recordings. 'mod/bigbluebuttonbn:importrecordings' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, ], ], // Ability to always see presentation files. 'mod/bigbluebuttonbn:seepresentation' => [ 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, ], ], ]; PK\db/upgrade.phpnu[. /** * Upgrade logic. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) */ use mod_bigbluebuttonbn\plugin; use mod_bigbluebuttonbn\local\config; use mod_bigbluebuttonbn\task\upgrade_recordings_task; /** * Performs data migrations and updates on upgrade. * * @param int $oldversion * @return bool */ function xmldb_bigbluebuttonbn_upgrade($oldversion = 0) { 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. if ($oldversion < 2024071900) { // Define field showpresentation to be added to bigbluebuttonbn. $table = new xmldb_table('bigbluebuttonbn'); $field = new xmldb_field('showpresentation', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '1', 'guestpassword'); // Conditionally launch add field showpresentation. if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } // Bigbluebuttonbn savepoint reached. upgrade_mod_savepoint(true, 2024071900, 'bigbluebuttonbn'); } // Automatically generated Moodle v4.5.0 release upgrade line. // Put any upgrade step following this. if ($oldversion < 2025011000) { // Define field grade to be added to bigbluebuttonbn. $table = new xmldb_table('bigbluebuttonbn'); $field = new xmldb_field('grade', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'showpresentation'); // Conditionally launch add field grade. if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } // Bigbluebuttonbn savepoint reached. upgrade_mod_savepoint(true, 2025011000, 'bigbluebuttonbn'); } // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. return true; } /** * Generic helper function for adding or changing a field in a table. * * @param database_manager $dbman * @param string $tablename * @param string $fieldname * @param array $fielddefinition * @deprecated please do not use this anymore (historical migrations) */ function xmldb_bigbluebuttonbn_add_change_field( database_manager $dbman, string $tablename, string $fieldname, array $fielddefinition ) { $table = new xmldb_table($tablename); $field = new xmldb_field($fieldname); $field->set_attributes( $fielddefinition['type'], $fielddefinition['precision'], $fielddefinition['unsigned'], $fielddefinition['notnull'], $fielddefinition['sequence'], $fielddefinition['default'], $fielddefinition['previous'] ); if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field, true, true); return; } // Drop key before if needed. $fieldkey = new xmldb_key($fieldname, XMLDB_KEY_FOREIGN, [$fieldname], 'user', ['id']); if ($dbman->find_key_name($table, $fieldkey)) { $dbman->drop_key($table, $fieldkey); } $dbman->change_field_type($table, $field, true, true); $dbman->change_field_precision($table, $field, true, true); $dbman->change_field_notnull($table, $field, true, true); $dbman->change_field_default($table, $field, true, true); } /** * Generic helper function for adding index to a table. * * @param database_manager $dbman * @param string $tablename * @param string $indexname * @param array $indexfields * @param string|false|null $indextype * @deprecated please do not use this anymore (historical migrations) */ function xmldb_bigbluebuttonbn_index_table( database_manager $dbman, string $tablename, string $indexname, array $indexfields, $indextype = XMLDB_INDEX_NOTUNIQUE ) { $table = new xmldb_table($tablename); if (!$dbman->table_exists($table)) { return; } $index = new xmldb_index($indexname, $indextype, $indexfields); if ($dbman->index_exists($table, $index)) { $dbman->drop_index($table, $index); } $dbman->add_index($table, $index, true, true); } PK\[k#k#db/install.xmlnu[
PK\Sbdb/messages.phpnu[. /** * Defines message providers (types of messages being sent) * * @package mod_bigbluebuttonbn * @copyright 2021 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $messageproviders = [ // Recording ready notification posts. 'recording_ready' => [ 'defaults' => [ 'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, 'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, ], ], // The instance was created or updated. 'instance_updated' => [ 'defaults' => [ 'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, 'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, ], ], ]; PK\ X db/caches.phpnu[. /** * Caches for Bigbluebuttonbn * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent [at] call-learning [dt] fr) */ defined('MOODLE_INTERNAL') || die(); $definitions = [ // Server information // version (double) => server version. 'serverinfo' => [ 'mode' => cache_store::MODE_APPLICATION, 'invalidationevents' => [ 'mod_bigbluebuttonbn/serversettingschanged', ], ], // The validatedurls cache stores a list of URLs which are either valid, or invalid. // Keys are a URL // Values are an integer. 'validatedurls' => [ 'mode' => cache_store::MODE_APPLICATION, 'simpledata' => true, ], // The 'recordings' cache stores a cache of recording data. 'recordings' => [ 'mode' => cache_store::MODE_APPLICATION, 'invalidationevents' => [ 'mod_bigbluebuttonbn/recordingchanged', 'mod_bigbluebuttonbn/serversettingschanged', ], 'ttl' => 5 * MINSECS, ], 'currentfetch' => [ 'mode' => cache_store::MODE_REQUEST, ], // The 'subplugins' cache stores a cache of subplugins data to accelerate some of the subplugin discovery features. 'subplugins' => [ 'mode' => cache_store::MODE_APPLICATION, 'invalidationevents' => [ 'mod_bigbluebuttonbn/subpluginschanged', ], ], ]; PK\Zlang/en/bigbluebuttonbn.phpnu[. /** * Strings for component 'mod_bigbluebuttonbn', language 'en'. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) */ defined('MOODLE_INTERNAL') || die(); $string['activityoverview'] = 'You have upcoming BigBlueButton sessions'; $string['credentials_warning'] = 'The use of default server credentials will soon expire (see note above to obtain new credentials).'; $string['bbbduetimeoverstartingtime'] = 'The close time must be later than the open time.'; $string['bbbdurationwarning'] = 'The maximum duration for this session is %duration% minutes.'; $string['bbbrecordwarning'] = 'This session may be recorded.'; $string['bbbrecordallfromstartwarning'] = 'This session is being recorded from the start.'; $string['bigbluebuttonbn:addinstance'] = 'Add a new BigBlueButton room'; $string['bigbluebuttonbn:join'] = 'Join a BigBlueButton room'; $string['bigbluebuttonbn:view'] = 'View BigBlueButton room'; $string['bigbluebuttonbn:addinstancewithmeeting'] = 'Create live sessions instance'; $string['bigbluebuttonbn:addinstancewithrecording'] = 'Create instance with recordings'; $string['bigbluebuttonbn:managerecordings'] = 'Manage recordings'; $string['bigbluebuttonbn:publishrecordings'] = 'Publish recordings'; $string['bigbluebuttonbn:unpublishrecordings'] = 'Unpublish recordings'; $string['bigbluebuttonbn:protectrecordings'] = 'Protect recordings'; $string['bigbluebuttonbn:unprotectrecordings'] = 'Unprotect recordings'; $string['bigbluebuttonbn:deleterecordings'] = 'Delete recordings'; $string['bigbluebuttonbn:importrecordings'] = 'Import recordings'; $string['bigbluebuttonbn:seepresentation'] = 'Always see presentation file on activity page'; $string['bigbluebuttonbn:viewallrecordingformats'] = 'View all recording formats'; $string['bigbluebuttonbn'] = 'BigBlueButton'; $string['cannotperformaction'] = 'Cannot perform action {$a} on this recording'; $string['indicator:cognitivedepth'] = 'BigBlueButton cognitive'; $string['indicator:cognitivedepth_help'] = 'This indicator is based on the cognitive depth reached by the student in a BigBlueButton activity.'; $string['indicator:socialbreadth'] = 'BigBlueButton social'; $string['indicator:socialbreadth_help'] = 'This indicator is based on the social breadth reached by the student in a BigBlueButton activity.'; $string['modulename'] = 'BigBlueButton'; $string['modulenameplural'] = 'BigBlueButton'; $string['modulename_help'] = 'BigBlueButton is an open-source web conferencing system designed for online learning, which supports real-time sharing of audio, video, chat, slides, screen sharing, a multi-user whiteboard, breakout rooms, polling and emojis. Using BigBlueButton you can create a room for sessions such as live online classes, virtual office hours or group collaboration with remote students. The session can be recorded for later playback.'; $string['modulename_link'] = 'BigBlueButton/view'; $string['nosuchinstance'] = 'No such instance {$a->entity} with ID {$a->id}'; $string['pluginadministration'] = 'BigBlueButton administration'; $string['pluginname'] = 'BigBlueButton'; $string['removedevents'] = 'Deleted events'; $string['removedtags'] = 'Deleted tags'; $string['removedlogs'] = 'Deleted custom logs'; $string['removedrecordings'] = 'Deleted recordings'; $string['resetevents'] = 'Delete events'; $string['resettags'] = 'Delete tags'; $string['resetlogs'] = 'Delete custom logs'; $string['resetrecordings'] = 'Delete recordings'; $string['resetlogs_help'] = 'Deleting the logs will result in lost references to recordings.'; $string['resetrecordings_help'] = 'Deleting the recordings will make them inaccessible from anywhere. This action cannot be undone!'; $string['search:activity'] = 'BigBlueButton - activity information'; $string['search:tags'] = 'BigBlueButton - tags information'; $string['settings'] = 'BigBlueButton settings'; $string['settings_credential_warning_no_capability'] = 'The BigBlueButton server credentials will soon expire. Please contact your site administrator.'; $string['settings_credential_warning'] = 'The BigBlueButton server credentials will soon expire. See the BigBlueButton general settings (opens in a new window).'; $string['privacy:metadata:bigbluebuttonbn'] = 'BigBlueButton session configuration'; $string['privacy:metadata:bigbluebuttonbn:participants'] = 'A list of rules that define the role users will have in the BigBlueButton session. A user ID may be stored as permissions can be granted per role or per user.'; $string['privacy:metadata:bigbluebuttonbn_logs'] = 'Stores events triggered when using the plugin.'; $string['privacy:metadata:bigbluebuttonbn_logs:userid'] = 'The user ID of the user who triggered the event.'; $string['privacy:metadata:bigbluebuttonbn_logs:timecreated'] = 'The time when the log was created.'; $string['privacy:metadata:bigbluebuttonbn_logs:meetingid'] = 'The session ID the user had access to.'; $string['privacy:metadata:bigbluebuttonbn_logs:log'] = 'The type of event triggered by the user.'; $string['privacy:metadata:bigbluebuttonbn_logs:meta'] = 'Additional information related to the session or the recording.'; $string['privacy:metadata:bigbluebutton'] = 'In order to create and join BigBlueButton sessions, user data needs to be exchanged with the server.'; $string['privacy:metadata:bigbluebutton:userid'] = 'The user ID of the user accessing the BigBlueButton server.'; $string['privacy:metadata:bigbluebutton:fullname'] = 'The full name of the user accessing the BigBlueButton server.'; $string['privacy:metadata:bigbluebuttonbn_recordings'] = 'Stores metadata about recordings.'; $string['privacy:metadata:bigbluebuttonbn_recordings:userid'] = 'The user ID of the user who last changed a recording.'; $string['completionattendance'] = 'Require attendance (minutes)'; $string['completionattendance_desc'] = 'Enter and remain in the room for at least {$a} minute(s).'; $string['completionattendance_event_desc'] = 'Student has entered the room and remained in the session for at least {$a} minute(s)'; $string['completionattendancegroup'] = 'Require attendance (minutes)'; $string['completionengagementchats'] = 'Chats'; $string['completionengagementchats_desc'] = 'Participate in {$a} chat(s).'; $string['completionengagementchats_event_desc'] = 'Has raised {$a} chat(s)'; $string['completionengagementtalks'] = 'Talk'; $string['completionengagementtalks_desc'] = 'Talk {$a} time(s)'; $string['completionengagementtalks_event_desc'] = 'Has raised {$a} talk(s)'; $string['completionengagementraisehand'] = 'Require raised hand'; $string['completionengagementraisehand_desc'] = 'Raise hand {$a} time(s).'; $string['completionengagementraisehand_event_desc'] = 'Has raised hand {$a} times'; $string['completionengagementpollvotes'] = 'Poll votes'; $string['completionengagementpollvotes_desc'] = 'Vote in polls {$a} time(s).'; $string['completionengagementpollvotes_event_desc'] = 'Has answered {$a} poll vote(s)'; $string['completionengagementemojis'] = 'Emojis'; $string['completionengagementemojis_desc'] = 'Change their emoji {$a} time(s).'; $string['completionengagementemojis_event_desc'] = 'Changed their emoji {$a} time(s)'; $string['completionengagement_desc'] = 'Engage in activities during the meeting.'; $string['completionengagementgroup'] = 'Require participation'; $string['completionupdatestate'] = 'Completion update state'; $string['completionvalidatestate'] = 'Validate completion'; $string['completionvalidatestatetriggered'] = 'Validate completion has been triggered.'; $string['completionview_event_desc'] = 'Has viewed the room.'; $string['sendnotification'] = 'Send notification'; $string['minute'] = 'minute'; $string['minutes'] = 'minutes'; $string['config_guestaccess_enabled'] = 'External guest access'; $string['config_guestaccess_enabled_description'] = 'Allow users without an account on your site to access the room.'; $string['config_general'] = 'General settings'; $string['config_general_description'] = 'To set up BigBlueButton, you can either use your own BigBlueButton server and credentials, or obtain credentials through the Blindside Networks Registration Portal (opens in a new window).'; $string['config_profile_picture_enabled'] = 'Show profile pictures'; $string['config_profile_picture_enabled_description'] = 'Should profile pictures of participants be shown in BigBlueButton sessions?'; $string['config_server_url'] = 'BigBlueButton server URL'; $string['config_server_url_description'] = 'The URL of your BigBlueButton server.'; $string['config_shared_secret'] = 'BigBlueButton shared secret'; $string['config_shared_secret_description'] = 'The security secret of your BigBlueButton server.'; $string['config_checksum_algorithm'] = 'BigBlueButton server checksum algorithm'; $string['config_checksum_algorithm_description'] = 'SHA1 is compatible with older servers. SHA256 and SHA512 are more secure. SHA512 is FIPS 140-2 compliant.'; $string['config_recording'] = 'Recording'; $string['config_recording_description'] = 'These settings are feature specific'; $string['config_recording_default'] = 'Recording enabled by default'; $string['config_recording_default_description'] = 'Should the setting \'Session can be recorded\' be enabled by default when adding a new BigBlueButton room?'; $string['config_recording_editable'] = 'Session can be recorded editable'; $string['config_recording_editable_description'] = 'Should \'Session can be recorded\' be editable in the BigBlueButton activity settings?'; $string['config_recording_protect_editable'] = 'Protected recordings state can be edited'; $string['config_recording_protect_editable_description'] = 'If checked the interface includes an option for protecting/unprotecting recordings.'; $string['config_recording_all_from_start_default'] = 'Start recording from the beginning'; $string['config_recording_all_from_start_default_description'] = 'Should the setting \'Start recording from the beginning\' be enabled by default when adding a new BigBlueButton room?'; $string['config_recording_all_from_start_editable'] = 'Start recording from the beginning editable'; $string['config_recording_all_from_start_editable_description'] = 'Should \'Start recording from the beginning\' be editable in the BigBlueButton activity settings?'; $string['config_recording_hide_button_default'] = 'Hide recording button'; $string['config_recording_hide_button_default_description'] = 'If checked the button for record will be hidden'; $string['config_recording_hide_button_editable'] = 'Hide recording button editable'; $string['config_recording_hide_button_editable_description'] = 'Should \'Hide recording button\' be editable in the BigBlueButton activity settings?'; $string['config_recording_refresh_period'] = 'Recording refresh period (in seconds)'; $string['config_recording_refresh_period_description'] = 'How often should the BigBlueButton server be queried to refresh remote information for a recording?'; $string['config_recordings'] = 'Show recordings'; $string['config_recordings_description'] = 'These settings are feature specific'; $string['config_recordings_general'] = 'Show recording settings'; $string['config_recordings_general_description'] = 'These settings are used only when showing recordings'; $string['config_recordings_deleted_default'] = 'Include recordings from deleted activities enabled by default'; $string['config_recordings_deleted_default_description'] = 'If enabled, the recording table will include recordings of any activities which have since been deleted.'; $string['config_recordings_deleted_editable'] = 'Include recordings from deleted activities feature can be edited'; $string['config_recordings_deleted_editable_description'] = 'Include recordings from deleted activities by default can be edited when the instance is added or updated.'; $string['config_recordings_imported_default'] = 'Show only imported links enabled by default'; $string['config_recordings_imported_default_description'] = 'If enabled the recording table will include only the imported links to recordings.'; $string['config_recordings_imported_editable'] = 'Show only imported links feature can be edited'; $string['config_recordings_imported_editable_description'] = 'Show only imported links by default can be edited when the instance is added or updated.'; $string['config_recordings_preview_default'] = 'Preview is enabled by default'; $string['config_recordings_preview_default_description'] = 'If enabled the table includes a preview of the presentation.'; $string['config_recordings_preview_editable'] = 'Preview feature can be edited'; $string['config_recordings_preview_editable_description'] = 'Preview feature can be edited when the instance is added or updated.'; $string['config_recordings_asc_sort'] = 'List recordings in chronological order'; $string['config_recordings_asc_sort_description'] = 'Recordings are ordered by date, either chronological or reverse chronological order (most recent recording first).'; $string['config_recording_safe_formats'] = 'Recording formats viewable by everyone'; $string['config_recording_safe_formats_description'] = 'Select the formats which are viewable by everyone. All other formats are only viewable by teachers and other users with the mod/bigbluebuttonbn:viewallrecordingformats and mod/bigbluebuttonbn:managerecordings capabilities.'; $string['config_importrecordings'] = 'Import recordings'; $string['config_importrecordings_description'] = 'These settings are feature specific.'; $string['config_importrecordings_enabled'] = 'Import recordings enabled'; $string['config_importrecordings_enabled_description'] = 'When this and the recording feature are enabled, it is possible to import recordings from different courses into an activity.'; $string['config_importrecordings_from_deleted_enabled'] = 'Import recordings from deleted activities enabled'; $string['config_importrecordings_from_deleted_enabled_description'] = 'When this and the import recording feature are enabled, it is possible to import recordings from activities that are no longer in the course.'; $string['config_waitformoderator'] = 'Wait for moderator'; $string['config_waitformoderator_description'] = 'These settings are feature specific'; $string['config_waitformoderator_default'] = 'Wait for moderator enabled by default'; $string['config_waitformoderator_default_description'] = 'Should the setting \'Wait for moderator\' be enabled by default when adding a new BigBlueButton room?'; $string['config_waitformoderator_editable'] = 'Wait for moderator editable'; $string['config_waitformoderator_editable_description'] = 'Should \'Wait for moderator\' be editable in the BigBlueButton activity settings?'; $string['config_waitformoderator_ping_interval'] = 'Wait for moderator ping (seconds)'; $string['config_waitformoderator_ping_interval_description'] = 'How often should the server be pinged to check if the moderator has entered the room?'; $string['config_waitformoderator_cache_ttl'] = 'Wait for moderator cache TTL (seconds)'; $string['config_waitformoderator_cache_ttl_description'] = 'To support a heavy load of clients this plugin makes use of a cache. This parameter defines the time the cache will be kept before the next request is sent to the BigBlueButton server.'; $string['config_voicebridge'] = 'Voice bridge'; $string['config_voicebridge_description'] = 'These settings enable or disable options in the UI and also define default values for these options.'; $string['config_voicebridge_editable'] = 'Conference voice bridge can be edited'; $string['config_voicebridge_editable_description'] = 'A conference voice bridge number can be permanently assigned to a room. When assigned, the number cannot be used for any other room.'; $string['config_preuploadpresentation'] = 'Presentation file'; $string['config_preuploadpresentation_description'] = 'These settings enable or disable options in the UI and also define default values for these options. The feature works only if the Moodle server is accessible to BigBlueButton.'; $string['config_preuploadpresentation_editable'] = 'Presentation file on activity page'; $string['config_preuploadpresentation_editable_description'] = 'Option in activity settings to upload presentation file to show on activity page.'; $string['config_presentation_default'] = 'Default presentation file'; $string['config_presentation_default_description'] = 'A file may be provided for use in all rooms.'; $string['config_showpresentation_default'] = 'Show presentation file on activity page'; $string['config_showpresentation_default_description'] = 'Should students have access to the presentation file before the session? This setting has no effect on users with the capability \'Always see presentation file on activity page\' .'; $string['config_showpresentation_editable'] = '\'Show presentation file on activity page\' option'; $string['config_showpresentation_editable_description'] = 'Option in activity settings for teachers to choose whether to show the presentation file to students.'; $string['config_participant'] = 'Participants'; $string['config_participant_description'] = 'These settings define the default role for participants.'; $string['config_participant_moderator_default'] = 'Moderator'; $string['config_participant_moderator_default_description'] = 'This rule is used by default when a new room is added.'; $string['config_poll_interval'] = 'Poll interval (in seconds) to refresh the room\'s status'; $string['config_poll_interval_description'] = 'The poll interval must be at least 2 seconds. If necessary, you can lower the load on the remote server by increasing the interval.'; $string['config_userlimit'] = 'User limit'; $string['config_userlimit_description'] = 'These settings enable or disable options in the UI and also define default values for these options.'; $string['config_userlimit_default'] = 'User limit enabled by default'; $string['config_userlimit_default_description'] = 'The number of users allowed in a session by default when a new room is added. Set to 0 to allow an unlimited number of users.'; $string['config_userlimit_editable'] = 'User limit feature can be edited'; $string['config_userlimit_editable_description'] = 'User limit value by default can be edited when the room or conference is added or updated.'; $string['config_scheduled'] = 'Scheduled sessions'; $string['config_scheduled_description'] = 'These settings define default behaviour for scheduled sessions.'; $string['config_scheduled_pre_opening'] = 'Accessible before opening time'; $string['config_scheduled_pre_opening_description'] = 'The time in minutes that the room is open for prior to the scheduled opening time.'; $string['config_extended_capabilities'] = 'Extended capabilities'; $string['config_extended_capabilities_description'] = 'Configuration for extended capabilities when the BigBlueButton server offers them.'; $string['config_uidelegation_enabled'] = 'UI delegation is enabled'; $string['config_uidelegation_enabled_description'] = 'These settings enable or disable the UI delegation to the BigBlueButton server.'; $string['config_recordingready_enabled'] = 'Send recording available notification'; $string['config_recordingready_enabled_description'] = 'If enabled, a notification will be sent when a recording link is available. This feature requires the script post_publish_recording_ready_callback to be enabled on the BigBlueButton server.'; $string['config_meetingevents_enabled'] = 'Register live sessions'; $string['config_meetingevents_enabled_description'] = 'If enabled, live sessions will be processed after the session ends. This feature is required for Activity completion and will only work if the BigBlueButton server is capable of processing post_events scripts.'; $string['config_warning_curl_not_installed'] = 'This feature requires the CURL extension for php installed and enabled. The settings will be accessible only if this condition is fulfilled.'; $string['config_warning_bigbluebuttonbn_cfg_deprecated'] = 'BigBlueButton makes use of config.php with a global variable that has been deprecated. Please convert the file as it will not be supported in future versions.'; $string['config_muteonstart'] = 'Mute on start'; $string['config_muteonstart_description'] = 'These settings enable or disable options in the UI and also define default values for these options.'; $string['config_muteonstart_default'] = 'Mute on start enabled by default'; $string['config_muteonstart_default_description'] = 'If enabled the session will be muted on start.'; $string['config_muteonstart_editable'] = 'Mute on start can be edited'; $string['config_muteonstart_editable_description'] = 'Mute on start by default can be edited when the instance is added or updated.'; $string['config_welcome_default'] = 'Default welcome message'; $string['config_welcome_default_description'] = 'The welcome message is displayed when participants enter the room. If the field is left blank, the language string \'mod_form_field_welcome_default\' is used.'; $string['config_welcome_editable'] = 'Default welcome message is editable by teachers'; $string['config_welcome_editable_description'] = 'Welcome message can be edited when the instance is added or updated'; $string['config_default_messages'] = 'Default messages'; $string['config_default_messages_description'] = 'Set message defaults for activities'; $string['config_locksettings'] = 'Configuration for locking settings'; $string['config_locksettings_description'] = 'These settings enable or disable options in the UI for locking settings, and also define default values for these options.'; $string['config_disablecam_default'] = 'Disable webcam enabled by default'; $string['config_disablecam_default_description'] = 'If enabled the webcams will be disabled.'; $string['config_disablecam_editable'] = 'Disable webcam can be edited'; $string['config_disablecam_editable_description'] = 'Disable webcam by default can be edited when the instance is added or updated.'; $string['config_disablemic_default'] = 'Disable mic enabled by default'; $string['config_disablemic_default_description'] = 'If enabled the microphones will be disabled.'; $string['config_disablemic_editable'] = 'Disable mic can be edited'; $string['config_disablemic_editable_description'] = 'Disable mic by default can be edited when the instance is added or updated.'; $string['config_disableprivatechat_default'] = 'Disable private chat enabled by default'; $string['config_disableprivatechat_default_description'] = 'If enabled the private chat will be disabled.'; $string['config_disableprivatechat_editable'] = 'Disable private chat can be edited'; $string['config_disableprivatechat_editable_description'] = 'Disable private chat by default can be edited when the instance is added or updated.'; $string['config_disablepublicchat_default'] = 'Disable public chat enabled by default'; $string['config_disablepublicchat_default_description'] = 'If enabled the public chat will be disabled.'; $string['config_disablepublicchat_editable'] = 'Disable public chat can be edited'; $string['config_disablepublicchat_editable_description'] = 'Disable public chat by default can be edited when the instance is added or updated.'; $string['config_disablenote_default'] = 'Disable shared notes enabled by default'; $string['config_disablenote_default_description'] = 'If enabled the shared notes will be disabled.'; $string['config_disablenote_editable'] = 'Disable shared notes can be edited'; $string['config_disablenote_editable_description'] = 'Disable shared notes by default can be edited when the instance is added or updated.'; $string['config_hideuserlist_default'] = 'Hide user list enabled by default'; $string['config_hideuserlist_default_description'] = 'If enabled the session user list will be hidden.'; $string['config_hideuserlist_editable'] = 'Hide user list can be edited'; $string['config_hideuserlist_editable_description'] = 'Hide user list by default can be edited when the instance is added or updated.'; $string['config_experimental_features'] = 'Experimental features'; $string['config_experimental_features_description'] = 'Configuration for experimental features.'; $string['general_error_unable_connect'] = 'Unable to connect. Please check the url of the BigBlueButton server AND check to see if the BigBlueButton server is running. Details : {$a}'; $string['general_error_no_answer'] = 'Empty response. Please check the url of the BigBlueButton server AND check to see if the BigBlueButton server is running.'; $string['general_error_not_allowed_to_create_instances'] = 'User is not allowed to create any type of instance.'; $string['general_error_not_found'] = 'Cannot find the BigBlueButton activity with ID {$a}.'; $string['general_error_cannot_create_meeting'] = 'Cannot create session.'; $string['general_error_cannot_get_recordings'] = 'Cannot get recordings.'; $string['guestaccess_add'] = 'Add guests'; $string['guestaccess_add_no_id'] = 'No BigBlueButton instance ID provided.'; $string['guestaccess_emails'] = 'Guest emails'; $string['guestaccess_emails_help'] = 'A list of email addresses, separated by commas.'; $string['guestaccess_emails_invalidemail'] = 'Invalid email: {$a}'; $string['guestaccess_username'] = 'Guest username'; $string['guestaccess_meeting_link'] = 'Meeting link'; $string['guestaccess_meeting_password'] = 'Meeting password'; $string['guestaccess_password'] = 'Password'; $string['guestaccess_meeting_not_started'] = 'The meeting has not yet started. Please come back later.'; $string['guestaccess_meeting_invalid_password'] = 'Incorrect password.'; $string['guestaccess_activitynotfound'] = 'Activity not found.'; $string['guestaccess_feature_disabled'] = 'Guest access disabled.'; $string['guestaccess_title'] = 'Add guests to this meeting'; $string['guestaccess_copy_link'] = 'Copy link'; $string['guestaccess_copy_password'] = 'Copy password'; $string['guestaccess_invite_success'] = 'An invitation will be sent to {$a->emails}.'; $string['guestaccess_invite_failure'] = 'Error {$a->errors} when inviting {$a->emails}.'; $string['guestaccess_join_meeting'] = 'Join meeting'; $string['guest_invitation_subject'] = 'Invitation: {$a->name} session in {$a->course_fullname}'; $string['guest_invitation_small_message'] = 'Hi, You are invited to a BigBlueButton session {$a->name} in the course {$a->course_fullname}. Link: {$a->guestjoinurl} Password: {$a->guestpassword} If you need help, please contact {$a->sender}.'; $string['guest_invitation_full_message'] = 'Hi,

You are invited to a BigBlueButton session {$a->name} in the course {$a->course_fullname}.

Link: {$a->guestjoinurl}

Password: {$a->guestpassword}

If you need help, please contact {$a->sender}.

'; $string['index_confirm_end'] = 'Do you want to end the session?'; $string['index_disabled'] = 'disabled'; $string['index_enabled'] = 'enabled'; $string['index_ending'] = 'Ending the session ... please wait'; $string['index_error_checksum'] = 'A checksum error occurred. Please check that you entered the correct secret.'; $string['index_error_forciblyended'] = 'Unable to join the session because it has been manually ended.'; $string['index_error_unable_display'] = 'Unable to display the sessions. Please check the url of the BigBlueButton server AND check to see if the BigBlueButton server is running.'; $string['index_heading_actions'] = 'Actions'; $string['index_heading_group'] = 'Group'; $string['index_heading_moderator'] = 'Moderators'; $string['index_heading_name'] = 'Room'; $string['index_heading_recording'] = 'Recording'; $string['index_heading_users'] = 'Users'; $string['index_heading_viewer'] = 'Viewers'; $string['index_heading'] = 'BigBlueButton rooms'; $string['instanceprofilewithoutrecordings'] = 'This instance profile cannot display recordings'; $string['managebbbextplugins'] = 'Manage BigBlueButton extension plugins'; $string['mod_form_block_general'] = 'General'; $string['mod_form_block_guestaccess'] = 'Guest access'; $string['mod_form_block_room'] = 'Room settings'; $string['mod_form_block_recordings'] = 'Recording view'; $string['mod_form_block_presentation'] = 'Presentation content'; $string['mod_form_block_presentation_default'] = 'Presentation default content'; $string['mod_form_block_participants'] = 'Role assigned during live session'; $string['mod_form_block_schedule'] = 'Session timing'; $string['mod_form_block_record'] = 'Record settings'; $string['mod_form_field_openingtime'] = 'Open'; $string['mod_form_field_closingtime'] = 'Close'; $string['mod_form_field_intro'] = 'Description'; $string['mod_form_field_intro_help'] = 'A short description of the room.'; $string['mod_form_field_duration_help'] = 'Setting the duration for a meeting will establish the maximum time for a meeting to keep alive before the recording finish'; $string['mod_form_field_duration'] = 'Duration'; $string['mod_form_field_guestallowed'] = 'Allow guest access'; $string['mod_form_field_mustapproveuser'] = 'Guests joining must be admitted by a moderator'; $string['mod_form_field_userlimit'] = 'User limit'; $string['mod_form_field_userlimit_help'] = 'The maximum number of users allowed in a session. Set to 0 to allow an unlimited number of users.'; $string['mod_form_field_name'] = 'Room name'; $string['mod_form_field_room_name'] = 'Room name'; $string['mod_form_field_conference_name'] = 'Session name'; $string['mod_form_field_record'] = 'The session may be recorded.'; $string['mod_form_field_voicebridge'] = 'Voice bridge [####]'; $string['mod_form_field_voicebridge_help'] = 'A number between 1 and 9999 that participants enter to join the voice session when using dial-in. If the value is 0 then the static voice bridge number will be ignored and a random number will be generated by BigBlueButton. A number 7 will prefix the four digits typed.'; $string['mod_form_field_voicebridge_format_error'] = 'Format error. You should input a number between 1 and 9999.'; $string['mod_form_field_voicebridge_notunique_error'] = 'Not a unique value. This number is being used by another room.'; $string['mod_form_field_wait'] = 'Wait for moderator'; $string['mod_form_field_wait_help'] = 'Do participants have to wait for a moderator before they can enter the room?'; $string['mod_form_field_welcome'] = 'Welcome message'; $string['mod_form_field_welcome_help'] = 'The welcome message is displayed when participants enter the room. If the field is left blank, then a default message set in the site administration is displayed.'; $string['mod_form_field_welcome_default'] = 'Welcome to %%CONFNAME%%.'; $string['mod_form_field_participant_add'] = 'Add assignee'; $string['mod_form_field_participant_list'] = 'Assignee'; $string['mod_form_field_participant_list_type_all'] = 'All users enrolled'; $string['mod_form_field_participant_list_type_role'] = 'Role'; $string['mod_form_field_participant_list_type_user'] = 'User'; $string['mod_form_field_participant_list_type_owner'] = 'Owner'; $string['mod_form_field_participant_list_text_as'] = 'joins session as'; $string['mod_form_field_participant_list_action_add'] = 'Add'; $string['mod_form_field_participant_list_action_remove'] = 'Remove'; $string['mod_form_field_participant_bbb_role_moderator'] = 'Moderator'; $string['mod_form_field_participant_bbb_role_viewer'] = 'Viewer'; $string['mod_form_field_showpresentation'] = 'Show presentation file on activity page'; $string['mod_form_field_instanceprofiles'] = 'Instance type'; $string['mod_form_field_instanceprofiles_help'] = 'If a session is to be recorded, select \'Room with recordings\', otherwise \'Room only\'. After a session is recorded, if there are to be no more sessions, select \'Recordings only\'.'; $string['mod_form_field_muteonstart'] = 'Mute on start'; $string['mod_form_field_recordings_html'] = 'Show the table in plain HTML'; $string['mod_form_field_recordings_deleted'] = 'Include recordings from deleted activities'; $string['mod_form_field_recordings_imported'] = 'Show only imported links'; $string['mod_form_field_recordings_preview'] = 'Show recording preview'; $string['mod_form_field_recordallfromstart'] = 'Record all from start'; $string['mod_form_field_recordhidebutton'] = 'Hide recording button'; $string['mod_form_field_nosettings'] = 'No settings can be edited'; $string['mod_form_field_disablecam'] = 'Disable webcams'; $string['mod_form_field_disablemic'] = 'Disable microphones'; $string['mod_form_field_disableprivatechat'] = 'Disable private chat'; $string['mod_form_field_disablepublicchat'] = 'Disable public chat'; $string['mod_form_field_disablenote'] = 'Disable shared notes'; $string['mod_form_field_hideuserlist'] = 'Hide user list'; $string['mod_form_locksettings'] = 'Lock settings'; $string['report_join_info'] = '{$a} meeting(s)'; $string['report_play_recording_info'] = '{$a} recording(s) played'; $string['report_room_view'] = 'viewed'; $string['progress_createbigbluebuttonbn'] = 'Creating BigBlueButton activity ({$a})'; $string['starts_at'] = 'Starts'; $string['started_at'] = 'Started'; $string['ends_at'] = 'Ends'; $string['calendarstarts'] = '{$a} is scheduled for'; $string['recordings_from_deleted_activities'] = 'Recordings from deleted activities'; $string['view_error_no_group_student'] = 'You have not been added to a group. Please contact your teacher.'; $string['view_error_no_group_teacher'] = 'There are no groups. You need to create some groups.'; $string['view_error_no_group'] = 'There are no groups. You need to create some groups before trying to join the session.'; $string['view_error_unable_join_student'] = 'Unable to connect to the BigBlueButton server.'; $string['view_error_unable_join_teacher'] = 'Unable to connect to the BigBlueButton server. Please contact an administrator.'; $string['view_error_unable_join'] = 'Unable to enter the room. Please check the URL of the BigBlueButton server AND check to see if the BigBlueButton server is running.'; $string['view_error_bigbluebutton'] = 'BigBlueButton responded with errors. {$a}'; $string['view_error_create'] = 'The BigBlueButton server responded with an error message. The room could not be created.'; $string['view_error_max_concurrent'] = 'The number of concurrent sessions allowed has been reached.'; $string['view_error_userlimit_reached'] = 'The limit of how many users allowed in a session has been reached.'; $string['view_error_url_missing_parameters'] = 'There are parameters missing in this URL'; $string['view_error_import_no_courses'] = 'There are no courses to look up for recordings.'; $string['view_error_import_no_recordings'] = 'There are no recordings in this course for importing.'; $string['view_error_invalid_session'] = 'The session has expired. Go back to the activity page.'; $string['view_groups_selection_join'] = 'Join'; $string['view_groups_selection'] = 'Select the group you want to join and confirm the action'; $string['view_login_moderator'] = 'Logging in as moderator ...'; $string['view_login_viewer'] = 'Logging in as viewer ...'; $string['view_noguests'] = 'The BigBlueButton room is not open to guests.'; $string['view_nojoin'] = 'You do not have a role that is allowed to join this session.'; $string['view_recording_list_actionbar_edit'] = 'Edit'; $string['view_recording_list_actionbar_delete'] = 'Delete'; $string['view_recording_list_actionbar_import'] = 'Import'; $string['view_recording_list_actionbar_hide'] = 'Hide'; $string['view_recording_list_actionbar_show'] = 'Show'; $string['view_recording_list_actionbar_publish'] = 'Publish'; $string['view_recording_list_actionbar_unpublish'] = 'Unpublish'; $string['view_recording_list_actionbar_protect'] = 'Make it private'; $string['view_recording_list_actionbar_unprotect'] = 'Make it public'; $string['view_recording_list_action_publish'] = 'Publishing'; $string['view_recording_list_action_unpublish'] = 'Unpublishing'; $string['view_recording_list_action_process'] = 'Processing'; $string['view_recording_list_action_delete'] = 'Deleting'; $string['view_recording_list_action_protect'] = 'Protecting'; $string['view_recording_list_action_unprotect'] = 'Unprotecting'; $string['view_recording_list_action_update'] = 'Updating'; $string['view_recording_list_action_edit'] = 'Updating'; $string['view_recording_list_action_play'] = 'Play'; $string['view_recording_list_actionbar'] = 'Toolbar'; $string['view_recording_list_activity'] = 'Activity'; $string['view_recording_list_course'] = 'Course'; $string['view_recording_list_date'] = 'Date'; $string['view_recording_list_description'] = 'Description'; $string['view_recording_list_duration'] = 'Duration'; $string['view_recording_list_recording'] = 'Recording'; $string['view_recording_button_import'] = 'Import recording links'; $string['view_recording_button_return'] = 'Go back'; $string['view_recording_format_notes'] = 'Notes'; $string['view_recording_format_podcast'] = 'Podcast'; $string['view_recording_format_presentation'] = 'Presentation'; $string['view_recording_format_screenshare'] = 'Screenshare'; $string['view_recording_format_statistics'] = 'Statistics'; $string['view_recording_format_video'] = 'Video'; $string['view_recording_format_errror_unreachable'] = 'The URL for this recording format is unreachable.'; $string['view_section_title_presentation'] = 'Presentation file'; $string['view_section_title_recordings'] = 'Recordings'; $string['view_message_norecordings'] = 'There are no recordings available.'; $string['view_message_finished'] = 'This activity is over.'; $string['view_message_notavailableyet'] = 'This session is not yet available.'; $string['view_recording_select_course'] = 'Select a course first in the drop down menu'; $string['view_message_session_started_at'] = 'Session started at'; $string['view_message_session_running_for'] = 'This session has been running for'; $string['view_message_hour'] = 'hour'; $string['view_message_hours'] = 'hours'; $string['view_message_minute'] = 'minute'; $string['view_message_minutes'] = 'minutes'; $string['view_message_moderator'] = 'Moderator'; $string['view_message_moderators'] = 'Moderators'; $string['view_message_viewer'] = 'Viewer'; $string['view_message_viewers'] = 'Viewers'; $string['view_message_user'] = 'User'; $string['view_message_users'] = 'Users'; $string['view_message_has_joined'] = 'has joined'; $string['view_message_have_joined'] = 'have joined'; $string['view_message_session_no_users'] = 'There are no users in this session'; $string['view_message_session_has_user'] = 'There is'; $string['view_message_session_has_users'] = 'There are'; $string['view_message_session_for'] = 'the session for'; $string['view_message_times'] = 'times'; $string['view_message_and'] = 'and'; $string['view_message_room_closed'] = 'This room is closed.'; $string['view_message_room_ready'] = 'This room is ready.'; $string['view_message_room_open'] = 'This room is open.'; $string['view_message_conference_room_ready'] = 'This room is ready. You can join the session now.'; $string['view_message_conference_not_started'] = 'The session has not started yet.'; $string['view_message_conference_wait_for_moderator'] = 'Waiting for a moderator to join.'; $string['view_message_conference_in_progress'] = 'The session is in progress.'; $string['view_message_conference_has_ended'] = 'The session has ended.'; $string['view_message_conference_user_limit_reached'] = 'The limit of how many users allowed in a session has been reached.'; $string['view_message_tab_close'] = 'This tab/window must be closed manually'; $string['view_message_recordings_disabled'] = 'Recordings are disabled on the server. BigBlueButton activities of type \'Recordings only\' cannot be used.'; $string['view_message_cron_disabled'] = 'The list of recordings may not be up to date. Please contact the site administrator with the following information: {$a}'; $string['view_message_importrecordings_disabled'] = 'Import recording links is disabled on the server.'; $string['view_groups_selection_warning'] = 'There is a room for each group and you have access to more than one. Be sure to select the correct one.'; $string['view_groups_nogroups_warning'] = 'The room was configured for using groups but the course does not have groups defined.'; $string['view_groups_notenrolled_warning'] = 'The room was configured for using groups but you are not a member of a group.'; $string['view_conference_action_join'] = 'Join session'; $string['view_conference_action_end'] = 'End session'; $string['view_recording'] = 'recording'; $string['view_recording_link'] = 'imported link'; $string['view_recording_link_warning'] = 'This is a link pointing to a recording that was created in a different course or activity.'; $string['view_recording_delete_confirmation'] = 'Are you sure you want to delete {$a}?'; $string['view_recording_delete_confirmation_warning_s'] = 'This recording has {$a} link associated that was imported into a different course or activity. If the recording is deleted, this link will also be removed.'; $string['view_recording_delete_confirmation_warning_p'] = 'This recording has {$a} links associated that were imported into different courses or activities. If the recording is deleted, these links will also be removed.'; $string['view_recording_publish_confirmation'] = 'Are you sure you want to publish this {$a}?'; $string['view_recording_publish_confirmation_warning_s'] = 'This recording has {$a} link associated that was imported into a different course or activity. If the recording is published, this link will also be published.'; $string['view_recording_publish_confirmation_warning_p'] = 'This recording has {$a} links associated that were imported into different courses or activities. If the recording is published, these links will also be published.'; $string['view_recording_publish_link_deleted'] = 'This link cannot be republished because the original recording does not exist on the BigBlueButton server. The link should be removed.'; $string['view_recording_publish_link_not_published'] = 'This link cannot be republished because the original recording is unpublished.'; $string['view_recording_unpublish_confirmation'] = 'Are you sure to unpublish this {$a}?'; $string['view_recording_unpublish_confirmation_warning_s'] = 'This recording has {$a} link associated that was imported into a different course or activity. If the recording is unpublished, this link will also be unpublished.'; $string['view_recording_unpublish_confirmation_warning_p'] = 'This recording has {$a} links associated that were imported into different courses or activities. If the recording is unpublished, these links will also be unpublished.'; $string['view_recording_protect_confirmation'] = 'Are you sure you want to protect this {$a}?'; $string['view_recording_protect_confirmation_warning_s'] = 'This recording has {$a} link associated that was imported into a different course or activity. If the recording is protected it will also affect the imported links.'; $string['view_recording_protect_confirmation_warning_p'] = 'This recording has {$a} links associated that were imported into different courses or activities. If the recording is protected it will also affect the imported links.'; $string['view_recording_unprotect_confirmation'] = 'Are you sure you want to unprotect this {$a}?'; $string['view_recording_unprotect_confirmation_warning_s'] = 'This recording has {$a} link associated that was imported into a different course or activity. If the recording is unprotected it will also affect the imported links.'; $string['view_recording_unprotect_confirmation_warning_p'] = 'This recording has {$a} links associated that were imported into different courses or activities. If the recording is unprotected it will also affect the imported links.'; $string['view_recording_import_confirmation'] = 'Are you sure you want to import this recording?'; $string['view_recording_unprotect_link_deleted'] = 'This link cannot be unprotected because the original recording does not exist on the BigBlueButton server. The link should be removed.'; $string['view_recording_unprotect_link_not_unprotected'] = 'This link cannot be unprotected because the original recording is protected.'; $string['view_recording_actionbar'] = 'Toolbar'; $string['view_recording_activity'] = 'Activity'; $string['view_recording_course'] = 'Course'; $string['view_recording_date'] = 'Date'; $string['view_recording_description'] = 'Description'; $string['view_recording_description_editlabel'] = 'Edit description'; $string['view_recording_description_edithint'] = 'A description may be added to help identify the recording.'; $string['view_recording_length'] = 'Length'; $string['view_recording_meeting'] = 'Meeting'; $string['view_recording_duration'] = 'Duration'; $string['view_recording_recording'] = 'Recording'; $string['view_recording_duration_min'] = 'min'; $string['view_recording_name'] = 'Name'; $string['view_recording_name_editlabel'] = 'Edit name'; $string['view_recording_name_edithint'] = 'A name may be added to help identify the recording.'; $string['view_recording_tags'] = 'Tags'; $string['view_recording_playback'] = 'Playback'; $string['view_recording_preview'] = 'Preview'; $string['view_recording_preview_help'] = 'Hover over an image to view it in full size'; $string['view_recording_modal_button'] = 'Apply'; $string['view_recording_modal_title'] = 'Set values for recording'; $string['view_recording_yui_first'] = 'First'; $string['view_recording_yui_prev'] = 'Previous'; $string['view_recording_yui_next'] = 'Next'; $string['view_recording_yui_last'] = 'Last'; $string['view_recording_yui_page'] = 'Page'; $string['view_recording_yui_go'] = 'Go'; $string['view_recording_yui_rows'] = 'Rows'; $string['view_recording_yui_show_all'] = 'Show all'; $string['event_activity_created'] = 'Activity created'; $string['event_activity_deleted'] = 'Activity deleted'; $string['event_activity_updated'] = 'Activity updated'; $string['event_meeting_created'] = 'Meeting created'; $string['event_meeting_ended'] = 'Meeting forcibly ended'; $string['event_meeting_joined'] = 'Meeting joined'; $string['event_meeting_left'] = 'Meeting left'; $string['event_recording_viewed'] = 'Recording viewed'; $string['event_recording_edited'] = 'Recording edited'; $string['event_recording_deleted'] = 'Recording deleted'; $string['event_recording_imported'] = 'Recording imported'; $string['event_recording_published'] = 'Recording published'; $string['event_recording_unpublished'] = 'Recording unpublished'; $string['event_recording_protected'] = 'Recording protected'; $string['event_recording_unprotected'] = 'Recording unprotected'; $string['event_live_session'] = 'Live session event'; $string['event_unknown'] = 'Unknown event'; $string['instance_type_default'] = 'Room with recordings'; $string['instance_type_room_only'] = 'Room only'; $string['instance_type_recording_only'] = 'Recordings only'; $string['messageprovider:instance_updated'] = 'BigBlueButton session updated'; $string['messageprovider:recording_ready'] = 'BigBlueButton recording available'; $string['new_bigblubuttonbn_activities'] = 'BigBlueButton activity'; $string['notification_instance_created_intro'] = 'The {$a->name} BigBlueButton room has been created.'; $string['notification_instance_created_small'] = 'A new BigBlueButton room named {$a->name} was created.'; $string['notification_instance_created_subject'] = 'A new BigBlueButton room has been created'; $string['notification_instance_description'] = 'Description'; $string['notification_instance_end_date'] = 'End date'; $string['notification_instance_name'] = 'Title'; $string['notification_instance_start_date'] = 'Start date'; $string['notification_instance_updated_intro'] = 'The {$a->name} BigBlueButton room has been updated.'; $string['notification_instance_updated_small'] = 'The {$a->name} BigBlueButton session was updated'; $string['notification_instance_updated_subject'] = 'Your BigBlueButton room has been updated'; $string['notification_recording_ready_small'] = 'A recording is available for the BigBlueButton room {$a->name}.'; $string['notification_recording_ready_html'] = 'A recording is now available for the session in the BigBlueButton room {$a->name}.'; $string['notification_recording_ready_plain'] = 'A recording is now available for the session in the BigBlueButton room {$a->name}. Go to {$a->link} to access the recording link.'; $string['notification_recording_ready_subject'] = 'Recording available'; $string['view_error_meeting_not_running'] = 'Something went wrong; the session is not running.'; $string['view_error_current_state_not_found'] = 'Current state was not found. The recording may have been deleted or the BigBlueButton server is not compatible with the action performed.'; $string['view_error_action_not_completed'] = 'Action could not be completed'; $string['view_room'] = 'View room'; $string['index_error_noinstances'] = 'There are no instances of BigBlueButton rooms'; $string['index_error_bbtn'] = 'BigBlueButton ID {$a} is incorrect'; $string['end_session_confirm_title'] = 'Really end session?'; $string['end_session_confirm'] = 'Are you sure you want to end the session?'; $string['end_session_notification'] = 'The session has ended.'; $string['cachedef_currentfetch'] = 'Data to list any recording fetched recently.'; $string['cachedef_serverinfo'] = 'Remote server information'; $string['cachedef_recordings'] = 'Recording metadata'; $string['cachedef_validatedurls'] = 'Cache of validated URL checks'; $string['cachedef_subplugins'] = 'Cache used by subplugin routines to accelerate when needed the plugin discovery process.'; $string['taskname:check_pending_recordings'] = 'Fetch pending recordings'; $string['taskname:check_dismissed_recordings'] = 'Check for recordings that haven\'t been found yet'; $string['userlimitreached'] = 'The number of users allowed in a session has been reached.'; $string['waitformoderator'] = 'Waiting for a moderator to join.'; $string['recordingnotfound'] = 'The recording was not found.'; $string['recordingurlnotfound'] = 'The recording URL is invalid.'; $string['subplugintype_bbbext'] = 'BigBlueButton activity extension'; $string['subplugintype_bbbext_plural'] = 'BigBlueButton activity extensions'; // Deprecated since Moodle 4.5. $string['view_mobile_message_reload_page_creation_time_meeting'] = 'You exceeded 45 seconds on this page. Please refresh the page to join the session.'; $string['view_mobile_message_groups_not_supported'] = 'This instance is enabled to work with groups but the mobile app doesn\'t yet support it. Please use the web version.'; PK\lang/en/deprecated.txtnu[view_mobile_message_reload_page_creation_time_meeting,mod_bigbluebuttonbn view_mobile_message_groups_not_supported,mod_bigbluebuttonbn PK\+YY2backup/moodle2/backup_bigbluebuttonbn_stepslib.phpnu[. /** * Class for the structure used for backup BigBlueButtonBN. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) */ /** * Define all the backup steps that will be used by the backup_bigbluebuttonbn_activity_task. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_bigbluebuttonbn_activity_structure_step extends backup_activity_structure_step { /** * Define the complete bigbluebuttonbn structure for backup, with file and id annotations. * * @return object */ protected function define_structure() { // To know if we are including userinfo. $userinfo = $this->get_setting_value('userinfo'); // Define each element separated. $bigbluebuttonbn = new backup_nested_element('bigbluebuttonbn', ['id'], [ 'type', 'course', 'name', 'intro', 'introformat', 'meetingid', 'moderatorpass', 'viewerpass', 'wait', 'record', 'recordallfromstart', 'recordhidebutton', 'welcome', 'voicebridge', 'openingtime', 'closingtime', 'timecreated', 'timemodified', 'presentation', 'participants', 'userlimit', 'recordings_html', 'recordings_deleted', 'recordings_imported', 'recordings_preview', 'clienttype', 'muteonstart', 'completionattendance', 'completionengagementchats', 'completionengagementtalks', 'completionengagementraisehand', 'completionengagementpollvotes', 'completionengagementemojis', 'guestallowed', 'mustapproveuser', 'showpresentation', 'grade']); $logs = new backup_nested_element('logs'); $log = new backup_nested_element('log', ['id'], [ 'courseid', 'bigbluebuttonbnid', 'userid', 'timecreated', 'meetingid', 'log', 'meta']); $recordings = new backup_nested_element('recordings'); $recording = new backup_nested_element('recording', ['id'], [ 'courseid', 'bigbluebuttonbnid', 'groupid', 'recordingid', 'headlesss', 'imported', 'status', 'importeddata', 'timecreated']); // Build the tree. $bigbluebuttonbn->add_child($logs); $logs->add_child($log); $bigbluebuttonbn->add_child($recordings); $recordings->add_child($recording); // Define sources. $bigbluebuttonbn->set_source_table('bigbluebuttonbn', ['id' => backup::VAR_ACTIVITYID]); // This source definition only happen if we are including user info. if ($userinfo) { $log->set_source_table('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => backup::VAR_PARENTID]); $recording->set_source_table('bigbluebuttonbn_recordings', ['bigbluebuttonbnid' => backup::VAR_PARENTID]); } // Define id annotations. $log->annotate_ids('user', 'userid'); // Define file annotations. $bigbluebuttonbn->annotate_files('mod_bigbluebuttonbn', 'intro', null); $this->add_subplugin_structure('bbbext', $bigbluebuttonbn, true); // Return the root element (bigbluebuttonbn), wrapped into standard activity structure. return $this->prepare_activity_structure($bigbluebuttonbn); } } PK\iS>backup/moodle2/restore_bigbluebuttonbn_activity_task.class.phpnu[. /** * Class for restore BigBlueButtonBN. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot.'/mod/bigbluebuttonbn/backup/moodle2/restore_bigbluebuttonbn_stepslib.php'); /** * Restore task that provides all the settings and steps to perform one complete restore of the activity. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_bigbluebuttonbn_activity_task extends restore_activity_task { /** * Define (add) particular settings this activity can have. * * @return void */ protected function define_my_settings() { // No particular settings for this activity. } /** * Define (add) particular steps this activity can have. * * @return void */ protected function define_my_steps() { // BigBlueButtonBN only has one structure step. $this->add_step(new restore_bigbluebuttonbn_activity_structure_step('bigbluebuttonbn_structure', 'bigbluebuttonbn.xml')); } /** * Define the contents in the activity that must be processed by the link decoder. * * @return array */ public static function define_decode_contents() { $contents = []; $contents[] = new restore_decode_content('bigbluebuttonbn', ['intro'], 'bigbluebuttonbn'); $contents[] = new restore_decode_content('bigbluebuttonbn_logs', ['log'], 'bigbluebuttonbn_logs'); $contents[] = new restore_decode_content('bigbluebuttonbn_recordings', ['importeddata'], 'bigbluebuttonbn_recordings'); return $contents; } /** * Define the decoding rules for links belonging to the activity to be executed by the link decoder. * * @return array */ public static function define_decode_rules() { $rules = []; $rules[] = new restore_decode_rule('BIGBLUEBUTTONBNVIEWBYID', '/mod/bigbluebuttonbn/view.php?id=$1', 'course_module'); $rules[] = new restore_decode_rule('BIGBLUEBUTTONBNINDEX', '/mod/bigbluebuttonbn/index.php?id=$1', 'course'); return $rules; } /** * Define the restoring rules for logs belonging to the activity to be executed by the link decoder. * * @return array */ public static function define_restore_log_rules() { $rules = []; $rules[] = new restore_log_rule('bigbluebuttonbn', 'add', 'view.php?id={course_module}', '{bigbluebuttonbn}'); $rules[] = new restore_log_rule('bigbluebuttonbn', 'update', 'view.php?id={course_module}', '{bigbluebuttonbn}'); $rules[] = new restore_log_rule('bigbluebuttonbn', 'view', 'view.php?id={course_module}', '{bigbluebuttonbn}'); $rules[] = new restore_log_rule('bigbluebuttonbn', 'report', 'report.php?id={course_module}', '{bigbluebuttonbn}'); return $rules; } /** * Define the restoring rules for course associated to the activity to be executed by the link decoder. * * @return array */ public static function define_restore_log_rules_for_course() { $rules = []; $rules[] = new restore_log_rule('bigbluebuttonbn', 'view all', 'index.php?id={course}', null); return $rules; } } PK\o  =backup/moodle2/backup_bigbluebuttonbn_activity_task.class.phpnu[. /** * Class for backup BigBlueButtonBN. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) */ defined('MOODLE_INTERNAL') || die; global $CFG; require_once($CFG->dirroot.'/mod/bigbluebuttonbn/backup/moodle2/backup_bigbluebuttonbn_stepslib.php'); /** * Backup task that provides all the settings and steps to perform one complete backup of the activity. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_bigbluebuttonbn_activity_task extends backup_activity_task { /** * Define (add) particular settings this activity can have. * * @return void */ protected function define_my_settings() { // No particular settings for this activity. } /** * Define (add) particular steps this activity can have. * * @return void */ protected function define_my_steps() { // Choice only has one structure step. $this->add_step(new backup_bigbluebuttonbn_activity_structure_step('bigbluebuttonbn_structure', 'bigbluebuttonbn.xml')); } /** * Code the transformations to perform in the activity in order to get transportable (encoded) links. * * @param string $content * * @return string */ public static function encode_content_links($content) { global $CFG; $base = preg_quote($CFG->wwwroot.'/mod/bigbluebuttonbn', '#'); // Link to the list of bigbluebuttonbns. $pattern = '#('.$base."\/index.php\?id\=)([0-9]+)#"; $content = preg_replace($pattern, '$@BIGBLUEBUTTONBNINDEX*$2@$', $content); // Link to bigbluebuttonbn view by moduleid. $pattern = '#('.$base."\/view.php\?id\=)([0-9]+)#"; $content = preg_replace($pattern, '$@BIGBLUEBUTTONBNVIEWBYID*$2@$', $content); return $content; } } PK\Y? KK3backup/moodle2/restore_bigbluebuttonbn_stepslib.phpnu[. /** * Class for the structure used for restore BigBlueButtonBN. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) */ /** * Define all the restore steps that will be used by the restore_url_activity_task. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_bigbluebuttonbn_activity_structure_step extends restore_activity_structure_step { /** * Structure step to restore one bigbluebuttonbn activity. * * @return array */ protected function define_structure() { $paths = []; $bbb = new restore_path_element('bigbluebuttonbn', '/activity/bigbluebuttonbn'); $paths[] = $bbb; $paths[] = new restore_path_element('bigbluebuttonbn_logs', '/activity/bigbluebuttonbn/logs/log'); $paths[] = new restore_path_element('bigbluebuttonbn_recordings', '/activity/bigbluebuttonbn/recordings/recording'); $this->add_subplugin_structure('bbbext', $bbb); // Return the paths wrapped into standard activity structure. return $this->prepare_activity_structure($paths); } /** * Process a bigbluebuttonbn restore. * * @param array $data The data in object form * @return void */ protected function process_bigbluebuttonbn(array $data) { global $DB; $data = (object) $data; $data->course = $this->get_courseid(); $data->timemodified = $this->apply_date_offset($data->timemodified); // Check if we are in backup::MODE_IMPORT (we set a new meetingid) or backup::MODE_GENERAL (we keep the same meetingid). if ($this->get_task()->get_info()->mode == backup::MODE_IMPORT || empty($data->meetingid)) { // We are in backup::MODE_IMPORT, we need to renew the meetingid. $data->meetingid = \mod_bigbluebuttonbn\meeting::get_unique_meetingid_seed(); } // Insert the bigbluebuttonbn record. $newitemid = $DB->insert_record('bigbluebuttonbn', $data); // Immediately after inserting "activity" record, call this. $this->apply_activity_instance($newitemid); } /** * Process a bigbluebuttonbn_logs restore (additional table). * * @param array $data The data in object form * @return void */ protected function process_bigbluebuttonbn_logs(array $data) { global $DB; $data = (object) $data; // Apply modifications. $data->courseid = $this->get_mappingid('course', $data->courseid); $data->bigbluebuttonbnid = $this->get_new_parentid('bigbluebuttonbn'); $data->userid = $this->get_mappingid('user', $data->userid); $data->timecreated = $this->apply_date_offset($data->timecreated); // Insert the bigbluebuttonbn_logs record. $newitemid = $DB->insert_record('bigbluebuttonbn_logs', $data); // Immediately after inserting associated record, call this. $this->set_mapping('bigbluebuttonbn_logs', $data->id, $newitemid); } /** * Process a bigbluebuttonbn_recordings restore (additional table). * * @param array $data The data in object form * @return void */ protected function process_bigbluebuttonbn_recordings(array $data) { global $DB; $data = (object) $data; // Apply modifications. $data->courseid = $this->get_mappingid('course', $data->courseid); $data->bigbluebuttonbnid = $this->get_new_parentid('bigbluebuttonbn'); $data->timecreated = $this->apply_date_offset($data->timecreated); // Insert the bigbluebuttonbn_recordings record. $newitemid = $DB->insert_record('bigbluebuttonbn_recordings', $data); // Immediately after inserting associated record, call this. $this->set_mapping('bigbluebuttonbn_recordings', $data->id, $newitemid); } /** * Actions to be executed after the restore is completed * * @return void */ protected function after_execute() { // Add bigbluebuttonbn related files, no need to match by itemname (just internally handled context). $this->add_related_files('mod_bigbluebuttonbn', 'intro', null); } } PK\JJJview.phpnu[. /** * View a BigBlueButton room. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) * @author Darko Miletic (darko.miletic [at] gmail [dt] com) */ use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\local\config; use mod_bigbluebuttonbn\local\exceptions\server_not_available_exception; use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; use mod_bigbluebuttonbn\logger; use mod_bigbluebuttonbn\output\view_page; use mod_bigbluebuttonbn\plugin; require(__DIR__ . '/../../config.php'); global $OUTPUT, $PAGE; // Get the bbb instance from either the cmid (id), or the instanceid (bn). $id = optional_param('id', 0, PARAM_INT); if ($id) { $instance = instance::get_from_cmid($id); } else { $bn = optional_param('bn', 0, PARAM_INT); if ($bn) { $instance = instance::get_from_instanceid($bn); } } if (!$instance) { throw new moodle_exception('view_error_url_missing_parameters', plugin::COMPONENT); } $cm = $instance->get_cm(); $course = $instance->get_course(); $bigbluebuttonbn = $instance->get_instance_data(); require_login($course, true, $cm); $groupid = groups_get_activity_group($cm, true) ?: null; if ($groupid) { $instance->set_group_id($groupid); } logger::log_instance_viewed($instance); // Require a working server. bigbluebutton_proxy::require_working_server($instance); // Mark viewed by user (if required). $completion = new completion_info($course); $completion->set_module_viewed($cm); // Print the page header. $PAGE->set_url($instance->get_view_url()); $PAGE->set_title($cm->name); $PAGE->set_cacheable(false); $PAGE->set_heading($course->fullname); // Output starts. $renderer = $PAGE->get_renderer('mod_bigbluebuttonbn'); try { $renderedinfo = $renderer->render(new view_page($instance)); } catch (server_not_available_exception $e) { bigbluebutton_proxy::handle_server_not_available($instance); } echo $OUTPUT->header(); // Valid credentials have not been setup, then we output a message to teachers and admin. if (config::server_credentials_invalid()) { if (has_capability('moodle/site:config', context_system::instance())) { $settingslink = new moodle_url('/admin/settings.php', ['section' => 'modsettingbigbluebuttonbn']); echo $OUTPUT->notification(get_string('settings_credential_warning', 'bigbluebuttonbn', ['settingslink' => $settingslink->out()]), 'notifywarning'); } else if (has_capability('moodle/course:manageactivities', context_course::instance($course->id))) { echo $OUTPUT->notification(get_string('settings_credential_warning_no_capability', 'bigbluebuttonbn'), 'notifywarning'); } } // Validate if the user is in a role allowed to join. if (!$instance->can_join() && $instance->get_type() != instance::TYPE_RECORDING_ONLY) { if (isguestuser()) { notice(get_string('view_noguests', plugin::COMPONENT), get_login_url()); } else { notice( get_string('view_nojoin', plugin::COMPONENT), new moodle_url('/course/view.php', ['id' => $course->id]) ); } } echo $renderedinfo; // Output finishes. echo $OUTPUT->footer(); // Shows version as a comment. echo '' . "\n"; PK\Rjpix/monologo.svgnu[ PK\XFnxnxlib.phpnu[. /** * Library calls for Moodle and BigBlueButton. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) */ defined('MOODLE_INTERNAL') || die; use core_calendar\action_factory; use core_calendar\local\event\entities\action_interface; use mod_bigbluebuttonbn\completion\custom_completion; use mod_bigbluebuttonbn\extension; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\local\bigbluebutton; use mod_bigbluebuttonbn\local\exceptions\server_not_available_exception; use mod_bigbluebuttonbn\local\helpers\files; use mod_bigbluebuttonbn\local\helpers\mod_helper; use mod_bigbluebuttonbn\local\helpers\reset; use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; use mod_bigbluebuttonbn\logger; use mod_bigbluebuttonbn\meeting; use mod_bigbluebuttonbn\recording; use mod_bigbluebuttonbn\local\config; global $CFG; /** * Indicates API features that the bigbluebuttonbn supports. * * @param string $feature * @return mixed True if yes (some features may use other values) * @uses FEATURE_IDNUMBER * @uses FEATURE_GROUPS * @uses FEATURE_GROUPINGS * @uses FEATURE_GROUPMEMBERSONLY * @uses FEATURE_MOD_INTRO * @uses FEATURE_BACKUP_MOODLE2 * @uses FEATURE_COMPLETION_TRACKS_VIEWS * @uses FEATURE_COMPLETION_HAS_RULES * @uses FEATURE_GRADE_HAS_GRADE * @uses FEATURE_GRADE_OUTCOMES * @uses FEATURE_SHOW_DESCRIPTION */ function bigbluebuttonbn_supports($feature) { if (!$feature) { return null; } $features = [ FEATURE_IDNUMBER => true, FEATURE_GROUPS => true, FEATURE_GROUPINGS => true, FEATURE_MOD_INTRO => true, FEATURE_BACKUP_MOODLE2 => true, FEATURE_COMPLETION_TRACKS_VIEWS => true, FEATURE_COMPLETION_HAS_RULES => true, FEATURE_GRADE_HAS_GRADE => true, FEATURE_GRADE_OUTCOMES => false, FEATURE_SHOW_DESCRIPTION => true, FEATURE_MOD_PURPOSE => MOD_PURPOSE_COMMUNICATION, ]; if (isset($features[(string) $feature])) { return $features[$feature]; } return null; } /** * Given an object containing all the necessary data, * (defined by the form in mod_form.php) this function * will create a new instance and return the id number * of the new instance. * * @param stdClass $bigbluebuttonbn An object from the form in mod_form.php * @return int The id of the newly inserted bigbluebuttonbn record */ function bigbluebuttonbn_add_instance($bigbluebuttonbn) { global $DB; // Excecute preprocess. mod_helper::process_pre_save($bigbluebuttonbn); // Pre-set initial values. $bigbluebuttonbn->presentation = files::save_media_file($bigbluebuttonbn); // Encode meetingid. $bigbluebuttonbn->meetingid = meeting::get_unique_meetingid_seed(); [$bigbluebuttonbn->guestlinkuid, $bigbluebuttonbn->guestpassword] = \mod_bigbluebuttonbn\plugin::generate_guest_meeting_credentials(); // Insert a record. $bigbluebuttonbn->id = $DB->insert_record('bigbluebuttonbn', $bigbluebuttonbn); // Log insert action. logger::log_instance_created($bigbluebuttonbn); // Complete the process. mod_helper::process_post_save($bigbluebuttonbn); // Call any active subplugin so to signal a new creation. extension::add_instance($bigbluebuttonbn); // Create new grade item. bigbluebuttonbn_grade_item_update($bigbluebuttonbn); return $bigbluebuttonbn->id; } /** * Given an object containing all the necessary data, * (defined by the form in mod_form.php) this function * will update an existing instance with new data. * * @param stdClass $bigbluebuttonbn An object from the form in mod_form.php * @return bool Success/Fail */ function bigbluebuttonbn_update_instance($bigbluebuttonbn) { global $DB; // Excecute preprocess. mod_helper::process_pre_save($bigbluebuttonbn); // Pre-set initial values. $bigbluebuttonbn->id = $bigbluebuttonbn->instance; $bigbluebuttonbn->presentation = files::save_media_file($bigbluebuttonbn); if (empty($bigbluebuttonbn->guestjoinurl) || empty($bigbluebuttonbn->guestpassword)) { [$bigbluebuttonbn->guestlinkuid, $bigbluebuttonbn->guestpassword] = \mod_bigbluebuttonbn\plugin::generate_guest_meeting_credentials(); } // Update a record. $DB->update_record('bigbluebuttonbn', $bigbluebuttonbn); bigbluebuttonbn_grade_item_update($bigbluebuttonbn); // Get the meetingid column in the bigbluebuttonbn table. $bigbluebuttonbn->meetingid = (string) $DB->get_field('bigbluebuttonbn', 'meetingid', ['id' => $bigbluebuttonbn->id]); // Log update action. logger::log_instance_updated(instance::get_from_instanceid($bigbluebuttonbn->id)); // Complete the process. mod_helper::process_post_save($bigbluebuttonbn); // Call any active subplugin so to signal update. extension::update_instance($bigbluebuttonbn); return true; } /** * Given an ID of an instance of this module, * this function will permanently delete the instance * and any data that depends on it. * * @param int $id Id of the module instance * * @return bool Success/Failure */ function bigbluebuttonbn_delete_instance($id) { global $DB; $instance = instance::get_from_instanceid($id); if (empty($instance)) { return false; } // End all meeting if any still running. try { $meeting = new meeting($instance); $meeting->end_meeting(); } catch (moodle_exception $e) { // Do not log any issue when testing. if (!(defined('PHPUNIT_TEST') && PHPUNIT_TEST) && !defined('BEHAT_SITE_RUNNING')) { debugging($e->getMessage(), DEBUG_DEVELOPER, $e->getTrace()); } } // Get all possible groups (course and course module). $groupids = []; if (groups_get_activity_groupmode($instance->get_cm())) { $coursegroups = groups_get_activity_allowed_groups($instance->get_cm()); $groupids = array_map( function($gp) { return $gp->id; }, $coursegroups); } // End all meetings for all groups. foreach ($groupids as $groupid) { try { $instance->set_group_id($groupid); $meeting = new meeting($instance); $meeting->end_meeting(); } catch (moodle_exception $e) { debugging($e->getMessage() . ' for group ' . $groupid, DEBUG_NORMAL, $e->getTrace()); } } $result = true; // Delete grades. $bigbluebuttonbn = $DB->get_record('bigbluebuttonbn', ['id' => $id]); bigbluebuttonbn_grade_item_delete($bigbluebuttonbn); // Call any active subplugin so to signal deletion. extension::delete_instance($id); // Delete the instance. if (!$DB->delete_records('bigbluebuttonbn', ['id' => $id])) { $result = false; } // Delete dependant events. if (!$DB->delete_records('event', ['modulename' => 'bigbluebuttonbn', 'instance' => $id])) { $result = false; } // Log action performed. logger::log_instance_deleted($instance); // Mark dependent recordings as headless. foreach (recording::get_records(['bigbluebuttonbnid' => $id]) as $recording) { $recording->set('headless', recording::RECORDING_HEADLESS); $recording->update(); } return $result; } /** * Return a small object with summary information about what a * user has done with a given particular instance of this module * Used for user activity reports. * * @param stdClass $course * @param stdClass $user * @param cm_info $mod * @param stdClass $bigbluebuttonbn * * @return stdClass with info and time (timestamp of the last log) */ function bigbluebuttonbn_user_outline(stdClass $course, stdClass $user, cm_info $mod, stdClass $bigbluebuttonbn): stdClass { [$infos, $logtimestamps] = \mod_bigbluebuttonbn\local\helpers\user_info::get_user_info_outline($course, $user, $mod); return (object) [ 'info' => join(',', $infos), 'time' => !empty($logtimestamps) ? max($logtimestamps) : 0 ]; } /** * Print a detailed representation of what a user has done with * a given particular instance of this module, for user activity reports. * * @param stdClass $course * @param stdClass $user * @param cm_info $mod * @param stdClass $bigbluebuttonbn * */ function bigbluebuttonbn_user_complete(stdClass $course, stdClass $user, cm_info $mod, stdClass $bigbluebuttonbn) { [$infos] = \mod_bigbluebuttonbn\local\helpers\user_info::get_user_info_outline($course, $user, $mod); echo join(', ', $infos); } /** * This flags this module with the capability to override the completion status with the custom completion rules. * * * @return int */ function bigbluebuttonbn_get_completion_aggregation_state() { return COMPLETION_CUSTOM_MODULE_FLOW; } /** * Returns all other caps used in module. * * @return string[] */ function bigbluebuttonbn_get_extra_capabilities() { return ['moodle/site:accessallgroups']; } /** * Called by course/reset.php * * @param MoodleQuickForm $mform */ function bigbluebuttonbn_reset_course_form_definition(&$mform) { $items = reset::reset_course_items(); $mform->addElement('header', 'bigbluebuttonbnheader', get_string('modulenameplural', 'bigbluebuttonbn')); foreach ($items as $item => $default) { $mform->addElement( 'advcheckbox', "reset_bigbluebuttonbn_{$item}", get_string("reset{$item}", 'bigbluebuttonbn') ); if ($item == 'logs' || $item == 'recordings') { $mform->addHelpButton("reset_bigbluebuttonbn_{$item}", "reset{$item}", 'bigbluebuttonbn'); } } } /** * Course reset form defaults. * * @param stdClass $course * @return array */ function bigbluebuttonbn_reset_course_form_defaults(stdClass $course) { $formdefaults = []; $items = reset::reset_course_items(); // All unchecked by default. foreach ($items as $item => $default) { $formdefaults["reset_bigbluebuttonbn_{$item}"] = $default; } return $formdefaults; } /** * This function is used by the reset_course_userdata function in moodlelib. * * @param stdClass $data the data submitted from the reset course. * @return array status array */ function bigbluebuttonbn_reset_userdata(stdClass $data) { $items = reset::reset_course_items(); $status = []; // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset. // See MDL-9367. if (array_key_exists('recordings', $items) && !empty($data->reset_bigbluebuttonbn_recordings)) { // Remove all the recordings from a BBB server that are linked to the room/activities in this course. reset::reset_recordings($data->courseid); unset($items['recordings']); $status[] = reset::reset_getstatus('recordings'); } if (!empty($data->reset_bigbluebuttonbn_tags)) { // Remove all the tags linked to the room/activities in this course. reset::reset_tags($data->courseid); unset($items['tags']); $status[] = reset::reset_getstatus('tags'); } if (!empty($data->reset_bigbluebuttonbn_logs)) { // Remove all the tags linked to the room/activities in this course. reset::reset_logs($data->courseid); unset($items['logs']); $status[] = reset::reset_getstatus('logs'); } // Remove all grades from gradebook. if (!empty($data->reset_gradebook_grades)) { bigbluebuttonbn_reset_gradebook($data->courseid); } return $status; } /** * Given a course_module object, this function returns any * "extra" information that may be needed when printing * this activity in a course listing. * See get_array_of_activities() in course/lib.php. * * @param stdClass $coursemodule * * @return null|cached_cm_info */ function bigbluebuttonbn_get_coursemodule_info($coursemodule) { $instance = instance::get_from_instanceid($coursemodule->instance); if (empty($instance)) { return null; } $info = new cached_cm_info(); // Warning here: if any of the instance method calls ::get_cm this will result is a recursive call. // So best is just to access instance variables not linked to the cm. $info->name = $instance->get_instance_var('name'); if ($coursemodule->showdescription) { // Convert intro to html. Do not filter cached version, filters run at display time. $info->content = format_module_intro('bigbluebuttonbn', $instance->get_instance_data(), $coursemodule->id, false); } $customcompletionfields = custom_completion::get_defined_custom_rules(); // Populate the custom completion rules as key => value pairs, but only if the completion mode is 'automatic'. if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) { foreach ($customcompletionfields as $completiontype) { $info->customdata['customcompletionrules'][$completiontype] = $instance->get_instance_var($completiontype) ?? 0; } } return $info; } /** * Serves the bigbluebuttonbn attachments. Implements needed access control ;-). * * @param stdClass $course course object * @param stdClass $cm course module object * @param context $context context object * @param string $filearea file area * @param array $args extra arguments * @param bool $forcedownload whether or not force download * @param array $options additional options affecting the file serving * * @return false|null false if file not found, does not return if found - justsend the file * @category files * */ function bigbluebuttonbn_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = []) { if (!files::pluginfile_valid($context, $filearea)) { return false; } $file = files::pluginfile_file($course, $cm, $context, $filearea, $args); if (empty($file)) { return false; } // Finally send the file. return send_stored_file($file, 0, 0, $forcedownload, $options); // Download MUST be forced - security! } /** * Mark the activity completed (if required) and trigger the course_module_viewed event. * * @param stdClass $bigbluebuttonbn bigbluebuttonbn object * @param stdClass $course course object * @param cm_info $cm course module object * @param context $context context object * @since Moodle 3.0 */ function bigbluebuttonbn_view($bigbluebuttonbn, $course, $cm, $context) { // Trigger course_module_viewed event. $params = [ 'context' => $context, 'objectid' => $bigbluebuttonbn->id ]; $event = \mod_bigbluebuttonbn\event\course_module_viewed::create($params); // Fix event name. $cmrecord = $cm->get_course_module_record(); $event->add_record_snapshot('course_modules', $cmrecord); $event->add_record_snapshot('course', $course); $event->add_record_snapshot('bigbluebuttonbn', $bigbluebuttonbn); $event->trigger(); // Completion. $completion = new completion_info($course); $completion->set_module_viewed($cm); } /** * Check if the module has any update that affects the current user since a given time. * * @param cm_info $cm course module data * @param int $from the time to check updates from * @param array $filter if we need to check only specific updates * @return stdClass an object with the different type of areas indicating if they were updated or not * @since Moodle 3.2 */ function bigbluebuttonbn_check_updates_since(cm_info $cm, $from, $filter = []) { $updates = course_check_module_updates_since($cm, $from, ['content'], $filter); return $updates; } /** * This function receives a calendar event and returns the action associated with it, or null if there is none. * * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event * is not displayed on the block. * * @param calendar_event $event * @param action_factory $factory * @return action_interface|null */ function mod_bigbluebuttonbn_core_calendar_provide_event_action( calendar_event $event, action_factory $factory ) { global $DB; $time = time(); // Get mod info. $cm = get_fast_modinfo($event->courseid)->instances['bigbluebuttonbn'][$event->instance]; // Get bigbluebuttonbn activity. $bigbluebuttonbn = $DB->get_record('bigbluebuttonbn', ['id' => $event->instance], '*', MUST_EXIST); // Set flag haspassed if closingtime has already passed only if it is defined. $haspassed = ($bigbluebuttonbn->closingtime) && $bigbluebuttonbn->closingtime < $time; // Set flag hasstarted if startingtime has already passed or not defined. $hasstarted = $bigbluebuttonbn->openingtime < $time; // Return null if it has passed or not started. if ($haspassed || !$hasstarted) { return null; } // Get if the user has joined in live session or viewed the recorded. $customcompletion = new custom_completion($cm, $event->userid); $usercomplete = $customcompletion->get_overall_completion_state(); $instance = instance::get_from_instanceid($bigbluebuttonbn->id); // Get if the room is available. $roomavailable = $instance->is_currently_open(); $meetinginfo = null; // Check first if the server can be contacted. try { if (empty(bigbluebutton_proxy::get_server_version())) { // In this case we should already have debugging message printed. return null; } // Get if the user can join. $meetinginfo = meeting::get_meeting_info_for_instance($instance); } catch (moodle_exception $e) { debugging('Error - Cannot retrieve info from meeting ('.$instance->get_meeting_id().') ' . $e->getMessage()); return null; } $usercanjoin = $meetinginfo->canjoin; // Check if the room is closed and the user has already joined this session or played the record. if (!$roomavailable && $usercomplete) { return null; } // Check if the user can join this session. $actionable = ($roomavailable && $usercanjoin); // Action data. $string = get_string('view_room', 'bigbluebuttonbn'); $url = new moodle_url('/mod/bigbluebuttonbn/view.php', ['id' => $cm->id]); if (groups_get_activity_groupmode($cm) == NOGROUPS) { // No groups mode. $string = get_string('view_conference_action_join', 'bigbluebuttonbn'); $url = new moodle_url('/mod/bigbluebuttonbn/bbb_view.php', [ 'action' => 'join', 'id' => $cm->id, 'bn' => $bigbluebuttonbn->id, 'timeline' => 1, ] ); } return $factory->create_instance($string, $url, 1, $actionable); } /** * Is the event visible? * * @param calendar_event $event * @return bool Returns true if the event is visible to the current user, false otherwise. */ function mod_bigbluebuttonbn_core_calendar_is_event_visible(calendar_event $event) { $instance = instance::get_from_instanceid($event->instance); if (!$instance) { return false; } $activitystatus = mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy::view_get_activity_status($instance); return $activitystatus != 'ended'; } /** * Adds module specific settings to the settings block * * @param settings_navigation $settingsnav The settings navigation object * @param navigation_node $nodenav The node to add module settings to */ function bigbluebuttonbn_extend_settings_navigation(settings_navigation $settingsnav, navigation_node $nodenav) { global $USER; // Don't add validate completion if the callback for meetingevents is NOT enabled. if (!(boolean) \mod_bigbluebuttonbn\local\config::get('meetingevents_enabled')) { return; } // Don't add validate completion if user is not allowed to edit the activity. $context = context_module::instance($settingsnav->get_page()->cm->id); if (!has_capability('moodle/course:manageactivities', $context, $USER->id)) { return; } $completionvalidate = '#action=completion_validate&bigbluebuttonbn=' . $settingsnav->get_page()->cm->instance; $nodenav->add(get_string('completionvalidatestate', 'bigbluebuttonbn'), $completionvalidate, navigation_node::TYPE_CONTAINER); } /** * In place editable for the recording table * * @param string $itemtype * @param string $itemid * @param mixed $newvalue * @return mixed|null */ function bigbluebuttonbn_inplace_editable($itemtype, $itemid, $newvalue) { $editableclass = "\\mod_bigbluebuttonbn\\output\\recording_{$itemtype}_editable"; if (class_exists($editableclass)) { return call_user_func([$editableclass, 'update'], $itemid, $newvalue); } return null; // Will raise an exception in core update_inplace_editable method. } /** * Returns all events since a given time in specified bigbluebutton activity. * We focus here on the two events: play and join. * * @param array $activities * @param int $index * @param int $timestart * @param int $courseid * @param int $cmid * @param int $userid * @param int $groupid * @return array */ function bigbluebuttonbn_get_recent_mod_activity(&$activities, &$index, $timestart, $courseid, $cmid, $userid = 0, $groupid = 0): array { $instance = instance::get_from_cmid($cmid); $instance->set_group_id($groupid); $cm = $instance->get_cm(); $logs = logger::get_user_completion_logs_with_userfields($instance, $userid ?? null, [logger::EVENT_JOIN, logger::EVENT_PLAYED], $timestart); foreach ($logs as $log) { $activity = new stdClass(); $activity->type = 'bigbluebuttonbn'; $activity->cmid = $cm->id; $activity->name = format_string($instance->get_meeting_name(), true); $activity->sectionnum = $cm->sectionnum; $activity->timestamp = $log->timecreated; $activity->user = new stdClass(); $userfields = explode(',', implode(',', \core_user\fields::get_picture_fields())); foreach ($userfields as $userfield) { if ($userfield == 'id') { // Aliased in SQL above. $activity->user->{$userfield} = $log->userid; } else { $activity->user->{$userfield} = $log->{$userfield}; } } $activity->user->fullname = fullname($log); $activity->content = ''; $activity->eventname = logger::get_printable_event_name($log); if ($log->log == logger::EVENT_PLAYED) { if (!empty($log->meta)) { $meta = json_decode($log->meta); if (!empty($meta->recordingid)) { $recording = recording::get_record(['id' => $meta->recordingid]); if ($recording) { $activity->content = $recording->get('name'); } } } } $activities[$index++] = $activity; } return $activities; } /** * Outputs the bigbluebutton logs indicated by $activity. * * @param stdClass $activity the activity object the bigbluebuttonbn resides in * @param int $courseid the id of the course the bigbluebuttonbn resides in * @param bool $detail not used, but required for compatibilty with other modules * @param array $modnames not used, but required for compatibilty with other modules * @param bool $viewfullnames not used, but required for compatibilty with other modules */ function bigbluebuttonbn_print_recent_mod_activity(stdClass $activity, int $courseid, bool $detail, array $modnames, bool $viewfullnames) { global $OUTPUT; $modinfo = []; $userpicture = $OUTPUT->user_picture($activity->user); $template = ['userpicture' => $userpicture, 'submissiontimestamp' => $activity->timestamp, 'modinfo' => $modinfo, 'userurl' => new moodle_url('/user/view.php', array('id' => $activity->user->id, 'course' => $courseid)), 'fullname' => $activity->user->fullname]; if (isset($activity->eventname)) { $template['eventname'] = $activity->eventname; } echo $OUTPUT->render_from_template('mod_bigbluebuttonbn/recentactivity', $template); } /** * Given a course and a date, prints a summary of all the activity for this module * * @param object $course * @param bool $viewfullnames capability * @param int $timestart * @return bool success */ function bigbluebuttonbn_print_recent_activity(object $course, bool $viewfullnames, int $timestart): bool { global $OUTPUT; $modinfo = get_fast_modinfo($course); if (empty($modinfo->instances['bigbluebuttonbn'])) { return true; } $out = ''; foreach ($modinfo->instances['bigbluebuttonbn'] as $cm) { if (!$cm->uservisible) { continue; } $instance = instance::get_from_cmid($cm->id); $logs = logger::get_user_completion_logs_with_userfields($instance, null, [logger::EVENT_JOIN, logger::EVENT_PLAYED], $timestart); if ($logs) { echo $OUTPUT->heading(get_string('new_bigblubuttonbn_activities', 'bigbluebuttonbn') . ':', 6); foreach ($logs as $log) { $activityurl = new moodle_url('/mod/bigbluebuttonbn/index.php', ['id' => $course->id]); print_recent_activity_note($log->timecreated, $log, logger::get_printable_event_name($log) . ' - ' . $instance->get_meeting_name(), $activityurl->out(), false, $viewfullnames); } } echo $out; } return true; } /** * Creates a number of BigblueButtonBN activities. * * @param tool_generator_course_backend $backend * @param testing_data_generator $generator * @param int $courseid * @param int $number * @return void */ function bigbluebuttonbn_course_backend_generator_create_activity(tool_generator_course_backend $backend, testing_data_generator $generator, int $courseid, int $number ) { // Set up generator. $bbbgenerator = $generator->get_plugin_generator('mod_bigbluebuttonbn'); // Create assignments. $backend->log('createbigbluebuttonbn', $number, true, 'mod_bigbluebuttonbn'); for ($i = 0; $i < $number; $i++) { $record = array('course' => $courseid); $options = array('section' => $backend->get_target_section()); $bbbgenerator->create_instance($record, $options); $backend->dot($i, $number); } $backend->end_log(); } /** * Whether the activity is branded. * This information is used, for instance, to decide if a filter should be applied to the icon or not. * * @return bool True if the activity is branded, false otherwise. */ function bigbluebuttonbn_is_branded(): bool { return true; } /** * Update/create grade item for given BigBlueButtonBN activity * * @category grade * @param stdClass $bigbluebuttonbn instance object * @param array|object|string|null $grades Optional array/object of grade(s); 'reset' means reset grades in gradebook * @return int 0 if ok, error code otherwise */ function bigbluebuttonbn_grade_item_update(stdClass $bigbluebuttonbn, array|object|string|null $grades=null): int { global $CFG; require_once($CFG->libdir.'/gradelib.php'); $params = ['itemname' => $bigbluebuttonbn->name]; if ($bigbluebuttonbn->grade == 0) { $params['gradetype'] = GRADE_TYPE_NONE; } else if ($bigbluebuttonbn->grade > 0) { $params['gradetype'] = GRADE_TYPE_VALUE; $params['grademax'] = $bigbluebuttonbn->grade; $params['grademin'] = 0; } else if ($bigbluebuttonbn->grade < 0) { $params['gradetype'] = GRADE_TYPE_SCALE; $params['scaleid'] = -$bigbluebuttonbn->grade; } if ($grades === 'reset') { $params['reset'] = true; $grades = null; } return grade_update( source: 'mod/bigbluebuttonbn', courseid: $bigbluebuttonbn->course, itemtype: 'mod', itemmodule: 'bigbluebuttonbn', iteminstance: $bigbluebuttonbn->id, itemnumber: 0, grades: $grades, itemdetails: $params ); } /** * Update activity grades. * * @param stdClass $bigbluebuttonbn instance object */ function bigbluebuttonbn_update_grades(stdClass $bigbluebuttonbn): void { // BigBlueButtonBN does not have a grades table, so we will only update grade item. bigbluebuttonbn_grade_item_update($bigbluebuttonbn); } /** * Removes all grades from gradebook * * @param int $courseid */ function bigbluebuttonbn_reset_gradebook(int $courseid): void { global $DB; $sql = "SELECT b.*, cm.idnumber as cmidnumber, b.course as courseid FROM {bigbluebuttonbn} b, {course_modules} cm, {modules} m WHERE m.name='bigbluebuttonbn' AND m.id=cm.module AND cm.instance=b.id AND b.course=?"; if ($bigbluebuttonbns = $DB->get_records_sql($sql, [$courseid])) { foreach ($bigbluebuttonbns as $bigbluebuttonbn) { bigbluebuttonbn_grade_item_update($bigbluebuttonbn, 'reset'); } } } /** * Delete grade item for given activity * * @param stdClass $bigbluebuttonbn instance object * @return int Returns GRADE_UPDATE_OK, GRADE_UPDATE_FAILED, GRADE_UPDATE_MULTIPLE or GRADE_UPDATE_ITEM_LOCKED */ function bigbluebuttonbn_grade_item_delete(stdClass $bigbluebuttonbn): int { global $CFG; require_once($CFG->libdir.'/gradelib.php'); return grade_update( source: 'mod/bigbluebuttonbn', courseid: $bigbluebuttonbn->course, itemtype: 'mod', itemmodule: 'bigbluebuttonbn', iteminstance: $bigbluebuttonbn->id, itemnumber: 0, grades: null, itemdetails: ['deleted' => 1] ); } PK\{( version.phpnu[. /** * Version for BigBlueButtonBN Moodle Activity Module. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) */ defined('MOODLE_INTERNAL') || die; $plugin->version = 2025041400; $plugin->requires = 2025040800; $plugin->component = 'mod_bigbluebuttonbn'; PK\B204 4 import_view.phpnu[. /** * View for importing BigBlueButtonBN recordings. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) */ use core\notification; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\output\import_view; use mod_bigbluebuttonbn\plugin; require(__DIR__ . '/../../config.php'); $destbn = required_param('destbn', PARAM_INT); $sourcebn = optional_param('sourcebn', -1, PARAM_INT); $sourcecourseid = optional_param('sourcecourseid', -1, PARAM_INT); $destinationinstance = instance::get_from_instanceid($destbn); if (!$destinationinstance) { throw new moodle_exception('view_error_url_missing_parameters', plugin::COMPONENT); } $cm = $destinationinstance->get_cm(); $course = $destinationinstance->get_course(); require_login($course, true, $cm); if (!(boolean) \mod_bigbluebuttonbn\local\config::importrecordings_enabled()) { notification::add( get_string('view_message_importrecordings_disabled', plugin::COMPONENT), notification::ERROR ); redirect($destinationinstance->get_view_url()); } // Print the page header. $PAGE->set_url($destinationinstance->get_import_url()); $PAGE->set_title($destinationinstance->get_meeting_name()); $PAGE->set_cacheable(false); $PAGE->set_heading($course->fullname); /** @var \mod_bigbluebuttonbn\renderer $renderer */ $renderer = $PAGE->get_renderer(plugin::COMPONENT); echo $OUTPUT->header(); echo $renderer->render(new import_view($destinationinstance, $sourcecourseid, $sourcebn)); echo $OUTPUT->footer(); PK\=Bsadminmanageplugins.phpnu[. /** * Allows the admin to manage extension plugins * * @copyright 2023 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ use mod_bigbluebuttonbn\local\plugins\admin_plugin_manager; require_once(__DIR__ . '/../../config.php'); global $PAGE; require_login(); $action = optional_param('action', null, PARAM_PLUGIN); $plugin = optional_param('plugin', null, PARAM_PLUGIN); if (!empty($plugin)) { require_sesskey(); } // Create the class for this controller. $pluginmanager = new admin_plugin_manager(); $PAGE->set_context(context_system::instance()); // Execute the controller. $pluginmanager->execute($action, $plugin); PK\.t.tclasses/recording.phpnu[. namespace mod_bigbluebuttonbn; use cache; use context; use context_course; use context_module; use core\persistent; use mod_bigbluebuttonbn\local\proxy\recording_proxy; use moodle_url; use stdClass; /** * The recording entity. * * This is utility class that defines a single recording, and provides methods for their local handling locally, and * communication with the bigbluebutton server. * * @package mod_bigbluebuttonbn * @copyright 2021 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class recording extends persistent { /** The table name. */ const TABLE = 'bigbluebuttonbn_recordings'; /** @var int Defines that the activity used to create the recording no longer exists */ public const RECORDING_HEADLESS = 1; /** @var int Defines that the recording is not the original but an imported one */ public const RECORDING_IMPORTED = 1; /** @var int Defines that the list should include imported recordings */ public const INCLUDE_IMPORTED_RECORDINGS = true; /** @var int A meeting set to be recorded still awaits for a recording update */ public const RECORDING_STATUS_AWAITING = 0; /** @var int A meeting set to be recorded was not recorded and dismissed by BBB */ public const RECORDING_STATUS_DISMISSED = 1; /** @var int A meeting set to be recorded has a recording processed */ public const RECORDING_STATUS_PROCESSED = 2; /** @var int A meeting set to be recorded received notification callback from BBB */ public const RECORDING_STATUS_NOTIFIED = 3; /** @var int A meeting set to be recorded was processed and set back to an awaiting state */ public const RECORDING_STATUS_RESET = 4; /** @var int A meeting set to be recorded was deleted from bigbluebutton */ public const RECORDING_STATUS_DELETED = 5; /** @var bool Whether metadata been changed so the remote information needs to be updated ? */ protected $metadatachanged = false; /** @var int A refresh period for recordings, defaults to 300s (5mins) */ public const RECORDING_REFRESH_DEFAULT_PERIOD = 300; /** @var int A time limit for recordings to be dismissed, defaults to 30d (30days) */ public const RECORDING_TIME_LIMIT_DAYS = 30; /** @var array A cached copy of the metadata */ protected $metadata = null; /** @var instance A cached copy of the instance */ protected $instance; /** @var bool imported recording status */ public $imported; /** * Create an instance of this class. * * @param int $id If set, this is the id of an existing record, used to load the data. * @param stdClass|null $record If set will be passed to from_record * @param null|array $metadata */ public function __construct($id = 0, ?stdClass $record = null, ?array $metadata = null) { if ($record) { $record->headless = $record->headless ?? false; $record->imported = $record->imported ?? false; $record->groupid = $record->groupid ?? 0; $record->status = $record->status ?? self::RECORDING_STATUS_AWAITING; } parent::__construct($id, $record); if ($metadata) { $this->metadata = $metadata; } } /** * Helper function to retrieve recordings from the BigBlueButton. * * @param instance $instance * @param bool $includeimported * @param bool $onlyimported * * @return recording[] containing the recordings indexed by recordID, each recording is also a * non sequential associative array itself that corresponds to the actual recording in BBB */ public static function get_recordings_for_instance( instance $instance, bool $includeimported = false, bool $onlyimported = false ): array { [$selects, $params] = self::get_basic_select_from_parameters(false, $includeimported, $onlyimported); $selects[] = "bigbluebuttonbnid = :bbbid"; $params['bbbid'] = $instance->get_instance_id(); $groupmode = groups_get_activity_groupmode($instance->get_cm()); $context = $instance->get_context(); if ($groupmode) { [$groupselects, $groupparams] = self::get_select_for_group( $groupmode, $context, $instance->get_course_id(), $instance->get_group_id(), $instance->get_cm()->groupingid ); if ($groupselects) { $selects[] = $groupselects; $params = array_merge_recursive($params, $groupparams); } } $recordings = self::fetch_records($selects, $params); foreach ($recordings as $recording) { $recording->instance = $instance; } return $recordings; } /** * Helper function to retrieve recordings from a given course. * * @param int $courseid id for a course record or null * @param array $excludedinstanceid exclude recordings from instance ids * @param bool $includeimported * @param bool $onlyimported * @param bool $includedeleted * @param bool $onlydeleted * * @return recording[] containing the recordings indexed by recordID, each recording is also a * non sequential associative array itself that corresponds to the actual recording in BBB */ public static function get_recordings_for_course( int $courseid, array $excludedinstanceid = [], bool $includeimported = false, bool $onlyimported = false, bool $includedeleted = false, bool $onlydeleted = false ): array { global $DB; [$selects, $params] = self::get_basic_select_from_parameters( $includedeleted, $includeimported, $onlyimported, $onlydeleted ); if ($courseid) { $selects[] = "courseid = :courseid"; $params['courseid'] = $courseid; $course = $DB->get_record('course', ['id' => $courseid]); $groupmode = groups_get_course_groupmode($course); $context = context_course::instance($courseid); } else { $context = \context_system::instance(); $groupmode = NOGROUPS; } if ($groupmode) { [$groupselects, $groupparams] = self::get_select_for_group($groupmode, $context, $course->id); if ($groupselects) { $selects[] = $groupselects; $params = array_merge($params, $groupparams); } } if ($excludedinstanceid) { [$sqlexcluded, $paramexcluded] = $DB->get_in_or_equal($excludedinstanceid, SQL_PARAMS_NAMED, 'param', false); $selects[] = "bigbluebuttonbnid {$sqlexcluded}"; $params = array_merge($params, $paramexcluded); } return self::fetch_records($selects, $params); } /** * Get select for given group mode and context * * @param int $groupmode * @param \context $context * @param int $courseid * @param int $groupid * @param int $groupingid * @return array */ protected static function get_select_for_group($groupmode, $context, $courseid, $groupid = 0, $groupingid = 0): array { global $DB, $USER; $selects = []; $params = []; if ($groupmode) { $accessallgroups = has_capability('moodle/site:accessallgroups', $context) || $groupmode == VISIBLEGROUPS; if ($accessallgroups) { if ($context instanceof context_module) { $allowedgroups = groups_get_all_groups($courseid, 0, $groupingid); } else { $allowedgroups = groups_get_all_groups($courseid); } } else { if ($context instanceof context_module) { $allowedgroups = groups_get_all_groups($courseid, $USER->id, $groupingid); } else { $allowedgroups = groups_get_all_groups($courseid, $USER->id); } } $allowedgroupsid = array_map(function ($g) { return $g->id; }, $allowedgroups); if ($groupid || empty($allowedgroups)) { $selects[] = "groupid = :groupid"; $params['groupid'] = ($groupid && in_array($groupid, $allowedgroupsid)) ? $groupid : 0; } else { if ($accessallgroups) { $allowedgroupsid[] = 0; } list($groupselects, $groupparams) = $DB->get_in_or_equal($allowedgroupsid, SQL_PARAMS_NAMED); $selects[] = 'groupid ' . $groupselects; $params = array_merge_recursive($params, $groupparams); } } return [ implode(" AND ", $selects), $params, ]; } /** * Get basic sql select from given parameters * * @param bool $includedeleted * @param bool $includeimported * @param bool $onlyimported * @param bool $onlydeleted * @return array */ protected static function get_basic_select_from_parameters( bool $includedeleted = false, bool $includeimported = false, bool $onlyimported = false, bool $onlydeleted = false ): array { $selects = []; $params = []; // Start with the filters. if ($onlydeleted) { // Only headless recordings when only deleted is set. $selects[] = "headless = :headless"; $params['headless'] = self::RECORDING_HEADLESS; } else if (!$includedeleted) { // Exclude headless recordings unless includedeleted. $selects[] = "headless != :headless"; $params['headless'] = self::RECORDING_HEADLESS; } if (!$includeimported) { // Exclude imported recordings unless includedeleted. $selects[] = "imported != :imported"; $params['imported'] = self::RECORDING_IMPORTED; } else if ($onlyimported) { // Exclude non-imported recordings. $selects[] = "imported = :imported"; $params['imported'] = self::RECORDING_IMPORTED; } // Now get only recordings that have been validated by recording ready callback. $selects[] = "status IN (:status_processed, :status_notified)"; $params['status_processed'] = self::RECORDING_STATUS_PROCESSED; $params['status_notified'] = self::RECORDING_STATUS_NOTIFIED; return [$selects, $params]; } /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return [ 'courseid' => [ 'type' => PARAM_INT, ], 'bigbluebuttonbnid' => [ 'type' => PARAM_INT, ], 'groupid' => [ 'type' => PARAM_INT, 'null' => NULL_ALLOWED, ], 'recordingid' => [ 'type' => PARAM_RAW, ], 'headless' => [ 'type' => PARAM_BOOL, ], 'imported' => [ 'type' => PARAM_BOOL, ], 'status' => [ 'type' => PARAM_INT, ], 'importeddata' => [ 'type' => PARAM_RAW, 'null' => NULL_ALLOWED, 'default' => '' ], 'name' => [ 'type' => PARAM_TEXT, 'null' => NULL_ALLOWED, 'default' => null ], 'description' => [ 'type' => PARAM_TEXT, 'null' => NULL_ALLOWED, 'default' => 0 ], 'protected' => [ 'type' => PARAM_BOOL, 'null' => NULL_ALLOWED, 'default' => null ], 'starttime' => [ 'type' => PARAM_INT, 'null' => NULL_ALLOWED, 'default' => null ], 'endtime' => [ 'type' => PARAM_INT, 'null' => NULL_ALLOWED, 'default' => null ], 'published' => [ 'type' => PARAM_BOOL, 'null' => NULL_ALLOWED, 'default' => null ], 'playbacks' => [ 'type' => PARAM_RAW, 'null' => NULL_ALLOWED, 'default' => null ], ]; } /** * Get the instance that this recording relates to. * * @return instance */ public function get_instance(): instance { if ($this->instance === null) { $this->instance = instance::get_from_instanceid($this->get('bigbluebuttonbnid')); } return $this->instance; } /** * Before doing the database update, let's check if we need to update metadata * * @return void */ protected function before_update() { // We update if the remote metadata has been changed locally. if ($this->metadatachanged && !$this->get('imported')) { $metadata = $this->fetch_metadata(); if ($metadata) { recording_proxy::update_recording( $this->get('recordingid'), $metadata ); } $this->metadatachanged = false; } } /** * Create a new imported recording from current recording * * @param instance $targetinstance * @return recording */ public function create_imported_recording(instance $targetinstance) { $recordingrec = $this->to_record(); $remotedata = $this->fetch_metadata(); unset($recordingrec->id); $recordingrec->bigbluebuttonbnid = $targetinstance->get_instance_id(); $recordingrec->courseid = $targetinstance->get_course_id(); $recordingrec->groupid = 0; // The recording is available to everyone. $recordingrec->importeddata = json_encode($remotedata); $recordingrec->imported = true; $recordingrec->headless = false; $importedrecording = new self(0, $recordingrec); $importedrecording->create(); return $importedrecording; } /** * Delete the recording in the BBB button * * @return void */ protected function before_delete() { $recordid = $this->get('recordingid'); if ($recordid && !$this->get('imported')) { recording_proxy::delete_recording($recordid); // Delete in cache if needed. $cachedrecordings = cache::make('mod_bigbluebuttonbn', 'recordings'); $cachedrecordings->delete($recordid); } } /** * Set name * * @param string $value */ protected function set_name($value) { $this->metadata_set('name', trim($value)); } /** * Set Description * * @param string $value */ protected function set_description($value) { $this->metadata_set('description', trim($value)); } /** * Recording is protected * * @param bool $value */ protected function set_protected($value) { $realvalue = $value ? "true" : "false"; $this->metadata_set('protected', $realvalue); recording_proxy::protect_recording($this->get('recordingid'), $realvalue); } /** * Recording starttime * * @param int $value */ protected function set_starttime($value) { $this->metadata_set('starttime', $value); } /** * Recording endtime * * @param int $value */ protected function set_endtime($value) { $this->metadata_set('endtime', $value); } /** * Recording is published * * @param bool $value */ protected function set_published($value) { $realvalue = $value ? "true" : "false"; $this->metadata_set('published', $realvalue); // Now set this flag onto the remote bbb server. recording_proxy::publish_recording($this->get('recordingid'), $realvalue); } /** * Update recording status * * @param bool $value */ protected function set_status($value) { $this->raw_set('status', $value); $this->update(); } /** * POSSIBLE_REMOTE_META_SOURCE match a field type and its metadataname (historical and current). */ const POSSIBLE_REMOTE_META_SOURCE = [ 'description' => ['meta_bbb-recording-description', 'meta_contextactivitydescription'], 'name' => ['meta_bbb-recording-name', 'meta_contextactivity', 'meetingName'], 'playbacks' => ['playbacks'], 'starttime' => ['startTime'], 'endtime' => ['endTime'], 'published' => ['published'], 'protected' => ['protected'], 'tags' => ['meta_bbb-recording-tags'] ]; /** * Get the real metadata name for the possible source. * * @param string $sourcetype the name of the source we look for (name, description...) * @param array $metadata current metadata */ protected function get_possible_meta_name_for_source($sourcetype, $metadata): string { $possiblesource = self::POSSIBLE_REMOTE_META_SOURCE[$sourcetype]; $possiblesourcename = $possiblesource[0]; foreach ($possiblesource as $possiblesname) { if (isset($meta[$possiblesname])) { $possiblesourcename = $possiblesname; } } return $possiblesourcename; } /** * Convert string (metadata) to json object * * @return mixed|null */ protected function remote_meta_convert() { $remotemeta = $this->raw_get('importeddata'); return json_decode($remotemeta, true); } /** * Description is stored in the metadata, so we sometimes needs to do some conversion. */ protected function get_description() { return trim($this->metadata_get('description')); } /** * Name is stored in the metadata */ protected function get_name() { return trim($this->metadata_get('name')); } /** * List of playbacks for this recording. * * @return array[] */ protected function get_playbacks() { if ($playbacks = $this->metadata_get('playbacks')) { return array_map(function (array $playback): array { $clone = array_merge([], $playback); $clone['url'] = new moodle_url('/mod/bigbluebuttonbn/bbb_view.php', [ 'action' => 'play', 'bn' => $this->raw_get('bigbluebuttonbnid'), 'rid' => $this->get('id'), 'rtype' => $clone['type'], ]); return $clone; }, $playbacks); } return []; } /** * Get the playback URL for the specified type. * * @param string $type * @return null|string */ public function get_remote_playback_url(string $type): ?string { $this->refresh_metadata_if_required(); $playbacks = $this->metadata_get('playbacks'); foreach ($playbacks as $playback) { if ($playback['type'] == $type) { return $playback['url']; } } return null; } /** * Is protected. Return null if protected is not implemented. * * @return bool|null */ protected function get_protected() { $protectedtext = $this->metadata_get('protected'); return is_null($protectedtext) ? null : $protectedtext === "true"; } /** * Start time * * @return mixed|null */ protected function get_starttime() { return $this->metadata_get('starttime'); } /** * Start time * * @return mixed|null */ protected function get_endtime() { return $this->metadata_get('endtime'); } /** * Is published * * @return bool */ protected function get_published() { $publishedtext = $this->metadata_get('published'); return $publishedtext === "true"; } /** * Set locally stored metadata from this instance * * @param string $fieldname * @param mixed $value */ protected function metadata_set($fieldname, $value) { // Can we can change the metadata on the imported record ? if ($this->get('imported')) { return; } $this->metadatachanged = true; $metadata = $this->fetch_metadata(); $possiblesourcename = $this->get_possible_meta_name_for_source($fieldname, $metadata); $metadata[$possiblesourcename] = $value; $this->metadata = $metadata; } /** * Get information stored in the recording metadata such as description, name and other info * * @param string $fieldname * @return mixed|null */ protected function metadata_get($fieldname) { $metadata = $this->fetch_metadata(); $possiblesourcename = $this->get_possible_meta_name_for_source($fieldname, $metadata); return $metadata[$possiblesourcename] ?? null; } /** * @var string Default sort for recordings when fetching from the database. */ const DEFAULT_RECORDING_SORT = 'timecreated ASC'; /** * Fetch all records which match the specified parameters, including all metadata that relates to them. * * @param array $selects * @param array $params * @return recording[] */ protected static function fetch_records(array $selects, array $params): array { global $DB, $CFG; $withindays = time() - (self::RECORDING_TIME_LIMIT_DAYS * DAYSECS); // Sort for recordings when fetching from the database. $recordingsort = $CFG->bigbluebuttonbn_recordings_asc_sort ? 'timecreated ASC' : 'timecreated DESC'; // Fetch the local data. Arbitrary sort by id, so we get the same result on different db engines. $recordings = $DB->get_records_select( static::TABLE, implode(" AND ", $selects), $params, $recordingsort ); // Grab the recording IDs. $recordingids = array_values(array_map(function ($recording) { return $recording->recordingid; }, $recordings)); // Fetch all metadata for these recordings. $metadatas = recording_proxy::fetch_recordings($recordingids); $failedids = recording_proxy::fetch_missing_recordings($recordingids); // Return the instances. return array_filter(array_map(function ($recording) use ($metadatas, $withindays, $failedids) { // Filter out if no metadata was fetched. if (!array_key_exists($recording->recordingid, $metadatas)) { // If the recording was successfully fetched, mark it as dismissed if it is older than 30 days. if (!in_array($recording->recordingid, $failedids) && $withindays > $recording->timecreated) { $recording = new self(0, $recording, null); $recording->set_status(self::RECORDING_STATUS_DISMISSED); } return false; } $metadata = $metadatas[$recording->recordingid]; // Filter out and mark it as deleted if it was deleted in BBB. if ($metadata['state'] == 'deleted') { $recording = new self(0, $recording, null); $recording->set_status(self::RECORDING_STATUS_DELETED); return false; } // Include it otherwise. return new self(0, $recording, $metadata); }, $recordings)); } /** * Fetch metadata * * If metadata has changed locally or if it an imported recording, nothing will be done. * * @param bool $force * @return array */ protected function fetch_metadata(bool $force = false): ?array { if ($this->metadata !== null && !$force) { // Metadata is already up-to-date. return $this->metadata; } if ($this->get('imported')) { $this->metadata = json_decode($this->get('importeddata'), true); } else { $this->metadata = recording_proxy::fetch_recording($this->get('recordingid')); } return $this->metadata; } /** * Refresh metadata if required. * * If this is a protected recording which whose data was not fetched in the current request, then the metadata will * be purged and refetched. This ensures that the url is safe for use with a protected recording. */ protected function refresh_metadata_if_required() { recording_proxy::purge_protected_recording($this->get('recordingid')); $this->fetch_metadata(true); } /** * Synchronise pending recordings from the server. * * This function should be called by the check_pending_recordings scheduled task. * * @param bool $dismissedonly fetch dismissed recording only */ public static function sync_pending_recordings_from_server(bool $dismissedonly = false): void { global $DB; $params = [ 'withindays' => time() - (self::RECORDING_TIME_LIMIT_DAYS * DAYSECS), ]; // Fetch the local data. if ($dismissedonly) { mtrace("=> Looking for any recording that has been 'dismissed' in the past " . self::RECORDING_TIME_LIMIT_DAYS . " days."); $select = 'status = :status_dismissed AND timemodified > :withindays'; $params['status_dismissed'] = self::RECORDING_STATUS_DISMISSED; } else { mtrace("=> Looking for any recording awaiting processing from the past " . self::RECORDING_TIME_LIMIT_DAYS . " days."); $select = '(status = :status_awaiting AND timecreated > :withindays) OR status = :status_reset'; $params['status_reset'] = self::RECORDING_STATUS_RESET; $params['status_awaiting'] = self::RECORDING_STATUS_AWAITING; } $recordings = $DB->get_records_select(static::TABLE, $select, $params, self::DEFAULT_RECORDING_SORT); // Sort by DEFAULT_RECORDING_SORT we get the same result on different db engines. $recordingcount = count($recordings); mtrace("=> Found {$recordingcount} recordings to query"); // Grab the recording IDs. $recordingids = array_map(function($recording) { return $recording->recordingid; }, $recordings); // Fetch all metadata for these recordings. mtrace("=> Fetching recording metadata from server"); $metadatas = recording_proxy::fetch_recordings($recordingids); $foundcount = 0; foreach ($metadatas as $recordingid => $metadata) { mtrace("==> Found metadata for {$recordingid}."); $id = array_search($recordingid, $recordingids); if (!$id) { // Recording was not found, skip. mtrace("===> Skip as fetched recording was not found."); continue; } // Recording was found, update status. mtrace("===> Update local cache as fetched recording was found."); $recording = new self(0, $recordings[$id], $metadata); $recording->set_status(self::RECORDING_STATUS_PROCESSED); $foundcount++; if (array_key_exists('breakouts', $metadata)) { // Iterate breakout recordings (if any) and update status. foreach ($metadata['breakouts'] as $breakoutrecordingid => $breakoutmetadata) { $breakoutrecording = self::get_record(['recordingid' => $breakoutrecordingid]); if (!$breakoutrecording) { $breakoutrecording = new recording(0, (object) [ 'courseid' => $recording->get('courseid'), 'bigbluebuttonbnid' => $recording->get('bigbluebuttonbnid'), 'groupid' => $recording->get('groupid'), 'recordingid' => $breakoutrecordingid ], $breakoutmetadata); $breakoutrecording->create(); } $breakoutrecording->set_status(self::RECORDING_STATUS_PROCESSED); $foundcount++; } } } mtrace("=> Finished processing recordings. Updated status for {$foundcount} / {$recordingcount} recordings."); } } PK\C{mmclasses/form/guest_add.phpnu[. namespace mod_bigbluebuttonbn\form; use context; use core_form\dynamic_form; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\local\exceptions\bigbluebutton_exception; use mod_bigbluebuttonbn\task\send_guest_emails; use moodle_exception; use moodle_url; use MoodleQuickForm; /** * Popup form to add new guests to a meeting and show/copy credential to access the guest login page. * * @package mod_bigbluebuttonbn * @copyright 2022 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent [at] call-learning [dt] fr) */ class guest_add extends dynamic_form { /** * Max length for credential and url fields. */ const MAX_INPUT_LENGTH = 35; /** * Process the form submission, used if form was submitted via AJAX. * * @return array */ public function process_dynamic_submission(): array { global $USER; $data = $this->get_data(); $allmails = []; if (!empty($data->emails)) { $emails = explode(',', $data->emails); foreach ($emails as $email) { $email = trim($email); if (validate_email($email)) { $allmails[] = $email; } } $adhoctask = new send_guest_emails(); $adhoctask->set_custom_data( [ 'emails' => $allmails, 'useridfrom' => $USER->id ] ); $adhoctask->set_instance_id($data->id); \core\task\manager::queue_adhoc_task($adhoctask); } return [ 'result' => true, 'emails' => join(', ', $allmails), 'emailcount' => count($allmails), 'errors' => '' ]; } /** * Perform some validation. * * @param array $formdata * @param array $files * @return array */ public function validation($formdata, $files): array { $errors = []; $emailserrors = []; if (!empty($formdata['emails'])) { $emails = explode(',', $formdata['emails']); foreach ($emails as $email) { $email = trim($email); if (!validate_email($email)) { $emailserrors[] .= get_string('guestaccess_emails_invalidemail', 'mod_bigbluebuttonbn', $email); } } } if (!empty($emailserrors)) { $errors['emails'] = \html_writer::alist($emailserrors); } return $errors; } /** * Load in existing data as form defaults (not applicable). * * @return void */ public function set_data_for_dynamic_submission(): void { $instance = $this->get_instance_from_params(); $data = [ 'id' => $instance->get_instance_id(), 'groupid' => $instance->get_group_id(), 'guestjoinurl' => $instance->get_guest_access_url(), 'guestpassword' => $instance->get_guest_access_password(), ]; $this->set_data($data); } /** * Get BigblueButton instance from context params * * @return instance * @throws moodle_exception */ protected function get_instance_from_params(): instance { $bbid = $this->optional_param('id', null, PARAM_INT); $groupid = $this->optional_param('groupid', null, PARAM_INT); if (empty($bbid)) { throw new moodle_exception('guestaccess_add_no_id', 'mod_bigbluebuttonbn'); } $instance = instance::get_from_instanceid($bbid); if ($groupid) { $instance->set_group_id($groupid); } return $instance; } /** * Form definition */ protected function definition() { self::add_meeting_links_elements($this->_form); $mform = $this->_form; $mform->addElement('text', 'emails', get_string('guestaccess_emails', 'mod_bigbluebuttonbn'), ); $mform->addHelpButton('emails', 'guestaccess_emails', 'mod_bigbluebuttonbn'); $mform->setDefault('emails', ''); $mform->setType('emails', PARAM_RAW); $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); $mform->addElement('hidden', 'groupid'); $mform->setType('groupid', PARAM_INT); } /** * Add meeting links element. Helper for this form and the mod_form (module form) * * @param MoodleQuickForm $mform * @return void */ public static function add_meeting_links_elements(MoodleQuickForm &$mform): void { global $CFG; MoodleQuickForm::registerElementType('text_with_copy', "$CFG->dirroot/mod/bigbluebuttonbn/classes/form/text_with_copy_element.php", text_with_copy_element::class); $mform->addElement('text_with_copy', 'guestjoinurl', get_string('guestaccess_meeting_link', 'mod_bigbluebuttonbn'), [ 'copylabel' => get_string('guestaccess_copy_link', 'mod_bigbluebuttonbn'), 'size' => self::MAX_INPUT_LENGTH, 'readonly' => 'readonly' ] ); $mform->setType('guestjoinurl', PARAM_URL); $mform->addElement('text_with_copy', 'guestpassword', get_string('guestaccess_meeting_password', 'mod_bigbluebuttonbn'), [ 'copylabel' => get_string('guestaccess_copy_password', 'mod_bigbluebuttonbn'), 'readonly' => 'readonly', 'size' => self::MAX_INPUT_LENGTH, ] ); $mform->setType('guestpassword', PARAM_RAW); } /** * Check if current user has access to this form, otherwise throw exception. * * @return void * @throws moodle_exception */ protected function check_access_for_dynamic_submission(): void { $context = $this->get_context_for_dynamic_submission(); $instance = instance::get_from_cmid($context->instanceid); if (!$instance->is_moderator()) { throw new \restricted_context_exception(); } } /** * Return form context * * @return context */ protected function get_context_for_dynamic_submission(): context { $instance = $this->get_instance_from_params(); return $instance->get_context(); } /** * Returns url to set in $PAGE->set_url() when form is being rendered or submitted via AJAX. * * @return moodle_url */ protected function get_page_url_for_dynamic_submission(): moodle_url { $context = $this->get_context_for_dynamic_submission(); return new moodle_url('/mod/bigbluebuttonbn/view.php', ['id' => $context->instanceid]); } } PK\Ť classes/form/guest_login.phpnu[. namespace mod_bigbluebuttonbn\form; defined('MOODLE_INTERNAL') || die; global $CFG; require_once($CFG->libdir . '/formslib.php'); /** * Guest login form. * * @package mod_bigbluebuttonbn * @copyright 2022 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent [at] call-learning [dt] fr) */ class guest_login extends \moodleform { /** * Form definition */ protected function definition() { global $USER; $mform = $this->_form; $mform->addElement('text', 'username', get_string('guestaccess_username', 'mod_bigbluebuttonbn')); $mform->setType('username', PARAM_NOTAGS); $mform->addRule('username', get_string('required'), 'required', null, 'client'); if (isloggedin() && !isguestuser()) { $mform->setConstant('username', fullname($USER)); $mform->freeze('username'); } $mform->addElement('password', 'password', get_string('guestaccess_password', 'mod_bigbluebuttonbn')); $mform->setType('password', PARAM_RAW); $mform->addRule('password', get_string('required'), 'required', null, 'client'); $mform->addElement('hidden', 'uid', $this->_customdata['uid']); $mform->setType('uid', PARAM_ALPHANUMEXT); $this->add_action_buttons(false, get_string('guestaccess_join_meeting', 'mod_bigbluebuttonbn')); } /** * Validate form * * @param array $data * @param array $files * @return array * @throws \coding_exception */ public function validation($data, $files): array { $errors = parent::validation($data, $files); $instance = $this->_customdata['instance']; if ($data['password'] != $instance->get_guest_access_password()) { $errors['password'] = get_string('guestaccess_meeting_invalid_password', 'mod_bigbluebuttonbn'); } return $errors; } } PK\q7'classes/form/text_with_copy_element.phpnu[. namespace mod_bigbluebuttonbn\form; use MoodleQuickForm_text; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once("$CFG->libdir/form/text.php"); /** * Text type form element with a copy widget * * Contains HTML class for a text type element and a link that will copy its content in the copy/paste buffer * * @package mod_bigbluebuttonbn * @copyright 2022 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent [at] call-learning [dt] fr) */ class text_with_copy_element extends MoodleQuickForm_text { /** @var string an element template. */ public $_groupElementTemplate; /** * Accepts a renderer * * @param object $renderer An HTML_QuickForm_Renderer object * @param bool $required Whether an element is required * @param string $error An error message associated with an element * @return void */ public function accept(&$renderer, $required = false, $error = null) { global $OUTPUT; $elementname = $this->getName(); // Make sure the element has an id. $this->_generateId(); $advanced = isset($renderer->_advancedElements[$elementname]); $elementcontext = $this->export_for_template($OUTPUT); $helpbutton = ''; if (method_exists($this, 'getHelpButton')) { $helpbutton = $this->getHelpButton(); } $label = $this->getLabel(); $text = ''; if (method_exists($this, 'getText')) { // There currently exists code that adds a form element with an empty label. // If this is the case then set the label to the description. if (empty($label)) { $label = $this->getText(); } else { $text = $this->getText(); } } $context = array( 'element' => $elementcontext, 'label' => $label, 'text' => $text, 'required' => $required, 'advanced' => $advanced, 'helpbutton' => $helpbutton, 'error' => $error, 'copylabel' => $this->_attributes['copylabel'] ?? get_string('copy', 'core_editor') ); $html = $OUTPUT->render_from_template('mod_bigbluebuttonbn/element_text_with_copy', $context); if ($renderer->_inGroup) { $this->_groupElementTemplate = $html; } if (($renderer->_inGroup) && !empty($renderer->_groupElementTemplate)) { $renderer->_groupElementTemplate = $html; } else if (!isset($renderer->_templates[$elementname])) { $renderer->_templates[$elementname] = $html; } if (in_array($elementname, $renderer->_stopFieldsetElements) && $renderer->_fieldsetsOpen > 0) { $renderer->_html .= $renderer->_closeFieldsetTemplate; $renderer->_fieldsetsOpen--; } $renderer->_html .= $html; } } PK\v)classes/external/view_bigbluebuttonbn.phpnu[. namespace mod_bigbluebuttonbn\external; use context_module; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; use mod_bigbluebuttonbn\instance; /** * External service to trigger the course module viewed event and update the module completion status * * This is mainly used by the mobile application. * * @package mod_bigbluebuttonbn * @category external * @copyright 2018 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class view_bigbluebuttonbn extends external_api { /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.0 */ public static function execute_parameters() { return new external_function_parameters([ 'bigbluebuttonbnid' => new external_value(PARAM_INT, 'bigbluebuttonbn instance id'), ] ); } /** * Trigger the course module viewed event and update the module completion status. * * @param int $instanceid the bigbluebuttonbn instance id * @return array of warnings and status result * @since Moodle 3.0 */ public static function execute($instanceid) { global $CFG; require_once($CFG->dirroot . "/mod/bigbluebuttonbn/lib.php"); ['bigbluebuttonbnid' => $instanceid] = self::validate_parameters( self::execute_parameters(), ['bigbluebuttonbnid' => $instanceid] ); $instance = instance::get_from_instanceid($instanceid); if (empty($instance)) { return [ 'status' => false, 'warnings' => [ [ 'item' => 'mod_bigbluebuttonbn', 'itemid' => 0, 'warningcode' => 'nosuchinstance', 'message' => get_string('nosuchinstance', 'mod_bigbluebuttonbn', (object) ['id' => $instanceid, 'entity' => 'bigbluebuttonbn']) ] ] ]; } $context = context_module::instance($instance->get_cm_id()); self::validate_context($context); require_capability('mod/bigbluebuttonbn:view', $context); // Call the bigbluebuttonbn/lib API. bigbluebuttonbn_view($instance->get_instance_data(), $instance->get_course(), $instance->get_cm(), $context); return [ 'status' => true, 'warnings' => [] ]; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.0 */ public static function execute_returns() { return new external_single_structure([ 'status' => new external_value(PARAM_BOOL, 'status: true if success'), 'warnings' => new external_warnings() ] ); } } PK\^E!classes/external/meeting_info.phpnu[. namespace mod_bigbluebuttonbn\external; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; use core_external\restricted_context_exception; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; use mod_bigbluebuttonbn\meeting; /** * External service to fetch meeting information. * * @package mod_bigbluebuttonbn * @category external * @copyright 2018 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class meeting_info extends external_api { /** * Returns description of method parameters. * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters([ 'bigbluebuttonbnid' => new external_value(PARAM_INT, 'bigbluebuttonbn instance id'), 'groupid' => new external_value(PARAM_INT, 'bigbluebuttonbn group id', VALUE_DEFAULT, 0), 'updatecache' => new external_value(PARAM_BOOL, 'update cache ?', VALUE_DEFAULT, false), ]); } /** * Fetch meeting information. * * @param int $bigbluebuttonbnid the bigbluebuttonbn instance id * @param int $groupid * @param bool $updatecache * @return array * @throws \moodle_exception * @throws restricted_context_exception */ public static function execute( int $bigbluebuttonbnid, int $groupid, bool $updatecache = false ): array { // Validate the bigbluebuttonbnid ID. [ 'bigbluebuttonbnid' => $bigbluebuttonbnid, 'groupid' => $groupid, 'updatecache' => $updatecache, ] = self::validate_parameters(self::execute_parameters(), [ 'bigbluebuttonbnid' => $bigbluebuttonbnid, 'groupid' => $groupid, 'updatecache' => $updatecache, ]); // Fetch the session, features, and profile. $instance = instance::get_from_instanceid($bigbluebuttonbnid); $instance->set_group_id($groupid); if (!groups_group_visible($groupid, $instance->get_course(), $instance->get_cm())) { throw new restricted_context_exception(); } $context = $instance->get_context(); // Validate that the user has access to this activity and to manage recordings. self::validate_context($context); // Check if the BBB server is working. $serverversion = bigbluebutton_proxy::get_server_version(); if ($serverversion === null) { throw new \moodle_exception('general_error_no_answer', 'mod_bigbluebuttonbn', bigbluebutton_proxy::get_server_not_available_url($instance), bigbluebutton_proxy::get_server_not_available_message($instance)); } $meetinginfo = (array) meeting::get_meeting_info_for_instance($instance, $updatecache); // Make the structure WS friendly. array_walk($meetinginfo['features'], function(&$value, $key){ $value = ['name' => $key, 'isenabled' => (bool) $value]; }); return $meetinginfo; } /** * Describe the return structure of the external service. * * @return external_single_structure * @since Moodle 3.0 */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'cmid' => new external_value(PARAM_INT, 'CM id'), 'userlimit' => new external_value(PARAM_INT, 'User limit'), 'bigbluebuttonbnid' => new external_value(PARAM_RAW, 'bigbluebuttonbn instance id'), 'groupid' => new external_value(PARAM_INT, 'bigbluebuttonbn group id', VALUE_DEFAULT, 0), 'meetingid' => new external_value(PARAM_RAW, 'Meeting id'), 'openingtime' => new external_value(PARAM_INT, 'Opening time', VALUE_OPTIONAL), 'closingtime' => new external_value(PARAM_INT, 'Closing time', VALUE_OPTIONAL), 'statusrunning' => new external_value(PARAM_BOOL, 'Status running', VALUE_OPTIONAL), 'statusclosed' => new external_value(PARAM_BOOL, 'Status closed', VALUE_OPTIONAL), 'statusopen' => new external_value(PARAM_BOOL, 'Status open', VALUE_OPTIONAL), 'statusmessage' => new external_value(PARAM_TEXT, 'Status message', VALUE_OPTIONAL), 'startedat' => new external_value(PARAM_INT, 'Started at', VALUE_OPTIONAL), 'moderatorcount' => new external_value(PARAM_INT, 'Moderator count', VALUE_OPTIONAL), 'participantcount' => new external_value(PARAM_INT, 'Participant count', VALUE_OPTIONAL), 'moderatorplural' => new external_value(PARAM_BOOL, 'Several moderators ?', VALUE_OPTIONAL), 'participantplural' => new external_value(PARAM_BOOL, 'Several participants ?', VALUE_OPTIONAL), 'canjoin' => new external_value(PARAM_BOOL, 'Can join'), 'ismoderator' => new external_value(PARAM_BOOL, 'Is moderator'), 'presentations' => new external_multiple_structure( new external_single_structure([ 'url' => new external_value(PARAM_URL, 'presentation URL'), 'iconname' => new external_value(PARAM_RAW, 'icon name'), 'icondesc' => new external_value(PARAM_TEXT, 'icon text'), 'name' => new external_value(PARAM_TEXT, 'presentation name'), ]) ), 'joinurl' => new external_value(PARAM_URL, 'Join URL'), 'guestaccessenabled' => new external_value(PARAM_BOOL, 'Guest access enabled', VALUE_OPTIONAL), 'guestjoinurl' => new external_value(PARAM_URL, 'Guest URL', VALUE_OPTIONAL), 'guestpassword' => new external_value(PARAM_RAW, 'Guest join password', VALUE_OPTIONAL), 'showpresentations' => new external_value(PARAM_BOOL, 'Show presentation file', VALUE_OPTIONAL), 'features' => new external_multiple_structure( new external_single_structure([ 'name' => new external_value(PARAM_ALPHA, 'Feature name.'), 'isenabled' => new external_value(PARAM_BOOL, 'Whether the feature is enabled.'), ]), 'List of features for the instance', VALUE_OPTIONAL ), ] ); } } PK\b##-classes/external/get_recordings_to_import.phpnu[. namespace mod_bigbluebuttonbn\external; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\local\bigbluebutton\recordings\recording_data; use mod_bigbluebuttonbn\recording; /** * External service to fetch a list of recordings from the BBB service. * * @package mod_bigbluebuttonbn * @category external * @copyright 2018 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class get_recordings_to_import extends external_api { /** * Returns description of method parameters * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters([ 'destinationinstanceid' => new external_value( PARAM_INT, 'Id of the other BBB we target for importing recordings into. The idea here is to remove already imported recordings.', VALUE_REQUIRED ), 'sourcebigbluebuttonbnid' => new external_value(PARAM_INT, 'bigbluebuttonbn instance id', VALUE_DEFAULT, 0), 'sourcecourseid' => new external_value(PARAM_INT, 'source courseid to filter by', VALUE_DEFAULT, 0), 'tools' => new external_value(PARAM_RAW, 'a set of enabled tools', VALUE_DEFAULT, 'protect,unprotect,publish,unpublish,delete'), 'groupid' => new external_value(PARAM_INT, 'Group ID', VALUE_DEFAULT, null), ]); } /** * Get a list of recordings * * @param int $destinationinstanceid the bigbluebuttonbn instance id where recordings have been already imported. * @param int|null $sourcebigbluebuttonbnid the bigbluebuttonbn instance id to which the recordings are referred. * @param int|null $sourcecourseid the source courseid to filter by * @param string|null $tools * @param int|null $groupid * @return array of warnings and status result * @throws \invalid_parameter_exception * @throws \restricted_context_exception */ public static function execute( int $destinationinstanceid, ?int $sourcebigbluebuttonbnid = 0, ?int $sourcecourseid = 0, ?string $tools = 'protect,unprotect,publish,unpublish,delete', ?int $groupid = null ): array { global $USER, $DB; $returnval = [ 'status' => false, 'warnings' => [], ]; // Validate the sourcebigbluebuttonbnid ID. [ 'destinationinstanceid' => $destinationinstanceid, 'sourcebigbluebuttonbnid' => $sourcebigbluebuttonbnid, 'sourcecourseid' => $sourcecourseid, 'tools' => $tools, 'groupid' => $groupid ] = self::validate_parameters(self::execute_parameters(), [ 'destinationinstanceid' => $destinationinstanceid, 'sourcebigbluebuttonbnid' => $sourcebigbluebuttonbnid, 'sourcecourseid' => $sourcecourseid, 'tools' => $tools, 'groupid' => $groupid ]); $tools = explode(',', $tools ?? 'protect,unprotect,publish,unpublish,delete'); // Fetch the session, features, and profile. $sourceinstance = null; $sourcecourse = null; if ($sourcecourseid) { $sourcecourse = $DB->get_record('course', ['id' => $sourcecourseid], '*', MUST_EXIST); } if (!empty($sourcebigbluebuttonbnid)) { $sourceinstance = instance::get_from_instanceid($sourcebigbluebuttonbnid); if (!$sourceinstance) { throw new \invalid_parameter_exception('Source Bigbluebutton Id is invalid'); } $sourcecourse = $sourceinstance->get_course(); // Validate that the user has access to this activity. self::validate_context($sourceinstance->get_context()); } $destinstance = instance::get_from_instanceid($destinationinstanceid); // Validate that the user has access to this activity. self::validate_context($destinstance->get_context()); if (!$destinstance->user_has_group_access($USER, $groupid)) { throw new \invalid_parameter_exception('Invalid group for this user ' . $groupid); } if ($groupid) { $destinstance->set_group_id($groupid); } // Exclude itself from the list if in import mode. $excludedids = [$destinstance->get_instance_id()]; if ($sourceinstance) { $recordings = $sourceinstance->get_recordings($excludedids); } else { // There is a course id or a 0, so we fetch all recording including deleted recordings from this course. $recordings = recording::get_recordings_for_course( $sourcecourseid, $excludedids, true, false, ($sourcecourseid == 0 || $sourcebigbluebuttonbnid == 0), ($sourcecourseid == 0 || $sourcebigbluebuttonbnid == 0) ); } if ($destinationinstanceid) { // Remove recording already imported in this specific activity. $destinationinstance = instance::get_from_instanceid($destinationinstanceid); $importedrecordings = recording::get_recordings_for_instance( $destinationinstance, true, true ); // Unset from $recordings if recording is already imported. // Recording $recordings are indexed by $id (moodle table column id). foreach ($recordings as $index => $recording) { foreach ($importedrecordings as $irecord) { if ($irecord->get('recordingid') == $recording->get('recordingid')) { unset($recordings[$index]); } } } } $tabledata = recording_data::get_recording_table($recordings, $tools, $sourceinstance, $sourcecourseid); $returnval['tabledata'] = $tabledata; $returnval['status'] = true; return $returnval; } /** * Describe the return structure of the external service. * * @return external_single_structure * @since Moodle 3.0 */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'status' => new external_value(PARAM_BOOL, 'Whether the fetch was successful'), 'tabledata' => new external_single_structure([ 'activity' => new external_value(PARAM_ALPHANUMEXT), 'ping_interval' => new external_value(PARAM_INT), 'locale' => new external_value(PARAM_TEXT), 'profile_features' => new external_multiple_structure(new external_value(PARAM_TEXT)), 'columns' => new external_multiple_structure(new external_single_structure([ 'key' => new external_value(PARAM_ALPHA), 'label' => new external_value(PARAM_TEXT), 'width' => new external_value(PARAM_ALPHANUMEXT), // See https://datatables.net/reference/option/columns.type . 'type' => new external_value(PARAM_ALPHANUMEXT, 'Column type', VALUE_OPTIONAL), 'sortable' => new external_value(PARAM_BOOL, 'Whether this column is sortable', VALUE_OPTIONAL, false), 'allowHTML' => new external_value(PARAM_BOOL, 'Whether this column contains HTML', VALUE_OPTIONAL, false), 'formatter' => new external_value(PARAM_ALPHANUMEXT, 'Formatter name', VALUE_OPTIONAL), ])), 'data' => new external_value(PARAM_RAW), // For now it will be json encoded. ], '', VALUE_OPTIONAL), 'warnings' => new external_warnings() ]); } } PK\h classes/external/can_join.phpnu[. namespace mod_bigbluebuttonbn\external; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_single_structure; use core_external\external_value; use core_external\restricted_context_exception; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\meeting; /** * External service to check whether a user can join a meeting. * * This is mainly used by the mobile application. * * @package mod_bigbluebuttonbn * @category external * @copyright 2018 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class can_join extends external_api { /** * Returns description of method 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), 'groupid' => new external_value(PARAM_INT, 'bigbluebuttonbn group id', VALUE_DEFAULT, 0), ]); } /** * Updates a recording * * @param int $cmid the bigbluebuttonbn course module id * @param null|int $groupid * @return array (empty array for now) * @throws \restricted_context_exception */ public static function execute( int $cmid, ?int $groupid = 0 ): array { // Validate the cmid ID. [ 'cmid' => $cmid, 'groupid' => $groupid, ] = self::validate_parameters(self::execute_parameters(), [ 'cmid' => $cmid, 'groupid' => $groupid, ]); $result = [ 'can_join' => false, 'cmid' => $cmid, ]; $instance = instance::get_from_cmid($cmid); if (empty($instance)) { return $result; } // Validate the groupid. if (!groups_group_visible($groupid, $instance->get_course(), $instance->get_cm())) { throw new restricted_context_exception(); } $instance->set_group_id($groupid); self::validate_context($instance->get_context()); $meeting = new meeting($instance); $result['can_join'] = $meeting->can_join(); return $result; } /** * Describe the return structure of the external service. * * @return external_single_structure * @since Moodle 3.3 */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'can_join' => new external_value(PARAM_BOOL, 'Can join session'), 'cmid' => new external_value(PARAM_INT, 'course module id', VALUE_REQUIRED), ]); } } PK\lj classes/external/end_meeting.phpnu[. namespace mod_bigbluebuttonbn\external; use core\notification; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; use core_external\restricted_context_exception; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\local\exceptions\bigbluebutton_exception; use mod_bigbluebuttonbn\logger; use mod_bigbluebuttonbn\meeting; /** * External service to end a meeting. * * @package mod_bigbluebuttonbn * @category external * @copyright 2018 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class end_meeting extends external_api { /** * Returns description of method parameters * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters([ 'bigbluebuttonbnid' => new external_value(PARAM_INT, 'bigbluebuttonbn instance id'), 'groupid' => new external_value(PARAM_INT, 'bigbluebuttonbn group id', VALUE_DEFAULT, 0), ]); } /** * Updates a recording * * @param int $bigbluebuttonbnid the bigbluebuttonbn instance id * @param int $groupid the groupid (either 0 or the groupid) * @return array (empty array for now) * @throws \invalid_parameter_exception * @throws \moodle_exception * @throws restricted_context_exception */ public static function execute( int $bigbluebuttonbnid, int $groupid ): array { // Validate the bigbluebuttonbnid ID. [ 'bigbluebuttonbnid' => $bigbluebuttonbnid, 'groupid' => $groupid, ] = self::validate_parameters(self::execute_parameters(), [ 'bigbluebuttonbnid' => $bigbluebuttonbnid, 'groupid' => $groupid, ]); // Fetch the session, features, and profile. $instance = instance::get_from_instanceid($bigbluebuttonbnid); if (empty($instance)) { throw new \moodle_exception('Unknown Instance'); } if (!groups_group_visible($groupid, $instance->get_course(), $instance->get_cm())) { throw new restricted_context_exception(); } $instance->set_group_id($groupid); $context = $instance->get_context(); // Validate that the user has access to this activity and to manage recordings. self::validate_context($context); if (!$instance->user_can_end_meeting()) { throw new restricted_context_exception(); } // Execute the end command. $meeting = new meeting($instance); try { $meeting->end_meeting(); } catch (bigbluebutton_exception $e) { return [ 'warnings' => [ [ 'item' => $instance->get_meeting_name(), 'itemid' => $instance->get_instance_id(), 'warningcode' => 'notFound', 'message' => $e->getMessage() ] ] ]; } logger::log_meeting_ended_event($instance); // Update the cache. $meeting->update_cache(); notification::add(get_string('end_session_notification', 'mod_bigbluebuttonbn'), notification::INFO); return []; } /** * Describe the return structure of the external service. * * @return external_single_structure * @since Moodle 3.0 */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'warnings' => new external_warnings(), ]); } } PK\|ZZ!classes/external/get_join_url.phpnu[. namespace mod_bigbluebuttonbn\external; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; use core_external\restricted_context_exception; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\local\exceptions\meeting_join_exception; use mod_bigbluebuttonbn\meeting; /** * External service to create the meeting (if needed), check user limit, and return the join URL when we can join. * * This is mainly used by the mobile application. * * @package mod_bigbluebuttonbn * @category external * @copyright 2018 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class get_join_url extends external_api { /** * Returns description of method 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), 'groupid' => new external_value(PARAM_INT, 'bigbluebuttonbn group id', VALUE_DEFAULT, 0), ]); } /** * Updates a recording * * @param int $cmid the bigbluebuttonbn course module id * @param null|int $groupid * @return array (empty array for now) * * @throws restricted_context_exception */ public static function execute( int $cmid, ?int $groupid = 0 ): array { // Validate the cmid ID. [ 'cmid' => $cmid, 'groupid' => $groupid, ] = self::validate_parameters(self::execute_parameters(), [ 'cmid' => $cmid, 'groupid' => $groupid, ]); $result = ['warnings' => []]; $instance = instance::get_from_cmid($cmid); if (empty($instance)) { throw new \moodle_exception('nosuchinstance', 'mod_bigbluebuttonbn', null, ['entity' => get_string('module', 'course'), 'id' => $cmid]); } // Validate the groupid. if (!groups_group_visible($groupid, $instance->get_course(), $instance->get_cm())) { throw new restricted_context_exception(); } $instance->set_group_id($groupid); // Validate that the user has access to this activity and to join the meeting. self::validate_context($instance->get_context()); if (!$instance->can_join()) { throw new restricted_context_exception(); } try { $result['join_url'] = meeting::join_meeting($instance); } catch (meeting_join_exception $e) { $result['warnings'][] = [ 'item' => 'mod_bigbluebuttonbn', 'itemid' => $instance->get_instance_id(), 'warningcode' => $e->errorcode, 'message' => $e->getMessage() ]; } return $result; } /** * Describe the return structure of the external service. * * @return external_single_structure * @since Moodle 3.3 */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'join_url' => new external_value(PARAM_RAW, 'Can join session', VALUE_OPTIONAL), 'warnings' => new external_warnings(), ]); } } PK\P<#classes/external/get_recordings.phpnu[. namespace mod_bigbluebuttonbn\external; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; use core_external\restricted_context_exception; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\local\bigbluebutton\recordings\recording_data; use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; /** * External service to fetch a list of recordings from the BBB service. * * @package mod_bigbluebuttonbn * @category external * @copyright 2018 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class get_recordings extends external_api { /** * Returns description of method parameters * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters([ 'bigbluebuttonbnid' => new external_value(PARAM_INT, 'bigbluebuttonbn instance id'), 'tools' => new external_value(PARAM_RAW, 'a set of enabled tools', VALUE_DEFAULT, 'protect,unprotect,publish,unpublish,delete'), 'groupid' => new external_value(PARAM_INT, 'Group ID', VALUE_DEFAULT, null), ]); } /** * Get a list of recordings * * @param int $bigbluebuttonbnid the bigbluebuttonbn instance id to which the recordings are referred. * @param string|null $tools * @param int|null $groupid * @return array of warnings and status result * @throws \invalid_parameter_exception * @throws restricted_context_exception */ public static function execute( int $bigbluebuttonbnid = 0, ?string $tools = 'protect,unprotect,publish,unpublish,delete', ?int $groupid = null ): array { global $USER; $returnval = [ 'status' => false, 'warnings' => [], ]; // Validate the bigbluebuttonbnid ID. [ 'bigbluebuttonbnid' => $bigbluebuttonbnid, 'tools' => $tools, 'groupid' => $groupid, ] = self::validate_parameters(self::execute_parameters(), [ 'bigbluebuttonbnid' => $bigbluebuttonbnid, 'tools' => $tools, 'groupid' => $groupid, ]); $tools = explode(',', $tools ?? 'protect,unprotect,publish,unpublish,delete'); // Fetch the session, features, and profile. $instance = instance::get_from_instanceid($bigbluebuttonbnid); if (!$instance) { $returnval['warnings'][] = [ 'item' => $bigbluebuttonbnid, 'warningcode' => 'nosuchinstance', 'message' => get_string('nosuchinstance', 'mod_bigbluebuttonbn', (object) ['id' => $bigbluebuttonbnid, 'entity' => 'bigbluebuttonbn']) ]; } else { $typeprofiles = bigbluebutton_proxy::get_instance_type_profiles(); $profilefeature = $typeprofiles[$instance->get_type()]['features']; $showrecordings = in_array('all', $profilefeature) || in_array('showrecordings', $profilefeature); if ($showrecordings) { $context = $instance->get_context(); // Validate that the user has access to this activity. self::validate_context($context); if (!$instance->user_has_group_access($USER, $groupid)) { new restricted_context_exception(); } if ($groupid) { $instance->set_group_id($groupid); } $recordings = $instance->get_recordings([], $instance->get_instance_var('recordings_deleted') ?? false); $tabledata = recording_data::get_recording_table($recordings, $tools, $instance); $returnval['tabledata'] = $tabledata; $returnval['status'] = true; } else { $returnval['warnings'][] = [ 'item' => $bigbluebuttonbnid, 'warningcode' => 'instanceprofilewithoutrecordings', 'message' => get_string('instanceprofilewithoutrecordings', 'mod_bigbluebuttonbn') ]; } } return $returnval; } /** * Describe the return structure of the external service. * * @return external_single_structure * @since Moodle 3.0 */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'status' => new external_value(PARAM_BOOL, 'Whether the fetch was successful'), 'tabledata' => new external_single_structure([ 'activity' => new external_value(PARAM_ALPHANUMEXT), 'ping_interval' => new external_value(PARAM_INT), 'locale' => new external_value(PARAM_TEXT), 'profile_features' => new external_multiple_structure(new external_value(PARAM_TEXT)), 'columns' => new external_multiple_structure(new external_single_structure([ 'key' => new external_value(PARAM_ALPHA), 'label' => new external_value(PARAM_TEXT), 'width' => new external_value(PARAM_ALPHANUMEXT), // See https://datatables.net/reference/option/columns.type . 'type' => new external_value(PARAM_ALPHANUMEXT, 'Column type', VALUE_OPTIONAL), 'sortable' => new external_value(PARAM_BOOL, 'Whether this column is sortable', VALUE_OPTIONAL, false), 'allowHTML' => new external_value(PARAM_BOOL, 'Whether this column contains HTML', VALUE_OPTIONAL, false), 'formatter' => new external_value(PARAM_ALPHANUMEXT, 'Formatter name', VALUE_OPTIONAL), ])), 'data' => new external_value(PARAM_RAW), // For now it will be json encoded. ], '', VALUE_OPTIONAL), 'warnings' => new external_warnings() ]); } } PK\KOO(classes/external/completion_validate.phpnu[. namespace mod_bigbluebuttonbn\external; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; /** * External service to validate completion. * * @package mod_bigbluebuttonbn * @category external * @copyright 2018 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class completion_validate extends external_api { /** * Returns description of method parameters * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters([ 'bigbluebuttonbnid' => new external_value(PARAM_INT, 'bigbluebuttonbn instance id'), ]); } /** * Mark activity as complete * * @param int $bigbluebuttonbnid the bigbluebuttonbn instance id * @return array (empty array for now) */ public static function execute( int $bigbluebuttonbnid ): array { // Validate the bigbluebuttonbnid ID. [ 'bigbluebuttonbnid' => $bigbluebuttonbnid, ] = self::validate_parameters(self::execute_parameters(), [ 'bigbluebuttonbnid' => $bigbluebuttonbnid, ]); $result = ['warnings' => []]; // Fetch the session, features, and profile. $instance = instance::get_from_instanceid($bigbluebuttonbnid); if ($instance) { $context = $instance->get_context(); // Validate that the user has access to this activity. self::validate_context($context); // Get list with all the users enrolled in the course. [$sort, $sqlparams] = users_order_by_sql('u'); if (has_capability('moodle/course:update', $context)) { $users = get_enrolled_users($context, 'mod/bigbluebuttonbn:view', 0, 'u.*', $sort); foreach ($users as $user) { // Enqueue a task for processing the completion. bigbluebutton_proxy::enqueue_completion_event($instance->get_instance_data(), $user->id); } } else { $result['warnings'][] = [ 'item' => 'mod_bigbluebuttonbn', 'itemid' => $instance->get_instance_id(), 'warningcode' => 'nopermissions', 'message' => get_string('nopermissions', 'error', 'completion_validate') ]; } } else { $result['warnings'][] = [ 'item' => 'mod_bigbluebuttonbn', 'itemid' => $bigbluebuttonbnid, 'warningcode' => 'indexerrorbbtn', 'message' => get_string('index_error_bbtn', 'mod_bigbluebuttonbn', $bigbluebuttonbnid) ]; } // We might want to return a status here or some warnings. return $result; } /** * Describe the return structure of the external service. * * @return external_single_structure * @since Moodle 3.0 */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'warnings' => new external_warnings(), ]); } } PK\*cO%%%classes/external/update_recording.phpnu[. namespace mod_bigbluebuttonbn\external; use coding_exception; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_single_structure; use core_external\external_value; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\local\bigbluebutton\recordings\recording_action; use mod_bigbluebuttonbn\recording; /** * External service to update the details of one recording. * * @package mod_bigbluebuttonbn * @category external * @copyright 2018 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class update_recording extends external_api { /** * Updates a recording * * @param int $bigbluebuttonbnid the bigbluebuttonbn instance id, either the same as the one set in the recording or a new * instance to import the recording into. * @param int $recordingid * @param string $action * @param string|null $additionaloptions * @return array (empty array for now) */ public static function execute( int $bigbluebuttonbnid, int $recordingid, string $action, ?string $additionaloptions = null ): array { // Validate the bigbluebuttonbnid ID. [ 'bigbluebuttonbnid' => $bigbluebuttonbnid, 'recordingid' => $recordingid, 'action' => $action, 'additionaloptions' => $additionaloptions, ] = self::validate_parameters(self::execute_parameters(), [ 'bigbluebuttonbnid' => $bigbluebuttonbnid, 'recordingid' => $recordingid, 'action' => $action, 'additionaloptions' => $additionaloptions, ]); switch ($action) { case 'delete': case 'edit': case 'protect': case 'publish': case 'unprotect': case 'unpublish': case 'import': break; default: throw new coding_exception("Unknown action '{$action}'"); } $instance = instance::get_from_instanceid($bigbluebuttonbnid); $recordingcontext = $instance->get_context(); self::validate_context($recordingcontext); require_capability('mod/bigbluebuttonbn:managerecordings', $recordingcontext); require_capability("mod/bigbluebuttonbn:{$action}recordings", $recordingcontext); // Fetch the session, features, and profile. $recording = new recording($recordingid); // Check both the recording instance context and the bbb context. $relatedinstance = instance::get_from_instanceid($recording->get('bigbluebuttonbnid')); if ($relatedinstance) { $recordingcontext = $relatedinstance->get_context(); // Validate that the user has access to this activity and to manage recordings. self::validate_context($recordingcontext); require_capability('mod/bigbluebuttonbn:managerecordings', $recordingcontext); require_capability("mod/bigbluebuttonbn:{$action}recordings", $recordingcontext); } $additionaloptionsobject = $additionaloptions ? json_decode($additionaloptions) : null; // Specific action such as import, delete, publish, unpublish, edit,.... if (method_exists(recording_action::class, "$action")) { forward_static_call( ['\mod_bigbluebuttonbn\local\bigbluebutton\recordings\recording_action', "$action"], $recording, $instance, $additionaloptionsobject ); } return []; } /** * Returns description of method parameters * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters([ 'bigbluebuttonbnid' => new external_value( PARAM_INT, 'bigbluebuttonbn instance id, this might be a different one from the one set in recordingid in case of importing' ), 'recordingid' => new external_value(PARAM_INT, 'The moodle internal recording ID'), 'action' => new external_value(PARAM_ALPHANUMEXT, 'The action to perform'), 'additionaloptions' => new external_value(PARAM_RAW, 'Additional options', VALUE_REQUIRED, null), ]); } /** * Describe the return structure of the external service. * * @return external_single_structure * @since Moodle 3.0 */ public static function execute_returns(): external_single_structure { return new external_single_structure([]); } } PK\N0--4classes/external/get_bigbluebuttonbns_by_courses.phpnu[. namespace mod_bigbluebuttonbn\external; use core_course\external\helper_for_get_mods_by_courses; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; use core_external\util as external_util; /** * External service to get activity per course * * This is mainly used by the mobile application. * * @package mod_bigbluebuttonbn * @category external * @copyright 2018 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class get_bigbluebuttonbns_by_courses extends external_api { /** * Describes the parameters for get_bigbluebuttonbns_by_courses. * * @return external_function_parameters * @since Moodle 3.11 */ public static function execute_parameters() { return new external_function_parameters([ 'courseids' => new external_multiple_structure( new external_value(PARAM_INT, 'Course id'), 'Array of course ids', VALUE_DEFAULT, [] ), ] ); } /** * Returns a list of bigbluebuttonbns in a provided list of courses. * If no list is provided all bigbluebuttonbns that the user can view will be returned. * * @param array $courseids course ids * @return array of warnings and bigbluebuttonbns * @since Moodle 3.11 */ public static function execute($courseids = []) { global $USER; $warnings = []; $returnedbigbluebuttonbns = []; ['courseids' => $courseids] = self::validate_parameters(self::execute_parameters(), ['courseids' => $courseids]); $mycourses = []; if (empty($courseids)) { $mycourses = enrol_get_my_courses(); $courseids = array_keys($mycourses); } // Ensure there are courseids to loop through. if (!empty($courseids)) { [$courses, $warnings] = external_util::validate_courses($courseids, $mycourses); // Get the bigbluebuttonbns in this course, this function checks users visibility permissions. // We can avoid then additional validate_context calls. $bigbluebuttonbns = get_all_instances_in_courses("bigbluebuttonbn", $courses, $USER->id); foreach ($bigbluebuttonbns as $bigbluebuttonbn) { helper_for_get_mods_by_courses::format_name_and_intro($bigbluebuttonbn, 'mod_bigbluebuttonbn'); $returnedbigbluebuttonbns[] = $bigbluebuttonbn; } } $result = [ 'bigbluebuttonbns' => $returnedbigbluebuttonbns, 'warnings' => $warnings ]; return $result; } /** * Describes the get_bigbluebuttonbns_by_courses return value. * * @return external_single_structure * @since Moodle 3.11 */ public static function execute_returns() { return new external_single_structure([ 'bigbluebuttonbns' => new external_multiple_structure( new external_single_structure(array_merge( helper_for_get_mods_by_courses::standard_coursemodule_elements_returns(), [ 'meetingid' => new external_value(PARAM_RAW, 'Meeting id'), 'timemodified' => new external_value(PARAM_INT, 'Last time the instance was modified'), ] )) ), 'warnings' => new external_warnings(), ] ); } } PK\x22classes/privacy/provider.phpnu[. namespace mod_bigbluebuttonbn\privacy; 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\helper; use core_privacy\local\request\transform; use core_privacy\local\request\userlist; use core_privacy\local\request\writer; /** * Privacy class for requesting user data. * * @package mod_bigbluebuttonbn * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) */ class provider implements // This plugin has data. \core_privacy\local\metadata\provider, // This plugin currently implements the original plugin\provider interface. \core_privacy\local\request\plugin\provider, // This plugin is capable of determining which users have data within it. \core_privacy\local\request\core_userlist_provider { /** * Returns metadata. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection): collection { // The table bigbluebuttonbn stores only the room properties. // However, there is a chance that some personal information is stored as metadata. // This would be done in the column 'participants' where rules can be set to define BBB roles. // It is fair to say that only the userid is stored, which is useless if user is removed. // But if this is a concern a refactoring on the way the rules are stored will be required. $collection->add_database_table('bigbluebuttonbn', [ 'participants' => 'privacy:metadata:bigbluebuttonbn:participants', ], 'privacy:metadata:bigbluebuttonbn'); // The table bigbluebuttonbn_logs stores events triggered by users when using the plugin. // Some personal information along with the resource accessed is stored. $collection->add_database_table('bigbluebuttonbn_logs', [ 'userid' => 'privacy:metadata:bigbluebuttonbn_logs:userid', 'timecreated' => 'privacy:metadata:bigbluebuttonbn_logs:timecreated', 'meetingid' => 'privacy:metadata:bigbluebuttonbn_logs:meetingid', 'log' => 'privacy:metadata:bigbluebuttonbn_logs:log', 'meta' => 'privacy:metadata:bigbluebuttonbn_logs:meta', ], 'privacy:metadata:bigbluebuttonbn_logs'); $collection->add_database_table('bigbluebuttonbn_recordings', [ 'userid' => 'privacy:metadata:bigbluebuttonbn_logs:userid', ], 'privacy:metadata:bigbluebuttonbn_recordings'); // Personal information has to be passed to BigBlueButton. // This includes the user ID and fullname. $collection->add_external_location_link('bigbluebutton', [ 'userid' => 'privacy:metadata:bigbluebutton:userid', 'fullname' => 'privacy:metadata:bigbluebutton:fullname', ], 'privacy:metadata:bigbluebutton'); return $collection; } /** * Get the list of contexts that contain user information for the specified user. * * @param int $userid The user to search. * @return contextlist $contextlist The list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid): contextlist { // If user was already deleted, do nothing. if (!\core_user::get_user($userid)) { return new contextlist(); } // Fetch all bigbluebuttonbn logs. $sql = "SELECT c.id FROM {context} c INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname INNER JOIN {bigbluebuttonbn} bigbluebuttonbn ON bigbluebuttonbn.id = cm.instance INNER JOIN {bigbluebuttonbn_logs} bigbluebuttonbnlogs ON bigbluebuttonbnlogs.bigbluebuttonbnid = bigbluebuttonbn.id WHERE bigbluebuttonbnlogs.userid = :userid"; $params = [ 'modname' => 'bigbluebuttonbn', 'contextlevel' => CONTEXT_MODULE, 'userid' => $userid, ]; $contextlist = new contextlist(); $contextlist->add_from_sql($sql, $params); return $contextlist; } /** * Export personal data for the given approved_contextlist. User and context information is contained within the contextlist. * * @param approved_contextlist $contextlist a list of contexts approved for export. */ public static function export_user_data(approved_contextlist $contextlist) { global $DB; // Filter out any contexts that are not related to modules. $cmids = array_reduce($contextlist->get_contexts(), function($carry, $context) { if ($context->contextlevel == CONTEXT_MODULE) { $carry[] = $context->instanceid; } return $carry; }, []); if (empty($cmids)) { return; } $user = $contextlist->get_user(); // Get all the bigbluebuttonbn activities associated with the above course modules. $instanceidstocmids = self::get_instance_ids_to_cmids_from_cmids($cmids); $instanceids = array_keys($instanceidstocmids); list($insql, $inparams) = $DB->get_in_or_equal($instanceids, SQL_PARAMS_NAMED); $params = array_merge($inparams, ['userid' => $user->id]); $recordset = $DB->get_recordset_select( 'bigbluebuttonbn_logs', "bigbluebuttonbnid $insql AND userid = :userid", $params, 'timecreated, id' ); self::recordset_loop_and_export($recordset, 'bigbluebuttonbnid', [], function($carry, $record) use ($user, $instanceidstocmids) { $carry[] = [ 'timecreated' => transform::datetime($record->timecreated), 'meetingid' => $record->meetingid, 'log' => $record->log, 'meta' => $record->meta, ]; return $carry; }, function($instanceid, $data) use ($user, $instanceidstocmids) { $context = \context_module::instance($instanceidstocmids[$instanceid]); $contextdata = helper::get_context_data($context, $user); $finaldata = (object) array_merge((array) $contextdata, ['logs' => $data]); helper::export_context_files($context, $user); writer::with_context($context)->export_data([], $finaldata); } ); } /** * Delete all data for all users in the specified context. * * @param \context $context the context to delete in. */ public static function delete_data_for_all_users_in_context(\context $context) { global $DB; if (!$context instanceof \context_module) { return; } $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST); $DB->delete_records('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => $instanceid]); } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist a list of contexts approved for deletion. */ public static function delete_data_for_user(approved_contextlist $contextlist) { global $DB; $count = $contextlist->count(); if (empty($count)) { return; } $userid = $contextlist->get_user()->id; foreach ($contextlist->get_contexts() as $context) { if (!$context instanceof \context_module) { return; } $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST); $DB->delete_records('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => $instanceid, 'userid' => $userid]); } } /** * Return a dict of bigbluebuttonbn IDs mapped to their course module ID. * * @param array $cmids The course module IDs. * @return array In the form of [$bigbluebuttonbnid => $cmid]. */ protected static function get_instance_ids_to_cmids_from_cmids(array $cmids) { global $DB; list($insql, $inparams) = $DB->get_in_or_equal($cmids, SQL_PARAMS_NAMED); $sql = "SELECT bigbluebuttonbn.id, cm.id AS cmid FROM {bigbluebuttonbn} bigbluebuttonbn JOIN {modules} m ON m.name = :bigbluebuttonbn JOIN {course_modules} cm ON cm.instance = bigbluebuttonbn.id AND cm.module = m.id WHERE cm.id $insql"; $params = array_merge($inparams, ['bigbluebuttonbn' => 'bigbluebuttonbn']); return $DB->get_records_sql_menu($sql, $params); } /** * Loop and export from a recordset. * * @param \moodle_recordset $recordset The recordset. * @param string $splitkey The record key to determine when to export. * @param mixed $initial The initial data to reduce from. * @param callable $reducer The function to return the dataset, receives current dataset, and the current record. * @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset. * @return void */ protected static function recordset_loop_and_export( \moodle_recordset $recordset, $splitkey, $initial, callable $reducer, callable $export ) { $data = $initial; $lastid = null; foreach ($recordset as $record) { if ($lastid && $record->{$splitkey} != $lastid) { $export($lastid, $data); $data = $initial; } $data = $reducer($data, $record); $lastid = $record->{$splitkey}; } $recordset->close(); if (!empty($lastid)) { $export($lastid, $data); } } /** * 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(\core_privacy\local\request\userlist $userlist) { $context = $userlist->get_context(); if (!$context instanceof \context_module) { return; } $params = [ 'instanceid' => $context->instanceid, 'modulename' => 'bigbluebuttonbn', ]; $sql = "SELECT bnl.userid FROM {course_modules} cm JOIN {modules} m ON m.id = cm.module AND m.name = :modulename JOIN {bigbluebuttonbn} bn ON bn.id = cm.instance JOIN {bigbluebuttonbn_logs} bnl ON bnl.bigbluebuttonbnid = bn.id WHERE cm.id = :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(\core_privacy\local\request\approved_userlist $userlist) { global $DB; $context = $userlist->get_context(); $cm = $DB->get_record('course_modules', ['id' => $context->instanceid]); list($userinsql, $userinparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED); $params = array_merge(['bigbluebuttonbnid' => $cm->instance], $userinparams); $sql = "bigbluebuttonbnid = :bigbluebuttonbnid AND userid {$userinsql}"; $DB->delete_records_select('bigbluebuttonbn_logs', $sql, $params); } } PK\5151classes/local/config.phpnu[. namespace mod_bigbluebuttonbn\local; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; use mod_bigbluebuttonbn\recording; /** * Handles the global configuration based on config.php. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) */ class config { /** @var string Default bigbluebutton server url */ public const DEFAULT_SERVER_URL = 'https://test-moodle.blindsidenetworks.com/bigbluebutton/'; /** @var string Default bigbluebutton server shared secret */ public const DEFAULT_SHARED_SECRET = '0b21fcaf34673a8c3ec8ed877d76ae34'; /** @var string the default bigbluebutton checksum algorithm */ public const DEFAULT_CHECKSUM_ALGORITHM = 'SHA256'; /** @var array list of supported bigbluebutton checksum algorithm */ const CHECKSUM_ALGORITHMS = [ self::DEFAULT_CHECKSUM_ALGORITHM, 'SHA1', 'SHA512' ]; /** * Returns moodle version. * * @return string */ protected static function get_moodle_version_major(): string { global $CFG; $versionarray = explode('.', $CFG->version); return $versionarray[0]; } /** * Returns configuration default values. * * @return array */ protected static function defaultvalues(): array { return [ 'server_url' => '', 'shared_secret' => '', 'voicebridge_editable' => false, 'importrecordings_enabled' => false, 'importrecordings_from_deleted_enabled' => false, 'waitformoderator_default' => false, 'waitformoderator_editable' => true, 'waitformoderator_ping_interval' => '10', 'waitformoderator_cache_ttl' => '60', 'userlimit_default' => '0', 'userlimit_editable' => false, 'preuploadpresentation_editable' => false, 'recordingready_enabled' => false, 'recordingstatus_enabled' => false, 'meetingevents_enabled' => false, 'participant_moderator_default' => '0', 'profile_picture_enabled' => false, 'scheduled_pre_opening' => '10', 'recordings_enabled' => true, 'recordings_deleted_default' => false, 'recordings_deleted_editable' => false, 'recordings_imported_default' => false, 'recordings_imported_editable' => false, 'recordings_preview_default' => true, 'recordings_preview_editable' => false, 'recording_default' => true, 'recording_editable' => true, 'recording_refresh_period' => recording::RECORDING_REFRESH_DEFAULT_PERIOD, 'recording_all_from_start_default' => false, 'recording_all_from_start_editable' => false, 'recording_hide_button_default' => false, 'recording_hide_button_editable' => false, 'recording_protect_editable' => true, 'general_warning_message' => '', 'general_warning_roles' => 'editingteacher,teacher', 'general_warning_box_type' => 'info', 'general_warning_button_text' => '', 'general_warning_button_href' => '', 'general_warning_button_class' => '', 'muteonstart_default' => false, 'muteonstart_editable' => false, 'disablecam_default' => false, 'disablecam_editable' => true, 'disablemic_default' => false, 'disablemic_editable' => true, 'disableprivatechat_default' => false, 'disableprivatechat_editable' => true, 'disablepublicchat_default' => false, 'disablepublicchat_editable' => true, 'disablenote_default' => false, 'disablenote_editable' => true, 'hideuserlist_default' => false, 'hideuserlist_editable' => true, 'welcome_default' => '', 'welcome_editable' => true, 'default_dpa_accepted' => false, 'poll_interval' => bigbluebutton_proxy::DEFAULT_POLL_INTERVAL, 'checksum_algorithm' => self::DEFAULT_CHECKSUM_ALGORITHM, 'showpresentation_default' => true, 'showpresentation_editable' => false, ]; } /** * Returns default value for an specific setting. * * @param string $setting * @return string|null */ public static function defaultvalue(string $setting): ?string { $defaultvalues = self::defaultvalues(); if (!array_key_exists($setting, $defaultvalues)) { return null; } return $defaultvalues[$setting]; } /** * Returns value for an specific setting. * * @param string $setting * @return string */ public static function get(string $setting): string { global $CFG; if (isset($CFG->bigbluebuttonbn[$setting])) { return (string) $CFG->bigbluebuttonbn[$setting]; } if (isset($CFG->{'bigbluebuttonbn_' . $setting})) { return (string) $CFG->{'bigbluebuttonbn_' . $setting}; } return (string) self::defaultvalue($setting); } /** * Validates if recording settings are enabled. * * @return bool */ public static function recordings_enabled(): bool { return (boolean) self::get('recordings_enabled'); } /** * Validates if imported recording settings are enabled. * * @return bool */ public static function importrecordings_enabled(): bool { return (boolean) self::get('importrecordings_enabled'); } /** * Check if bbb server credentials are invalid. * * @return bool */ public static function server_credentials_invalid(): bool { // Test server credentials across all versions of the plugin are flagged. $parsedurl = parse_url(self::get('server_url')); $defaultserverurl = parse_url(self::DEFAULT_SERVER_URL); if (!isset($parsedurl['host'])) { return false; } if (strpos($parsedurl['host'], $defaultserverurl['host']) === 0) { return true; } if (strpos($parsedurl['host'], 'test-install.blindsidenetworks.com') === 0) { return true; } return false; } /** * Wraps current settings in an array. * * @return array */ public static function get_options(): array { return [ 'version_major' => self::get_moodle_version_major(), 'voicebridge_editable' => self::get('voicebridge_editable'), 'importrecordings_enabled' => self::get('importrecordings_enabled'), 'importrecordings_from_deleted_enabled' => self::get('importrecordings_from_deleted_enabled'), 'waitformoderator_default' => self::get('waitformoderator_default'), 'waitformoderator_editable' => self::get('waitformoderator_editable'), 'userlimit_default' => self::get('userlimit_default'), 'userlimit_editable' => self::get('userlimit_editable'), 'preuploadpresentation_editable' => self::get('preuploadpresentation_editable'), 'recordings_enabled' => self::get('recordings_enabled'), 'meetingevents_enabled' => self::get('meetingevents_enabled'), 'recordings_deleted_default' => self::get('recordings_deleted_default'), 'recordings_deleted_editable' => self::get('recordings_deleted_editable'), 'recordings_imported_default' => self::get('recordings_imported_default'), 'recordings_imported_editable' => self::get('recordings_imported_editable'), 'recordings_preview_default' => self::get('recordings_preview_default'), 'recordings_preview_editable' => self::get('recordings_preview_editable'), 'recording_default' => self::get('recording_default'), 'recording_editable' => self::get('recording_editable'), 'recording_refresh_period' => self::get('recording_refresh_period'), 'recording_all_from_start_default' => self::get('recording_all_from_start_default'), 'recording_all_from_start_editable' => self::get('recording_all_from_start_editable'), 'recording_hide_button_default' => self::get('recording_hide_button_default'), 'recording_hide_button_editable' => self::get('recording_hide_button_editable'), 'recording_protect_editable' => self::get('recording_protect_editable'), 'general_warning_message' => self::get('general_warning_message'), 'general_warning_box_type' => self::get('general_warning_box_type'), 'general_warning_button_text' => self::get('general_warning_button_text'), 'general_warning_button_href' => self::get('general_warning_button_href'), 'general_warning_button_class' => self::get('general_warning_button_class'), 'muteonstart_editable' => self::get('muteonstart_editable'), 'muteonstart_default' => self::get('muteonstart_default'), 'disablecam_editable' => self::get('disablecam_editable'), 'disablecam_default' => self::get('disablecam_default'), 'disablemic_editable' => self::get('disablemic_editable'), 'disablemic_default' => self::get('disablemic_default'), 'disableprivatechat_editable' => self::get('disableprivatechat_editable'), 'disableprivatechat_default' => self::get('disableprivatechat_default'), 'disablepublicchat_editable' => self::get('disablepublicchat_editable'), 'disablepublicchat_default' => self::get('disablepublicchat_default'), 'disablenote_editable' => self::get('disablenote_editable'), 'disablenote_default' => self::get('disablenote_default'), 'hideuserlist_editable' => self::get('hideuserlist_editable'), 'hideuserlist_default' => self::get('hideuserlist_default'), 'welcome_default' => self::get('welcome_default'), 'welcome_editable' => self::get('welcome_editable'), 'poll_interval' => self::get('poll_interval'), 'guestaccess_enabled' => self::get('guestaccess_enabled'), 'showpresentation_editable' => self::get('showpresentation_editable'), 'showpresentation_default' => self::get('showpresentation_default'), ]; } /** * Helper function returns an array with enabled features for an specific profile type. * * @param array $typeprofiles * @param string|null $type * * @return array */ public static function get_enabled_features(array $typeprofiles, ?string $type = null): array { $enabledfeatures = []; $features = $typeprofiles[instance::TYPE_ALL]['features']; if (!is_null($type) && key_exists($type, $typeprofiles)) { $features = $typeprofiles[$type]['features']; } $enabledfeatures['showroom'] = (in_array('all', $features) || in_array('showroom', $features)); // Evaluates if recordings are enabled for the Moodle site. $enabledfeatures['showrecordings'] = false; if (self::recordings_enabled()) { $enabledfeatures['showrecordings'] = (in_array('all', $features) || in_array('showrecordings', $features)); } $enabledfeatures['importrecordings'] = false; if (self::importrecordings_enabled()) { $enabledfeatures['importrecordings'] = (in_array('all', $features) || in_array('importrecordings', $features)); } return $enabledfeatures; } } PK\۾| 9 9'classes/local/proxy/recording_proxy.phpnu[. namespace mod_bigbluebuttonbn\local\proxy; use cache; use cache_helper; use SimpleXMLElement; /** * The recording proxy. * * This class acts as a proxy between Moodle and the BigBlueButton API server, * and deals with all requests relating to recordings. * * @package mod_bigbluebuttonbn * @copyright 2021 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent [at] call-learning [dt] fr) * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) */ class recording_proxy extends proxy_base { /** * Invalidate the MUC cache for the specified recording. * * @param string $recordid */ protected static function invalidate_cache_for_recording(string $recordid): void { cache_helper::invalidate_by_event('mod_bigbluebuttonbn/recordingchanged', [$recordid]); } /** * Perform deleteRecordings on BBB. * * @param string $recordid a recording id * @return bool */ public static function delete_recording(string $recordid): bool { $result = self::fetch_endpoint_xml('deleteRecordings', ['recordID' => $recordid]); if (!$result || $result->returncode != 'SUCCESS') { return false; } return true; } /** * Perform publishRecordings on BBB. * * @param string $recordid * @param string $publish * @return bool */ public static function publish_recording(string $recordid, string $publish = 'true'): bool { $result = self::fetch_endpoint_xml('publishRecordings', [ 'recordID' => $recordid, 'publish' => $publish, ]); self::invalidate_cache_for_recording($recordid); if (!$result || $result->returncode != 'SUCCESS') { return false; } return true; } /** * Perform publishRecordings on BBB. * * @param string $recordid * @param string $protected * @return bool */ public static function protect_recording(string $recordid, string $protected = 'true'): bool { global $CFG; // Ignore action if recording_protect_editable is set to false. if (empty($CFG->bigbluebuttonbn_recording_protect_editable)) { return false; } $result = self::fetch_endpoint_xml('updateRecordings', [ 'recordID' => $recordid, 'protect' => $protected, ]); self::invalidate_cache_for_recording($recordid); if (!$result || $result->returncode != 'SUCCESS') { return false; } return true; } /** * Perform updateRecordings on BBB. * * @param string $recordid a single record identifier * @param array $params ['key'=>param_key, 'value'] */ public static function update_recording(string $recordid, array $params): bool { $result = self::fetch_endpoint_xml('updateRecordings', array_merge([ 'recordID' => $recordid ], $params)); self::invalidate_cache_for_recording($recordid); return $result ? $result->returncode == 'SUCCESS' : false; } /** * Helper function to fetch a single recording from a BigBlueButton server. * * @param string $recordingid * @return null|array */ public static function fetch_recording(string $recordingid): ?array { $data = self::fetch_recordings([$recordingid]); if (array_key_exists($recordingid, $data)) { return $data[$recordingid]; } return null; } /** * Check whether the current recording is a protected recording and purge the cache if necessary. * * @param string $recordingid */ public static function purge_protected_recording(string $recordingid): void { $cache = cache::make('mod_bigbluebuttonbn', 'recordings'); $recording = $cache->get($recordingid); if (empty($recording)) { // This value was not cached to begin with. return; } $currentfetchcache = cache::make('mod_bigbluebuttonbn', 'currentfetch'); if ($currentfetchcache->has($recordingid)) { // This item was fetched in the current request. return; } if (array_key_exists('protected', $recording) && $recording['protected'] === 'true') { // This item is protected. Purge it from the cache. $cache->delete($recordingid); return; } } /** * Helper function to fetch recordings from a BigBlueButton server. * * We use a cache to store recording indexed by keyids/recordingID. * @param array $keyids list of recordingids * @return array (associative) with recordings indexed by recordID, each recording is a non sequential array * and sorted by {@see recording_proxy::sort_recordings} */ public static function fetch_recordings(array $keyids = []): array { $recordings = []; // If $ids is empty return array() to prevent a getRecordings with meetingID and recordID set to ''. if (empty($keyids)) { return $recordings; } $cache = cache::make('mod_bigbluebuttonbn', 'recordings'); $currentfetchcache = cache::make('mod_bigbluebuttonbn', 'currentfetch'); $recordings = array_filter($cache->get_many($keyids)); $missingkeys = array_diff(array_values($keyids), array_keys($recordings)); $recordings += self::do_fetch_recordings($missingkeys); $cache->set_many($recordings); $currentfetchcache->set_many(array_flip(array_keys($recordings))); return $recordings; } /** * Helper function to retrieve recordings that failed to be fetched from a BigBlueButton server. * * @param array $keyids list of recordingids * @return array array of recording recordingids not fetched from server * and sorted by {@see recording_proxy::sort_recordings} */ public static function fetch_missing_recordings(array $keyids = []): array { $unfetchedids = []; $pagesize = 25; // If $ids is empty return array() to prevent a getRecordings with meetingID and recordID set to ''. if (empty($keyids)) { return $unfetchedids; } while ($ids = array_splice($keyids, 0, $pagesize)) { // We make getRecordings API call to check recordings are successfully retrieved. $xml = self::fetch_endpoint_xml('getRecordings', ['recordID' => implode(',', $ids), 'state' => 'any']); if (!$xml || $xml->returncode != 'SUCCESS' || !isset($xml->recordings)) { $unfetchedids = array_merge($unfetchedids, $ids); continue; // We will keep record of all unfetched ids. } } return $unfetchedids; } /** * Helper function to fetch recordings from a BigBlueButton server. * * @param array $keyids list of meetingids * @return array (associative) with recordings indexed by recordID, each recording is a non sequential array * and sorted by {@see recording_proxy::sort_recordings} */ public static function fetch_recording_by_meeting_id(array $keyids = []): array { $recordings = []; // If $ids is empty return array() to prevent a getRecordings with meetingID and recordID set to ''. if (empty($keyids)) { return $recordings; } $recordings = self::do_fetch_recordings($keyids, 'meetingID'); return $recordings; } /** * Helper function to fetch recordings from a BigBlueButton server. * * @param array $keyids list of meetingids or recordingids * @param string $key the param name used for the BBB request (|meetingID) * @return array (associative) with recordings indexed by recordID, each recording is a non sequential array. * and sorted {@see recording_proxy::sort_recordings} */ private static function do_fetch_recordings(array $keyids = [], string $key = 'recordID'): array { $recordings = []; $pagesize = 25; while ($ids = array_splice($keyids, 0, $pagesize)) { $fetchrecordings = self::fetch_recordings_page($ids, $key); $recordings += $fetchrecordings; } // Sort recordings. return self::sort_recordings($recordings); } /** * Helper function to fetch a page of recordings from the remote server. * * @param array $ids * @param string $key * @return array */ private static function fetch_recordings_page(array $ids, $key = 'recordID'): array { // The getRecordings call is executed using a method GET (supported by all versions of BBB). $xml = self::fetch_endpoint_xml('getRecordings', [$key => implode(',', $ids), 'state' => 'any']); if (!$xml) { return []; } if ($xml->returncode != 'SUCCESS') { return []; } if (!isset($xml->recordings)) { return []; } $recordings = []; // If there were recordings already created. foreach ($xml->recordings->recording as $recordingxml) { $recording = self::parse_recording($recordingxml); $recordings[$recording['recordID']] = $recording; // Check if there are any child. if (isset($recordingxml->breakoutRooms->breakoutRoom)) { $breakoutrooms = []; foreach ($recordingxml->breakoutRooms->breakoutRoom as $breakoutroom) { $breakoutrooms[] = trim((string) $breakoutroom); } if ($breakoutrooms) { $xml = self::fetch_endpoint_xml('getRecordings', ['recordID' => implode(',', $breakoutrooms)]); if ($xml && $xml->returncode == 'SUCCESS' && isset($xml->recordings)) { // If there were already created meetings. foreach ($xml->recordings->recording as $subrecordingxml) { $recording = self::parse_recording($subrecordingxml); $recordings[$recording['recordID']] = $recording; } } } } } return $recordings; } /** * Helper function to sort an array of recordings. It compares the startTime in two recording objects. * * @param array $recordings * @return array */ public static function sort_recordings(array $recordings): array { global $CFG; uasort($recordings, function($a, $b) { if ($a['startTime'] < $b['startTime']) { return -1; } if ($a['startTime'] == $b['startTime']) { return 0; } return 1; }); return $recordings; } /** * Helper function to parse an xml recording object and produce an array in the format used by the plugin. * * @param SimpleXMLElement $recording * * @return array */ public static function parse_recording(SimpleXMLElement $recording): array { // Add formats. $playbackarray = []; foreach ($recording->playback->format as $format) { $playbackarray[(string) $format->type] = [ 'type' => (string) $format->type, 'url' => trim((string) $format->url), 'length' => (string) $format->length ]; // Add preview per format when existing. if ($format->preview) { $playbackarray[(string) $format->type]['preview'] = self::parse_preview_images($format->preview); } } // Add the metadata to the recordings array. $metadataarray = self::parse_recording_meta(get_object_vars($recording->metadata)); $recordingarray = [ 'recordID' => (string) $recording->recordID, 'meetingID' => (string) $recording->meetingID, 'meetingName' => (string) $recording->name, 'published' => (string) $recording->published, 'state' => (string) $recording->state, 'startTime' => (string) $recording->startTime, 'endTime' => (string) $recording->endTime, 'playbacks' => $playbackarray ]; if (isset($recording->protected)) { $recordingarray['protected'] = (string) $recording->protected; } return $recordingarray + $metadataarray; } /** * Helper function to convert an xml recording metadata object to an array in the format used by the plugin. * * @param array $metadata * * @return array */ public static function parse_recording_meta(array $metadata): array { $metadataarray = []; foreach ($metadata as $key => $value) { if (is_object($value)) { $value = ''; } $metadataarray['meta_' . $key] = $value; } return $metadataarray; } /** * Helper function to convert an xml recording preview images to an array in the format used by the plugin. * * @param SimpleXMLElement $preview * * @return array */ public static function parse_preview_images(SimpleXMLElement $preview): array { $imagesarray = []; foreach ($preview->images->image as $image) { $imagearray = ['url' => trim((string) $image)]; foreach ($image->attributes() as $attkey => $attvalue) { $imagearray[$attkey] = (string) $attvalue; } array_push($imagesarray, $imagearray); } return $imagesarray; } } PK\؆classes/local/proxy/curl.phpnu[. /** * A curl wrapper for bbb. * * @package mod_bigbluebuttonbn * @copyright 2021 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_bigbluebuttonbn\local\proxy; use SimpleXMLElement; defined('MOODLE_INTERNAL') || die; global $CFG; require_once("{$CFG->libdir}/filelib.php"); /** * A curl wrapper for bbb. * * @package mod_bigbluebuttonbn * @copyright 2021 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class curl extends \curl { /** @var string */ protected $contenttype; /** * Constructor for the class. */ public function __construct() { $settings = []; if (debugging()) { $settings = ['ignoresecurity' => true]; } parent::__construct($settings); $this->setopt(['SSL_VERIFYPEER' => true]); $this->set_content_type('application/xml'); } /** * Fetch the content type. */ public function get_content_type(): string { return $this->contenttype; } /** * Set the desired current content type. * * @param string $type * @return self */ public function set_content_type(string $type): self { $this->contenttype = $type; return $this; } /** * HTTP POST method * * @param string $url * @param array|string $params * @param array $options * @return null|SimpleXMLElement Null on error */ public function post($url, $params = '', $options = []) { if (!is_string($params)) { debugging('Only string parameters are supported', DEBUG_DEVELOPER); $params = ''; } $options = array_merge($options, [ 'CURLOPT_HTTPHEADER' => [ 'Content-Type: ' . $this->get_content_type(), 'Content-Length: ' . strlen($params), 'Content-Language: en-US', ] ]); return $this->handle_response(parent::post($url, $params, $options)); } /** * Fetch the specified URL via a HEAD request. * * @param string $url * @param array $options */ public function head($url, $options = []) { $options['followlocation'] = true; $options['timeout'] = 1; parent::head($url, $options); return $this->get_info(); } /** * Fetch the specified URL via a GET request. * * @param string $url * @param string $params * @param array $options */ public function get($url, $params = [], $options = []) { return $this->handle_response(parent::get($url, $params, $options)); } /** * Handle the response. * * @param mixed $response * @return null|SimpleXMLElement Null on error */ protected function handle_response($response): ?SimpleXMLElement { if (!$response) { debugging('No response returned for call', DEBUG_DEVELOPER); return null; } $previous = libxml_use_internal_errors(true); try { $xml = simplexml_load_string($response, 'SimpleXMLElement', LIBXML_NOCDATA | LIBXML_NOBLANKS); } catch (Exception $e) { libxml_use_internal_errors($previous); debugging('Caught exception: ' . $e->getMessage(), DEBUG_DEVELOPER); return null; } if ($xml instanceof SimpleXMLElement) { return $xml; } $debugabstract = html_to_text($response); $debugabstract = substr($debugabstract, 0, 1024); // Limit to small amount of info so we do not overload logs. debugging('Issue retrieving information from the server: ' . $debugabstract, DEBUG_DEVELOPER); return null; } } PK\0(T"classes/local/proxy/proxy_base.phpnu[. namespace mod_bigbluebuttonbn\local\proxy; use mod_bigbluebuttonbn\extension; use mod_bigbluebuttonbn\local\config; use mod_bigbluebuttonbn\local\exceptions\bigbluebutton_exception; use mod_bigbluebuttonbn\local\exceptions\server_not_available_exception; use mod_bigbluebuttonbn\plugin; use moodle_url; /** * The abstract proxy base class. * * This class provides common and shared functions used when interacting with * the BigBlueButton API server. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) */ abstract class proxy_base { /** * Sometimes the server sends back some error and errorKeys that * can be converted to Moodle error messages */ const BBB_TO_MOODLE_ERROR_CODE = [ 'checksumError' => 'index_error_checksum', 'notFound' => 'general_error_not_found', 'maxConcurrent' => 'view_error_max_concurrent', ]; /** * Returns the right URL for the action specified. * * @param string $action * @param array $data * @param array $metadata * @param int|null $instanceid * @return string */ protected static function action_url( string $action = '', array $data = [], array $metadata = [], ?int $instanceid = null ): string { $baseurl = self::sanitized_url() . $action . '?'; ['data' => $additionaldata, 'metadata' => $additionalmetadata] = extension::action_url_addons($action, $data, $metadata, $instanceid); $data = array_merge($data, $additionaldata ?? []); $metadata = array_merge($metadata, $additionalmetadata ?? []); $metadata = array_combine(array_map(function($k) { return 'meta_' . $k; }, array_keys($metadata)), $metadata); $params = http_build_query($data + $metadata, '', '&'); $checksum = self::get_checksum($action, $params); return $baseurl . $params . '&checksum=' . $checksum; } /** * Makes sure the url used doesn't is in the format required. * * @return string */ protected static function sanitized_url(): string { $serverurl = trim(config::get('server_url')); if (PHPUNIT_TEST) { $serverurl = (new moodle_url(TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER))->out(false); } if (substr($serverurl, -1) == '/') { $serverurl = rtrim($serverurl, '/'); } if (substr($serverurl, -4) == '/api') { $serverurl = rtrim($serverurl, '/api'); } return $serverurl . '/api/'; } /** * Makes sure the shared_secret used doesn't have trailing white characters. * * @return string */ protected static function sanitized_secret(): string { return trim(config::get('shared_secret')); } /** * Throw an exception if there is a problem in the returned XML value * * @param \SimpleXMLElement|bool $xml * @param array|null $additionaldetails * @throws bigbluebutton_exception * @throws server_not_available_exception */ protected static function assert_returned_xml($xml, ?array $additionaldetails = null): void { $messagekey = ''; if (!empty($xml)) { $messagekey = (string) ($xml->messageKey ?? ''); } if (empty($xml) || static::is_known_server_unavailable_errorcode($messagekey)) { $errorcode = self::get_errorcode_from_xml_messagekey($messagekey); throw new server_not_available_exception( $errorcode, plugin::COMPONENT, (new moodle_url('/admin/settings.php?section=modsettingbigbluebuttonbn'))->out(), ); } // If it is a checksum error, this is equivalent to the server not being available. // So we treat it the same way as if there is not answer. if (is_bool($xml) && $xml) { // Nothing to do here, this might be a post returning that everything went well. return; } if ((string) $xml->returncode == 'FAILED') { $errorcode = self::get_errorcode_from_xml_messagekey($messagekey); if (!$additionaldetails) { $additionaldetails = []; } $additionaldetails['xmlmessage'] = (string) $xml->message ?? ''; throw new bigbluebutton_exception($errorcode, json_encode($additionaldetails)); } } /** * Get Moodle error code from returned Message Key * * @param string $messagekey * @return string */ private static function get_errorcode_from_xml_messagekey(string $messagekey): string { $errorcode = 'general_error_no_answer'; if ($messagekey) { $errorcode = self::BBB_TO_MOODLE_ERROR_CODE[$messagekey] ?? $errorcode; } return $errorcode; } /** * Get Moodle error code from returned Message Key * * @param string $messagekey * @return string */ private static function is_known_server_unavailable_errorcode(string $messagekey): string { // For now, only checksumError is supposed to mean that the server is unavailable. // Other errors are recoverable. return in_array($messagekey, ['checksumError']); } /** * Fetch the XML from an endpoint and test for success. * * If the result could not be loaded, or the returncode was not 'SUCCESS', a null value is returned. * * @param string $action * @param array $data * @param array $metadata * @param int|null $instanceid * @return null|bool|\SimpleXMLElement */ protected static function fetch_endpoint_xml( string $action, array $data = [], array $metadata = [], ?int $instanceid = null ) { if (PHPUNIT_TEST && !defined('TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER')) { return true; // In case we still use fetch and mock server is not defined, this prevents // an error. This can happen if a function from lib.php is called in test from other modules // for example. } $curl = new curl(); return $curl->get(self::action_url($action, $data, $metadata, $instanceid)); } /** * Get checksum * * @param string $action * @param string $params * @return string */ public static function get_checksum(string $action, string $params): string { return hash(config::get('checksum_algorithm'), $action . $params . self::sanitized_secret()); } } PK\HLL+classes/local/proxy/bigbluebutton_proxy.phpnu[. namespace mod_bigbluebuttonbn\local\proxy; use cache; use completion_info; use Exception; use mod_bigbluebuttonbn\completion\custom_completion; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\local\config; use mod_bigbluebuttonbn\local\exceptions\bigbluebutton_exception; use mod_bigbluebuttonbn\local\exceptions\server_not_available_exception; use moodle_url; use stdClass; use user_picture; /** * The bigbluebutton proxy class. * * This class acts as a proxy between Moodle and the BigBlueButton API server, * and handles all requests relating to the server and meetings. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) */ class bigbluebutton_proxy extends proxy_base { /** * Minimum poll interval for remote bigbluebutton server in seconds. */ const MIN_POLL_INTERVAL = 2; /** * Default poll interval for remote bigbluebutton server in seconds. */ const DEFAULT_POLL_INTERVAL = 5; /** * Builds and returns a url for joining a BigBlueButton meeting. * * @param instance $instance * @param string|null $createtime * * @return string */ public static function get_join_url( instance $instance, ?string $createtime ): string { return self::internal_get_join_url($instance, $createtime); } /** * Builds and returns a url for joining a BigBlueButton meeting. * * @param instance $instance * @param string|null $createtime * @param string $username * @return string */ public static function get_guest_join_url( instance $instance, ?string $createtime, string $username ): string { return self::internal_get_join_url($instance, $createtime, $username, true); } /** * Internal helper method to builds and returns a url for joining a BigBlueButton meeting. * * @param instance $instance * @param string|null $jointime = null * @param string|null $userfullname * @param bool $isguestjoin * @return string */ private static function internal_get_join_url( instance $instance, ?string $jointime, ?string $userfullname = null, bool $isguestjoin = false ): string { $data = [ 'meetingID' => $instance->get_meeting_id(), 'fullName' => $userfullname ?? $instance->get_user_fullname(), 'password' => $instance->get_current_user_password(), 'logoutURL' => $isguestjoin ? $instance->get_guest_access_url()->out(false) : $instance->get_logout_url()->out(false), 'role' => $instance->get_current_user_role() ]; if (!$isguestjoin) { $data['userID'] = $instance->get_user_id(); $data['guest'] = "false"; } else { $data['guest'] = "true"; } if (!is_null($jointime)) { $data['createTime'] = $jointime; } $currentlang = current_language(); if (!empty(trim($currentlang))) { $data['userdata-bbb_override_default_locale'] = $currentlang; } if ($instance->is_profile_picture_enabled()) { $user = $instance->get_user(); if (!empty($user->picture)) { $data['avatarURL'] = self::get_avatar_url($user)->out(false); } } return self::action_url('join', $data, [], $instance->get_instance_id()); } /** * Get user avatar URL * * @param stdClass $user * @return moodle_url */ private static function get_avatar_url(stdClass $user): moodle_url { global $PAGE; $userpicture = new user_picture($user); $userpicture->includetoken = true; $userpicture->size = 3; // Size f3. return $userpicture->get_url($PAGE); } /** * Perform api request on BBB. * * @return null|string */ public static function get_server_version(): ?string { $cache = cache::make('mod_bigbluebuttonbn', 'serverinfo'); $serverversion = $cache->get('serverversion'); if (!$serverversion) { $xml = self::fetch_endpoint_xml(''); if (!$xml || $xml->returncode != 'SUCCESS') { return null; } if (!isset($xml->version)) { return null; } $serverversion = (string) $xml->version; $cache->set('serverversion', $serverversion); } return (double) $serverversion; } /** * Helper for getting the owner userid of a bigbluebuttonbn instance. * * @param stdClass $bigbluebuttonbn BigBlueButtonBN instance * @return int ownerid (a valid user id or null if not registered/found) */ public static function get_instance_ownerid(stdClass $bigbluebuttonbn): int { global $DB; $filters = [ 'bigbluebuttonbnid' => $bigbluebuttonbn->id, 'log' => 'Add', ]; return (int) $DB->get_field('bigbluebuttonbn_logs', 'userid', $filters); } /** * Helper evaluates if a voicebridge number is unique. * * @param int $instance * @param int $voicebridge * @return bool */ public static function is_voicebridge_number_unique(int $instance, int $voicebridge): bool { global $DB; if ($voicebridge == 0) { return true; } $select = 'voicebridge = ' . $voicebridge; if ($instance != 0) { $select .= ' AND id <>' . $instance; } if (!$DB->get_records_select('bigbluebuttonbn', $select)) { return true; } return false; } /** * Helper function validates a remote resource. * * @param string $url * @return bool */ public static function is_remote_resource_valid(string $url): bool { $urlhost = parse_url($url, PHP_URL_HOST); $serverurlhost = parse_url(\mod_bigbluebuttonbn\local\config::get('server_url'), PHP_URL_HOST); if ($urlhost == $serverurlhost) { // Skip validation when the recording URL host is the same as the configured BBB server. return true; } $cache = cache::make('mod_bigbluebuttonbn', 'validatedurls'); if ($cachevalue = $cache->get($urlhost)) { // Skip validation when the recording URL was already validated. return $cachevalue == 1; } $curl = new curl(); $curl->head($url); $isvalid = false; if ($info = $curl->get_info()) { if ($info['http_code'] == 200) { $isvalid = true; } else { debugging( "Resources hosted by {$urlhost} are unreachable. Server responded with {$info['http_code']}", DEBUG_DEVELOPER ); $isvalid = false; } // Note: When a cache key is not found, it returns false. // We need to distinguish between a result not found, and an invalid result. $cache->set($urlhost, $isvalid ? 1 : 0); } return $isvalid; } /** * Helper function enqueues one user for being validated as for completion. * * @param stdClass $bigbluebuttonbn * @param int $userid * @return void */ public static function enqueue_completion_event(stdClass $bigbluebuttonbn, int $userid): void { try { // Create the instance of completion_update_state task. $task = new \mod_bigbluebuttonbn\task\completion_update_state(); // Add custom data. $data = [ 'bigbluebuttonbn' => $bigbluebuttonbn, 'userid' => $userid, ]; $task->set_custom_data($data); // CONTRIB-7457: Task should be executed by a user, maybe Teacher as Student won't have rights for overriding. // $ task -> set_userid ( $ user -> id );. // Enqueue it. \core\task\manager::queue_adhoc_task($task); } catch (Exception $e) { mtrace("Error while enqueuing completion_update_state task. " . (string) $e); } } /** * Helper function enqueues completion trigger. * * @param stdClass $bigbluebuttonbn * @param int $userid * @return void */ public static function update_completion_state(stdClass $bigbluebuttonbn, int $userid) { global $CFG; require_once($CFG->libdir . '/completionlib.php'); list($course, $cm) = get_course_and_cm_from_instance($bigbluebuttonbn, 'bigbluebuttonbn'); $completion = new completion_info($course); if (!$completion->is_enabled($cm)) { mtrace("Completion not enabled"); return; } $bbbcompletion = new custom_completion($cm, $userid); if ($bbbcompletion->get_overall_completion_state()) { mtrace("Completion for userid $userid and bigbluebuttonid {$bigbluebuttonbn->id} updated."); $completion->update_state($cm, COMPLETION_COMPLETE, $userid, true); } else { // Still update state to current value (prevent unwanted caching). $completion->update_state($cm, COMPLETION_UNKNOWN, $userid); mtrace("Activity not completed for userid $userid and bigbluebuttonid {$bigbluebuttonbn->id}."); } } /** * Helper function returns an array with the profiles (with features per profile) for the different types * of bigbluebuttonbn instances. * * @return array */ public static function get_instance_type_profiles(): array { $instanceprofiles = [ instance::TYPE_ALL => [ 'id' => instance::TYPE_ALL, 'name' => get_string('instance_type_default', 'bigbluebuttonbn'), 'features' => ['all'] ], instance::TYPE_ROOM_ONLY => [ 'id' => instance::TYPE_ROOM_ONLY, 'name' => get_string('instance_type_room_only', 'bigbluebuttonbn'), 'features' => ['showroom', 'welcomemessage', 'voicebridge', 'waitformoderator', 'userlimit', 'recording', 'sendnotifications', 'lock', 'preuploadpresentation', 'permissions', 'schedule', 'groups', 'modstandardelshdr', 'availabilityconditionsheader', 'tagshdr', 'competenciessection', 'completionattendance', 'completionengagement', 'activitycompletionheader', 'modstandardgrade', ], ], instance::TYPE_RECORDING_ONLY => [ 'id' => instance::TYPE_RECORDING_ONLY, 'name' => get_string('instance_type_recording_only', 'bigbluebuttonbn'), 'features' => ['showrecordings', 'importrecordings', 'availabilityconditionsheader'] ], ]; return $instanceprofiles; } /** * Helper function returns an array with the profiles (with features per profile) for the different types * of bigbluebuttonbn instances that the user is allowed to create. * * @param bool $room * @param bool $recording * * @return array */ public static function get_instance_type_profiles_create_allowed(bool $room, bool $recording): array { $profiles = self::get_instance_type_profiles(); if (!$room) { unset($profiles[instance::TYPE_ROOM_ONLY]); unset($profiles[instance::TYPE_ALL]); } if (!$recording) { unset($profiles[instance::TYPE_RECORDING_ONLY]); unset($profiles[instance::TYPE_ALL]); } return $profiles; } /** * Helper function returns an array with the profiles (with features per profile) for the different types * of bigbluebuttonbn instances. * * @param array $profiles * * @return array */ public static function get_instance_profiles_array(array $profiles = []): array { $profilesarray = []; foreach ($profiles as $key => $profile) { $profilesarray[$profile['id']] = $profile['name']; } return $profilesarray; } /** * Return the status of an activity [open|not_started|ended]. * * @param instance $instance * @return string */ public static function view_get_activity_status(instance $instance): string { $now = time(); if (!empty($instance->get_instance_var('openingtime')) && $now < $instance->get_instance_var('openingtime')) { // The activity has not been opened. return 'not_started'; } if (!empty($instance->get_instance_var('closingtime')) && $now > $instance->get_instance_var('closingtime')) { // The activity has been closed. return 'ended'; } // The activity is open. return 'open'; } /** * Ensure that the remote server was contactable. * * @param instance $instance */ public static function require_working_server(instance $instance): void { $version = null; try { $version = self::get_server_version(); } catch (server_not_available_exception $e) { self::handle_server_not_available($instance); } if (empty($version)) { self::handle_server_not_available($instance); } } /** * Handle the server not being available. * * @param instance $instance */ public static function handle_server_not_available(instance $instance): void { \core\notification::add( self::get_server_not_available_message($instance), \core\notification::ERROR ); redirect(self::get_server_not_available_url($instance)); } /** * Get message when server not available * * @param instance $instance * @return string */ public static function get_server_not_available_message(instance $instance): string { if ($instance->is_admin()) { return get_string('view_error_unable_join', 'mod_bigbluebuttonbn'); } else if ($instance->is_moderator()) { return get_string('view_error_unable_join_teacher', 'mod_bigbluebuttonbn'); } else { return get_string('view_error_unable_join_student', 'mod_bigbluebuttonbn'); } } /** * Get URL to the page displaying that the server is not available * * @param instance $instance * @return string */ public static function get_server_not_available_url(instance $instance): string { if ($instance->is_admin()) { return new moodle_url('/admin/settings.php', ['section' => 'modsettingbigbluebuttonbn']); } else if ($instance->is_moderator()) { return new moodle_url('/course/view.php', ['id' => $instance->get_course_id()]); } else { return new moodle_url('/course/view.php', ['id' => $instance->get_course_id()]); } } /** * Create a Meeting * * @param array $data * @param array $metadata * @param string|null $presentationname * @param string|null $presentationurl * @param int|null $instanceid * @return array * @throws bigbluebutton_exception */ public static function create_meeting( array $data, array $metadata, ?string $presentationname = null, ?string $presentationurl = null, ?int $instanceid = null ): array { $createmeetingurl = self::action_url('create', $data, $metadata, $instanceid); $curl = new curl(); if (!is_null($presentationname) && !is_null($presentationurl)) { $payload = ""; $xml = $curl->post($createmeetingurl, $payload); } else { $xml = $curl->get($createmeetingurl); } self::assert_returned_xml($xml); if (empty($xml->meetingID)) { throw new bigbluebutton_exception('general_error_cannot_create_meeting'); } if ($xml->hasBeenForciblyEnded === 'true') { throw new bigbluebutton_exception('index_error_forciblyended'); } return [ 'meetingID' => (string) $xml->meetingID, 'internalMeetingID' => (string) $xml->internalMeetingID, 'attendeePW' => (string) $xml->attendeePW, 'moderatorPW' => (string) $xml->moderatorPW ]; } /** * Get meeting info for a given meeting id * * @param string $meetingid * @param int|null $instanceid * @return array */ public static function get_meeting_info(string $meetingid, ?int $instanceid = null): array { $xmlinfo = self::fetch_endpoint_xml('getMeetingInfo', ['meetingID' => $meetingid], [], $instanceid); self::assert_returned_xml($xmlinfo, ['meetingid' => $meetingid]); return (array) $xmlinfo; } /** * Perform end meeting on BBB. * * @param string $meetingid * @param string $modpw * @param int|null $instanceid */ public static function end_meeting(string $meetingid, string $modpw, ?int $instanceid = null): void { $xml = self::fetch_endpoint_xml('end', ['meetingID' => $meetingid, 'password' => $modpw], [], $instanceid); self::assert_returned_xml($xml, ['meetingid' => $meetingid]); } /** * Helper evaluates if the bigbluebutton server used belongs to blindsidenetworks domain. * * @return bool */ public static function is_bn_server() { if (config::get('bn_server')) { return true; } $parsedurl = parse_url(config::get('server_url')); if (!isset($parsedurl['host'])) { return false; } $h = $parsedurl['host']; $hends = explode('.', $h); $hendslength = count($hends); return ($hends[$hendslength - 1] == 'com' && $hends[$hendslength - 2] == 'blindsidenetworks'); } /** * Get the poll interval as it is set in the configuration * * If configuration value is under the threshold of {@see self::MIN_POLL_INTERVAL}, * then return the {@see self::MIN_POLL_INTERVAL} value. * * @return int the poll interval in seconds */ public static function get_poll_interval(): int { $pollinterval = intval(config::get('poll_interval')); if ($pollinterval < self::MIN_POLL_INTERVAL) { $pollinterval = self::MIN_POLL_INTERVAL; } return $pollinterval; } } PK\I;classes/local/bigbluebutton/recordings/recording_action.phpnu[. namespace mod_bigbluebuttonbn\local\bigbluebutton\recordings; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\recording; use mod_bigbluebuttonbn\local\config; /** * Collection of helper methods for handling recordings actions in Moodle. * * Utility class for meeting actions * * @package mod_bigbluebuttonbn * @copyright 2021 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class recording_action { /** * Import recording * * @param recording $recording * @param instance $targetinstance */ public static function import(recording $recording, instance $targetinstance): void { $recording->create_imported_recording($targetinstance); } /** * Helper for performing delete on recordings. * * @param recording $recording */ public static function delete(recording $recording): void { // As the recordingid was not identified as imported recording link, execute delete on a real recording. // Step 1, delete imported links associated to the recording. $recordingstodelete = recording::get_records(['recordingid' => $recording->get('recordingid'), 'imported' => true]); foreach ($recordingstodelete as $rec) { $rec->delete(); } $recording->delete(); } /** * Helper for performing edit on recordings. * * @param recording $recording */ public static function edit(recording $recording): void { $recording->update(); } /** * Helper for performing unprotect on recordings. * * @param recording $recording */ public static function unprotect(recording $recording): void { if (!(boolean) config::get('recording_protect_editable')) { // Recording protect action through UI is disabled, there is no need to do anything else. throw new \moodle_exception('cannotperformaction', 'mod_bigblubuebuttobn', '', 'unprotect'); } if ($recording->get('imported')) { // Imported recordings can not be unprotected. There is no need to do anything else. throw new \moodle_exception('cannotperformaction', 'mod_bigblubuebuttobn', '', 'unprotect'); } $recording->set('protected', false); $recording->update(); } /** * Helper for performing protect on recordings. * * @param recording $recording */ public static function protect(recording $recording): void { if (!(boolean) config::get('recording_protect_editable')) { // Recording protect action through UI is disabled, there is no need to do anything else. throw new \moodle_exception('cannotperformaction', 'mod_bigblubuebuttobn', '', 'protect'); } if ($recording->get('imported')) { // Imported recordings can not be unprotected. There is no need to do anything else. throw new \moodle_exception('cannotperformaction', 'mod_bigblubuebuttobn', '', 'protect'); } $recording->set('protected', true); $recording->update(); } /** * Helper for performing unpublish on recordings. * * @param recording $recording */ public static function unpublish(recording $recording): void { if ($recording->get('imported')) { /* Since the recording link is the one fetched from the BBB server, imported recordings can not be * unpublished. There is no need to do anything else. */ throw new \moodle_exception('cannotperformaction', 'mod_bigblubuebuttobn', '', 'unpublish'); } $recording->set('published', false); $recording->update(); } /** * Helper for performing publish on recordings. * * @param recording $recording */ public static function publish(recording $recording): void { if ($recording->get('imported')) { /* Since the recording link is the one fetched from the BBB server, imported recordings can not be * unpublished. There is no need to do anything else. */ throw new \moodle_exception('cannotperformaction', 'mod_bigblubuebuttobn', '', 'publish'); } $recording->set('published', true); $recording->update(); } } PK\q229classes/local/bigbluebutton/recordings/recording_data.phpnu[. namespace mod_bigbluebuttonbn\local\bigbluebutton\recordings; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\local\config; use mod_bigbluebuttonbn\local\helpers\roles; use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; use mod_bigbluebuttonbn\output\recording_description_editable; use mod_bigbluebuttonbn\output\recording_name_editable; use mod_bigbluebuttonbn\output\recording_row_actionbar; use mod_bigbluebuttonbn\output\recording_row_playback; use mod_bigbluebuttonbn\output\recording_row_preview; use mod_bigbluebuttonbn\recording; use stdClass; /** * The recordings_data. * * @package mod_bigbluebuttonbn * @copyright 2021 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent.david [at] call-learning [dt] fr) * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) */ class recording_data { /** * Get the full recording table * * @param array $recordings * @param array $tools * @param instance|null $instance * @param int $courseid * @return array */ public static function get_recording_table(array $recordings, array $tools, ?instance $instance = null, int $courseid = 0): array { $typeprofiles = bigbluebutton_proxy::get_instance_type_profiles(); $typeprofile = empty($instance) ? $typeprofiles[0] : $typeprofiles[$instance->get_type()]; $lang = get_string('locale', 'core_langconfig'); $locale = substr($lang, 0, strpos($lang, '.')); $tabledata = [ 'activity' => empty($instance) ? '' : bigbluebutton_proxy::view_get_activity_status($instance), 'ping_interval' => (int) config::get('waitformoderator_ping_interval') * 1000, 'locale' => substr($locale, 0, strpos($locale, '_')), 'profile_features' => $typeprofile['features'], 'columns' => [], 'data' => '', ]; $hascapabilityincourse = empty($instance) && roles::has_capability_in_course($courseid, 'mod/bigbluebuttonbn:managerecordings'); $data = []; // Build table content. foreach ($recordings as $recording) { $rowtools = $tools; // Protected recordings may be enabled or disabled from UI through configuration. if (!(boolean) config::get('recording_protect_editable')) { $rowtools = array_diff($rowtools, ['protect', 'unprotect']); } // Protected recordings is not a standard feature, remove actions when protected flag is not present. if (in_array('protect', $rowtools) && $recording->get('protected') === null) { $rowtools = array_diff($rowtools, ['protect', 'unprotect']); } $rowdata = self::row($instance, $recording, $rowtools); if (!empty($rowdata)) { $data[] = $rowdata; } } $columns = [ [ 'key' => 'playback', 'label' => get_string('view_recording_playback', 'bigbluebuttonbn'), 'width' => '125px', 'type' => 'html', 'allowHTML' => true, ], [ 'key' => 'recording', 'label' => get_string('view_recording_name', 'bigbluebuttonbn'), 'width' => '125px', 'type' => 'html', 'allowHTML' => true, ], [ 'key' => 'description', 'label' => get_string('view_recording_description', 'bigbluebuttonbn'), 'sortable' => true, 'width' => '250px', 'type' => 'html', 'allowHTML' => true, ], ]; // Initialize table headers. $ispreviewenabled = !empty($instance) && self::preview_enabled($instance); $ispreviewenabled = $ispreviewenabled || $hascapabilityincourse; if ($ispreviewenabled) { $columns[] = [ 'key' => 'preview', 'label' => get_string('view_recording_preview', 'bigbluebuttonbn'), 'width' => '250px', 'type' => 'html', 'allowHTML' => true, ]; } $columns[] = [ 'key' => 'date', 'label' => get_string('view_recording_date', 'bigbluebuttonbn'), 'sortable' => true, 'width' => '225px', 'type' => 'html', 'formatter' => 'customDate', ]; $columns[] = [ 'key' => 'duration', 'label' => get_string('view_recording_duration', 'bigbluebuttonbn'), 'width' => '50px', 'allowHTML' => false, 'sortable' => true, ]; // Either instance is empty and we must show the toolbar (with restricted content) or we check // specific rights related to the instance. $canmanagerecordings = !empty($instance) && $instance->can_manage_recordings(); $canmanagerecordings = $canmanagerecordings || $hascapabilityincourse; if ($canmanagerecordings) { $columns[] = [ 'key' => 'actionbar', 'label' => get_string('view_recording_actionbar', 'bigbluebuttonbn'), 'width' => '120px', 'type' => 'html', 'allowHTML' => true, ]; } $tabledata['columns'] = $columns; $tabledata['data'] = json_encode($data); return $tabledata; } /** * Helper function builds a row for the data used by the recording table. * * TODO: replace this with templates whenever possible so we just * return the data via the API. * * @param instance|null $instance $instance * @param recording $rec a recording row * @param array|null $tools * @param int|null $courseid * @return stdClass|null */ public static function row(?instance $instance, recording $rec, ?array $tools = null, ?int $courseid = 0): ?stdClass { global $PAGE; $hascapabilityincourse = empty($instance) && roles::has_capability_in_course($courseid, 'mod/bigbluebuttonbn:managerecordings'); $renderer = $PAGE->get_renderer('mod_bigbluebuttonbn'); foreach ($tools as $key => $tool) { if ((!empty($instance) && !$instance->can_perform_on_recordings($tool)) || (empty($instance) && !$hascapabilityincourse)) { unset($tools[$key]); } } if (!self::include_recording_table_row($instance, $rec)) { return null; } $rowdata = new stdClass(); // Set recording_playback. $recordingplayback = new recording_row_playback($rec, $instance); $rowdata->playback = $renderer->render($recordingplayback); if (empty($instance)) { // Set activity name. $rowdata->recording = $rec->get('name'); // Set activity description. $rowdata->description = $rec->get('description'); } else { // Set activity name. $recordingname = new recording_name_editable($rec, $instance); $rowdata->recording = $renderer->render_inplace_editable($recordingname); // Set activity description. $recordingdescription = new recording_description_editable($rec, $instance); $rowdata->description = $renderer->render_inplace_editable($recordingdescription); } if ((!empty($instance) && self::preview_enabled($instance)) || $hascapabilityincourse) { // Set recording_preview. $rowdata->preview = ''; if ($rec->get('playbacks')) { $rowpreview = new recording_row_preview($rec); $rowdata->preview = $renderer->render($rowpreview); } } // Set date. $starttime = $rec->get('starttime'); $rowdata->date = !is_null($starttime) ? floatval($starttime) : 0; // Set duration. $rowdata->duration = self::row_duration($rec); // Set actionbar, if user is allowed to manage recordings. if ((!empty($instance) && $instance->can_manage_recordings()) || $hascapabilityincourse) { $actionbar = new recording_row_actionbar($rec, $tools); $rowdata->actionbar = $renderer->render($actionbar); } return $rowdata; } /** * Helper function evaluates if recording preview should be included. * * @param instance $instance * @return bool */ public static function preview_enabled(instance $instance): bool { return $instance->get_instance_var('recordings_preview') == '1'; } /** * Helper function converts recording duration used in row for the data used by the recording table. * * @param recording $recording * @return int */ protected static function row_duration(recording $recording): int { $playbacks = $recording->get('playbacks'); if (empty($playbacks)) { return 0; } foreach ($playbacks as $playback) { // Ignore restricted playbacks. if (array_key_exists('restricted', $playback) && strtolower($playback['restricted']) == 'true') { continue; } // Take the length form the fist playback with an actual value. if (!empty($playback['length'])) { return intval($playback['length']); } } return 0; } /** * Helper function to handle yet unknown recording types * * @param string $playbacktype : for now presentation, video, statistics, capture, notes, podcast * @return string the matching language string or a capitalised version of the provided string */ public static function type_text(string $playbacktype): string { // Check first if string exists, and if it does not, just default to the capitalised version of the string. $text = ucwords($playbacktype); $typestringid = 'view_recording_format_' . $playbacktype; if (get_string_manager()->string_exists($typestringid, 'bigbluebuttonbn')) { $text = get_string($typestringid, 'bigbluebuttonbn'); } return $text; } /** * Helper function evaluates if recording row should be included in the table. * * @param instance|null $instance * @param recording $rec a bigbluebuttonbn_recordings row * @return bool */ protected static function include_recording_table_row(?instance $instance, recording $rec): bool { if (empty($instance)) { return roles::has_capability_in_course($rec->get('courseid'), 'mod/bigbluebuttonbn:managerecordings'); } // Exclude unpublished recordings, only if user has no rights to manage them. if (!$rec->get('published') && !$instance->can_manage_recordings()) { return false; } // Imported recordings are always shown as long as they are published. if ($rec->get('imported')) { return true; } // When show imported recordings only is enabled, exclude all other recordings. if ($instance->get_recordings_imported() && !$rec->get('imported')) { return false; } // Administrators and moderators are always allowed. if ($instance->is_admin() || $instance->is_moderator()) { return true; } // When groups are enabled, exclude those to which the user doesn't have access to. if ($instance->uses_groups() && !$instance->can_manage_recordings()) { if (groups_get_activity_groupmode($instance->get_cm()) == VISIBLEGROUPS) { // In case we are in visible group mode, we show all recordings. return true; } // Else we check if the Recording group is the same as the instance. Instance group // being the group chosen for this instance. return intval($rec->get('groupid')) === $instance->get_group_id(); } return true; } } PK\>fRRclasses/local/helpers/reset.phpnu[. /** * The mod_bigbluebuttonbn resetting instance helper * * @package mod_bigbluebuttonbn * @copyright 2021 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent [at] call-learning [dt] fr) */ namespace mod_bigbluebuttonbn\local\helpers; use context_module; use core_tag_tag; use mod_bigbluebuttonbn\local\config; use mod_bigbluebuttonbn\recording; /** * Utility class for resetting instance routines helper * * @package mod_bigbluebuttonbn * @copyright 2021 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class reset { /** * Used by the reset_course_userdata for deleting recordings * * This will delete recordings in the database and not in the remote BBB server. * * @param int $courseid */ public static function reset_recordings(int $courseid): void { // Criteria for search : courseid or bigbluebuttonbn=null or subset=false or includedeleted=true. $recordings = recording::get_recordings_for_course( $courseid, [], // Exclude itself. false, true ); if ($recordings) { // Remove all the recordings. foreach ($recordings as $recording) { $recording->delete(); } } } /** * Used by the reset_course_userdata for deleting tags linked to bigbluebuttonbn instances in the course. * * @param int $courseid */ public static function reset_tags(int $courseid): void { global $DB; // Remove all the tags linked to the room/activities in this course. if ($bigbluebuttonbns = $DB->get_records('bigbluebuttonbn', ['course' => $courseid])) { foreach ($bigbluebuttonbns as $bigbluebuttonbn) { if (!$cm = get_coursemodule_from_instance('bigbluebuttonbn', $bigbluebuttonbn->id, $courseid)) { continue; } $context = context_module::instance($cm->id); core_tag_tag::delete_instances('mod_bigbluebuttonbn', null, $context->id); } } } /** * Used by the reset_course_userdata for deleting events linked to bigbluebuttonbn instances in the course. * * @param string $courseid * @return bool status */ public static function reset_events($courseid) { global $DB; // Remove all the events. return $DB->delete_records('event', ['modulename' => 'bigbluebuttonbn', 'courseid' => $courseid]); } /** * Returns status used on every defined reset action. * * @param string $item * @return array status array */ public static function reset_getstatus(string $item): array { return ['component' => get_string('modulenameplural', 'bigbluebuttonbn'), 'item' => get_string("removed{$item}", 'bigbluebuttonbn'), 'error' => false]; } /** * Define items to be reset by course/reset.php * * @return array */ public static function reset_course_items(): array { $items = ["events" => 0, "tags" => 0, "logs" => 0]; // Include recordings only if enabled. if ((boolean) config::recordings_enabled()) { $items["recordings"] = 0; } return $items; } /** * Reset logs for each BBB instance of this course * * @param int $courseid * @return bool status */ public static function reset_logs(int $courseid) { global $DB; return $DB->delete_records('bigbluebuttonbn_logs', ['courseid' => $courseid]); } } PK\0 0 $classes/local/helpers/mod_helper.phpnu[. namespace mod_bigbluebuttonbn\local\helpers; use calendar_event; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\logger; use mod_bigbluebuttonbn\plugin; use stdClass; /** * Utility class for all instance (module) routines helper. * * @package mod_bigbluebuttonbn * @copyright 2021 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent [at] call-learning [dt] fr) */ class mod_helper { /** * Runs any processes that must run before a bigbluebuttonbn insert/update. * * @param stdClass $bigbluebuttonbn BigBlueButtonBN form data **/ public static function process_pre_save(stdClass $bigbluebuttonbn) { self::process_pre_save_instance($bigbluebuttonbn); self::process_pre_save_checkboxes($bigbluebuttonbn); self::process_pre_save_common($bigbluebuttonbn); $bigbluebuttonbn->participants = htmlspecialchars_decode($bigbluebuttonbn->participants, ENT_COMPAT); // Conditionally force grade type to none if the activity is recording only. if ($bigbluebuttonbn->type == instance::TYPE_RECORDING_ONLY) { $bigbluebuttonbn->grade = GRADE_TYPE_NONE; } } /** * Runs process for defining the instance (insert/update). * * @param stdClass $bigbluebuttonbn BigBlueButtonBN form data **/ protected static function process_pre_save_instance(stdClass $bigbluebuttonbn): void { $bigbluebuttonbn->timemodified = time(); if ((integer) $bigbluebuttonbn->instance == 0) { $bigbluebuttonbn->meetingid = 0; $bigbluebuttonbn->timecreated = time(); $bigbluebuttonbn->timemodified = 0; // As it is a new activity, assign passwords. $bigbluebuttonbn->moderatorpass = plugin::random_password(12); $bigbluebuttonbn->viewerpass = plugin::random_password(12, $bigbluebuttonbn->moderatorpass); } } /** * Runs process for assigning default value to checkboxes. * * @param stdClass $bigbluebuttonbn BigBlueButtonBN form data **/ protected static function process_pre_save_checkboxes($bigbluebuttonbn) { if (!isset($bigbluebuttonbn->wait)) { $bigbluebuttonbn->wait = 0; } if (!isset($bigbluebuttonbn->record)) { $bigbluebuttonbn->record = 0; } if (!isset($bigbluebuttonbn->recordallfromstart)) { $bigbluebuttonbn->recordallfromstart = 0; } if (!isset($bigbluebuttonbn->recordhidebutton)) { $bigbluebuttonbn->recordhidebutton = 0; } if (!isset($bigbluebuttonbn->recordings_html)) { $bigbluebuttonbn->recordings_html = 0; } if (!isset($bigbluebuttonbn->recordings_deleted)) { $bigbluebuttonbn->recordings_deleted = 0; } if (!isset($bigbluebuttonbn->recordings_imported)) { $bigbluebuttonbn->recordings_imported = 0; } if (!isset($bigbluebuttonbn->recordings_preview)) { $bigbluebuttonbn->recordings_preview = 0; } if (!isset($bigbluebuttonbn->muteonstart)) { $bigbluebuttonbn->muteonstart = 0; } if (!isset($bigbluebuttonbn->disablecam)) { $bigbluebuttonbn->disablecam = 0; } if (!isset($bigbluebuttonbn->disablemic)) { $bigbluebuttonbn->disablemic = 0; } if (!isset($bigbluebuttonbn->disableprivatechat)) { $bigbluebuttonbn->disableprivatechat = 0; } if (!isset($bigbluebuttonbn->disablepublicchat)) { $bigbluebuttonbn->disablepublicchat = 0; } if (!isset($bigbluebuttonbn->disablenote)) { $bigbluebuttonbn->disablenote = 0; } if (!isset($bigbluebuttonbn->hideuserlist)) { $bigbluebuttonbn->hideuserlist = 0; } } /** * Runs process for wipping common settings when 'recordings only'. * * @param stdClass $bigbluebuttonbn BigBlueButtonBN form data **/ protected static function process_pre_save_common(stdClass $bigbluebuttonbn): void { // Make sure common settings are removed when 'recordings only'. if ($bigbluebuttonbn->type == instance::TYPE_RECORDING_ONLY) { $bigbluebuttonbn->groupmode = 0; $bigbluebuttonbn->groupingid = 0; } } /** * Runs any processes that must be run after a bigbluebuttonbn insert/update. * * @param stdClass $bigbluebuttonbn BigBlueButtonBN form data **/ public static function process_post_save(stdClass $bigbluebuttonbn): void { self::process_post_save_event($bigbluebuttonbn); self::process_post_save_completion($bigbluebuttonbn); } /** * Generates an event after a bigbluebuttonbn insert/update. * * @param stdClass $bigbluebuttonbn BigBlueButtonBN form data **/ protected static function process_post_save_event(stdClass $bigbluebuttonbn): void { global $CFG, $DB; require_once($CFG->dirroot . '/calendar/lib.php'); $eventid = $DB->get_field('event', 'id', [ 'modulename' => 'bigbluebuttonbn', 'instance' => $bigbluebuttonbn->id, 'eventtype' => logger::EVENT_MEETING_START ]); // Delete the event from calendar when/if openingtime is NOT set. if (!isset($bigbluebuttonbn->openingtime) || !$bigbluebuttonbn->openingtime) { if ($eventid) { $calendarevent = calendar_event::load($eventid); $calendarevent->delete(); } return; } // Add event to the calendar as openingtime is set. $event = (object) [ 'eventtype' => logger::EVENT_MEETING_START, 'type' => CALENDAR_EVENT_TYPE_ACTION, 'name' => get_string('calendarstarts', 'bigbluebuttonbn', $bigbluebuttonbn->name), 'description' => format_module_intro('bigbluebuttonbn', $bigbluebuttonbn, $bigbluebuttonbn->coursemodule, false), 'format' => FORMAT_HTML, 'courseid' => $bigbluebuttonbn->course, 'groupid' => 0, 'userid' => 0, 'modulename' => 'bigbluebuttonbn', 'instance' => $bigbluebuttonbn->id, 'timestart' => $bigbluebuttonbn->openingtime, 'timeduration' => 0, 'timesort' => $bigbluebuttonbn->openingtime, 'visible' => instance_is_visible('bigbluebuttonbn', $bigbluebuttonbn), 'priority' => null, ]; // Update the event in calendar when/if eventid was found. if ($eventid) { $event->id = $eventid; $calendarevent = calendar_event::load($eventid); $calendarevent->update($event); return; } calendar_event::create($event); } /** * Generates an event after a bigbluebuttonbn activity is completed. * * @param stdClass $bigbluebuttonbn BigBlueButtonBN form data **/ protected static function process_post_save_completion(stdClass $bigbluebuttonbn): void { if (empty($bigbluebuttonbn->completionexpected)) { return; } \core_completion\api::update_completion_date_event( $bigbluebuttonbn->coursemodule, 'bigbluebuttonbn', $bigbluebuttonbn->id, $bigbluebuttonbn->completionexpected ); } } PK\{B--classes/local/helpers/files.phpnu[. /** * The mod_bigbluebuttonbn files helper * * @package mod_bigbluebuttonbn * @copyright 2021 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent [at] call-learning [dt] fr) */ namespace mod_bigbluebuttonbn\local\helpers; use cache; use cache_store; use context; use context_module; use context_system; use mod_bigbluebuttonbn\instance; use moodle_url; use stdClass; /** * Utility class for all files routines helper * * @package mod_bigbluebuttonbn * @copyright 2021 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class files { /** * Helper for validating pluginfile. * * @param stdClass $context context object * @param string $filearea file area * * @return bool|null false if file not valid */ public static function pluginfile_valid(stdClass $context, string $filearea): ?bool { // Can be in context module or in context_system (if is the presentation by default). if (!in_array($context->contextlevel, [CONTEXT_MODULE, CONTEXT_SYSTEM])) { return false; } if (!array_key_exists($filearea, self::get_file_areas())) { return false; } return true; } /** * Helper for getting pluginfile. * * @param stdClass|null $course course object * @param stdClass|null $cm course module object * @param context $context context object * @param string $filearea file area * @param array $args extra arguments * * @return \stored_file|bool */ public static function pluginfile_file(?stdClass $course, ?stdClass $cm, context $context, string $filearea, array $args) { $filename = self::get_plugin_filename($course, $cm, $context, $args); if (!$filename) { return false; } $fullpath = "/$context->id/mod_bigbluebuttonbn/$filearea/0/" . $filename; $fs = get_file_storage(); $file = $fs->get_file_by_hash(sha1($fullpath)); if (!$file || $file->is_directory()) { return false; } return $file; } /** * Get a full path to the file attached as a preuploaded presentation * or if there is none, set the presentation field will be set to blank. * * @param stdClass $bigbluebuttonformdata BigBlueButtonBN form data * Note that $bigbluebuttonformdata->presentation is the id of the filearea whereas the bbb instance table * stores the file name/path * @return string */ public static function save_media_file(stdClass &$bigbluebuttonformdata): string { if (!isset($bigbluebuttonformdata->presentation) || $bigbluebuttonformdata->presentation == '') { return ''; } $context = context_module::instance($bigbluebuttonformdata->coursemodule); // Set the filestorage object. $fs = get_file_storage(); // Save the file if it exists that is currently in the draft area. file_save_draft_area_files($bigbluebuttonformdata->presentation, $context->id, 'mod_bigbluebuttonbn', 'presentation', 0); // Get the file if it exists. $files = $fs->get_area_files( $context->id, 'mod_bigbluebuttonbn', 'presentation', 0, 'itemid, filepath, filename', false ); // Check that there is a file to process. $filesrc = ''; if (count($files) == 1) { // Get the first (and only) file. $file = reset($files); $filesrc = '/' . $file->get_filename(); } return $filesrc; } /** * Helper return array containing the file descriptor for a preuploaded presentation. * * @param context $context * @param string $presentation matching presentation file name * @param int $id bigbluebutton instance id * @param bool $withnonce add nonce to the url * @return array|null the representation of the presentation as an associative array */ public static function get_presentation(context $context, string $presentation, $id = null, $withnonce = false): ?array { global $CFG; $fs = get_file_storage(); $files = []; $defaultpresentation = $fs->get_area_files( context_system::instance()->id, 'mod_bigbluebuttonbn', 'presentationdefault', 0, "filename", false ); $activitypresentation = $files = $fs->get_area_files( $context->id, 'mod_bigbluebuttonbn', 'presentation', false, 'itemid, filepath, filename', false ); // Presentation upload logic based on config settings. if (empty($defaultpresentation)) { if (empty($activitypresentation) || !\mod_bigbluebuttonbn\local\config::get('preuploadpresentation_editable')) { return null; } $files = $activitypresentation; } else { if (empty($activitypresentation) || !\mod_bigbluebuttonbn\local\config::get('preuploadpresentation_editable')) { $files = $defaultpresentation; $id = null; } else { $files = $activitypresentation; } } $pnoncevalue = 0; if ($withnonce) { $nonceid = 0; if (!is_null($id)) { $instance = instance::get_from_instanceid($id); $nonceid = $instance->get_instance_id(); } $pnoncevalue = self::generate_nonce($nonceid); } $file = null; foreach ($files as $f) { if (basename($f->get_filename()) == basename($presentation)) { $file = $f; } } if (!$file && !empty($files)) { $file = reset($files); } if (empty($file)) { return null; // File was not found. } // Note: $pnoncevalue is an int. $url = moodle_url::make_pluginfile_url( $file->get_contextid(), $file->get_component(), $file->get_filearea(), $withnonce ? $pnoncevalue : null, // Hack: item id as a nonce. $file->get_filepath(), $file->get_filename() ); return [ 'icondesc' => get_mimetype_description($file), 'iconname' => file_file_icon($file), 'name' => $file->get_filename(), 'url' => $url->out(false), ]; } /** * Helper for getting pluginfile name. * * @param stdClass|null $course course object * @param stdClass|null $cm course module object * @param context $context context object * @param array $args extra arguments * * @return string|null */ public static function get_plugin_filename(?stdClass $course, ?stdClass $cm, context $context, array $args): ?string { global $DB; if ($context->contextlevel != CONTEXT_SYSTEM) { // Plugin has a file to use as default in general setting. // The difference with the standard bigbluebuttonbn_pluginfile_filename() are. // - Context is system, so we don't need to check the cmid in this case. // - The area is "presentationdefault_cache". if (!$DB->get_record('bigbluebuttonbn', ['id' => $cm->instance])) { return null; } } // Plugin has a file to use as default in general setting. // The difference with the standard bigbluebuttonbn_pluginfile_filename() are. // - Context is system, so we don't need to check the cmid in this case. // - The area is "presentationdefault_cache". if (count($args) > 1) { $id = 0; if ($cm) { $instance = instance::get_from_cmid($cm->id); $id = $instance->get_instance_id(); } $actualnonce = self::get_nonce($id); return ($args['0'] == $actualnonce) ? $args['1'] : null; } if (!empty($course)) { require_course_login($course, true, $cm, true, true); } else { require_login(null, true, $cm, true, true); } // If the file belongs to a course module, ensure user can join the activity. if ($cm !== null && !has_capability('mod/bigbluebuttonbn:join', $context)) { return null; } return implode('/', $args); } /** * Helper generates a salt used for the preuploaded presentation callback url. * * @param int $id * @return int */ protected static function get_nonce(int $id): int { $cache = static::get_nonce_cache(); $pnoncekey = sha1($id); $existingnoncedata = $cache->get($pnoncekey); if ($existingnoncedata) { if ($existingnoncedata->counter > 0) { $existingnoncedata->counter--; $cache->set($pnoncekey, $existingnoncedata); return $existingnoncedata->nonce; } } // The item id was adapted for granting public access to the presentation once in order to allow BigBlueButton to gather // the file once. return static::generate_nonce($id); } /** * Generate a nonce and store it in the cache * * @param int $id * @return int */ protected static function generate_nonce($id): int { $cache = static::get_nonce_cache(); $pnoncekey = sha1($id); // The item id was adapted for granting public access to the presentation once in order to allow BigBlueButton to gather // the file once. $pnoncevalue = ((int) microtime()) + mt_rand(); $cache->set($pnoncekey, (object) ['nonce' => $pnoncevalue, 'counter' => 2]); return $pnoncevalue; } /** * Get cache for nonce * * @return \cache_application|\cache_session|cache_store */ private static function get_nonce_cache() { return cache::make_from_params( cache_store::MODE_APPLICATION, 'mod_bigbluebuttonbn', 'presentation_cache' ); } /** * Returns an array of file areas. * * @return array a list of available file areas * */ protected static function get_file_areas(): array { $areas = []; $areas['presentation'] = get_string('mod_form_block_presentation', 'bigbluebuttonbn'); $areas['presentationdefault'] = get_string('mod_form_block_presentation_default', 'bigbluebuttonbn'); return $areas; } } PK\ƪy==classes/local/helpers/roles.phpnu[. /** * The mod_bigbluebuttonbn roles helper * * @package mod_bigbluebuttonbn * @copyright 2021 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent [at] call-learning [dt] fr) */ namespace mod_bigbluebuttonbn\local\helpers; use cache; use cache_store; use core\context; use core\context\{course, system}; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; use stdClass; /** * Utility class for all roles routines helper * * @package mod_bigbluebuttonbn * @copyright 2021 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class roles { /** @var int The bigbluebutton viewer role */ public const ROLE_VIEWER = 'viewer'; /** @var string The bigbluebutton moderator role */ public const ROLE_MODERATOR = 'moderator'; /** * Returns user roles in a context. * * @param context $context * @param int $userid * * @return array $userroles */ public static function get_user_roles(context $context, int $userid) { global $DB; $userroles = get_user_roles($context, $userid); if ($userroles) { $where = ''; foreach ($userroles as $userrole) { $where .= (empty($where) ? ' WHERE' : ' OR') . ' id=' . $userrole->roleid; } $userroles = $DB->get_records_sql('SELECT * FROM {role}' . $where); } return $userroles; } /** * Returns guest role wrapped in an array. * * @return array */ protected static function get_guest_role() { $guestrole = get_guest_role(); return [$guestrole->id => $guestrole]; } /** * Returns an array containing all the users in a context wrapped for html select element. * * @param course $context * @param null $bbactivity * @return array $users */ public static function get_users_array(course $context, $bbactivity = null) { // CONTRIB-7972, check the group of current user and course group mode. $groups = null; $users = (array) get_enrolled_users($context, '', 0, 'u.*', null, 0, 0, true); $course = get_course($context->instanceid); $groupmode = groups_get_course_groupmode($course); if ($bbactivity) { list($bbcourse, $cm) = get_course_and_cm_from_instance($bbactivity->id, 'bigbluebuttonbn'); $groupmode = groups_get_activity_groupmode($cm); } if ($groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $context)) { global $USER; $groups = groups_get_all_groups($course->id, $USER->id); $users = []; foreach ($groups as $g) { $users += (array) get_enrolled_users($context, '', $g->id, 'u.*', null, 0, 0, true); } } return array_map( function($u) { return ['id' => $u->id, 'name' => fullname($u)]; }, $users); } /** * Can do some administration in this course, likely manage recordings * * @param int $courseid * @param string $capability */ public static function has_capability_in_course(int $courseid, string $capability) { global $DB; if (empty($courseid) || !$DB->record_exists('course', ['id' => $courseid])) { return has_capability('moodle/site:config', system::instance()); } return has_capability($capability, course::instance($courseid)); } /** * Returns an array containing all the roles in a context. * * @param context|null $context $context * @param bool|null $onlyviewableroles * * @return array $roles */ public static function get_roles(?context $context = null, ?bool $onlyviewableroles = true) { global $CFG; if ($onlyviewableroles == true && $CFG->branch >= 35) { $roles = (array) get_viewable_roles($context); foreach ($roles as $key => $value) { $roles[$key] = $value; } } else { $roles = (array) role_get_names($context); foreach ($roles as $key => $value) { $roles[$key] = $value->localname; } } return $roles; } /** * Returns an array containing all the roles in a context wrapped for html select element. * * @param context|null $context $context * @param bool $onlyviewableroles * * @return array $users */ protected static function get_roles_select(?context $context = null, bool $onlyviewableroles = true) { global $CFG; if ($onlyviewableroles == true && $CFG->branch >= 35) { $roles = (array) get_viewable_roles($context); foreach ($roles as $key => $value) { $roles[$key] = ['id' => $key, 'name' => $value]; } } else { $roles = (array) role_get_names($context); foreach ($roles as $key => $value) { $roles[$key] = ['id' => $value->id, 'name' => $value->localname]; } } return $roles; } /** * Returns role that corresponds to an id. * * @param string|integer $id * * @return stdClass|null $role */ protected static function get_role($id): ?stdClass { $roles = (array) role_get_names(); if (is_numeric($id) && isset($roles[$id])) { return (object) $roles[$id]; } foreach ($roles as $role) { if ($role->shortname == $id) { return $role; } } return null; } /** * Returns an array to populate a list of participants used in mod_form.js. * * @param context $context * @param null|stdClass $bbactivity * @return array $data */ public static function get_participant_data(context $context, ?stdClass $bbactivity = null) { $data = [ 'all' => [ 'name' => get_string('mod_form_field_participant_list_type_all', 'bigbluebuttonbn'), 'children' => [] ], ]; $data['role'] = [ 'name' => get_string('mod_form_field_participant_list_type_role', 'bigbluebuttonbn'), 'children' => self::get_roles_select($context, true) ]; $data['user'] = [ 'name' => get_string('mod_form_field_participant_list_type_user', 'bigbluebuttonbn'), 'children' => [], ]; if (empty($bbactivity)) { return $data; } $data['user']['children'] = self::get_users_array($context, $bbactivity); return $data; } /** * Returns an array to populate a list of participants used in mod_form.php. * * @param stdClass|null $bigbluebuttonbn * @param context $context * * @return array */ public static function get_participant_list(?stdClass $bigbluebuttonbn, context $context): array { global $USER; if ($bigbluebuttonbn == null) { return self::get_participant_rules_encoded( self::get_participant_list_default($context, $USER->id) ); } if (empty($bigbluebuttonbn->participants)) { $bigbluebuttonbn->participants = "[]"; } $rules = json_decode($bigbluebuttonbn->participants, true); if (empty($rules)) { $rules = self::get_participant_list_default($context, bigbluebutton_proxy::get_instance_ownerid($bigbluebuttonbn)); } return self::get_participant_rules_encoded($rules); } /** * Returns an array to populate a list of participants used in mod_form.php with default values. * * @param context $context * @param int|null $ownerid * * @return array */ protected static function get_participant_list_default(context $context, ?int $ownerid = null) { $participantlist = []; $participantlist[] = [ 'selectiontype' => 'all', 'selectionid' => 'all', 'role' => self::ROLE_VIEWER, ]; $defaultrules = explode(',', \mod_bigbluebuttonbn\local\config::get('participant_moderator_default')); foreach ($defaultrules as $defaultrule) { if ($defaultrule == '0') { if (!empty($ownerid) && is_enrolled($context, $ownerid)) { $participantlist[] = [ 'selectiontype' => 'user', 'selectionid' => (string) $ownerid, 'role' => self::ROLE_MODERATOR]; } continue; } $participantlist[] = [ 'selectiontype' => 'role', 'selectionid' => $defaultrule, 'role' => self::ROLE_MODERATOR]; } return $participantlist; } /** * Returns an array to populate a list of participants used in mod_form.php with bigbluebuttonbn values. * * @param array $rules * * @return array */ protected static function get_participant_rules_encoded(array $rules): array { foreach ($rules as $key => $rule) { if ($rule['selectiontype'] !== 'role' || is_numeric($rule['selectionid'])) { continue; } $role = self::get_role($rule['selectionid']); if ($role == null) { unset($rules[$key]); continue; } $rule['selectionid'] = $role->id; $rules[$key] = $rule; } return $rules; } /** * Returns an array to populate a list of participant_selection used in mod_form.php. * * @return array */ public static function get_participant_selection_data(): array { return [ 'type_options' => [ 'all' => get_string('mod_form_field_participant_list_type_all', 'bigbluebuttonbn'), 'role' => get_string('mod_form_field_participant_list_type_role', 'bigbluebuttonbn'), 'user' => get_string('mod_form_field_participant_list_type_user', 'bigbluebuttonbn'), ], 'type_selected' => 'all', 'options' => ['all' => '---------------'], 'selected' => 'all', ]; } /** * Evaluate if a user in a context is moderator based on roles and participation rules. * * @param context $context * @param array $participantlist * @param int $userid * * @return bool */ public static function is_moderator(context $context, array $participantlist, ?int $userid = null): bool { global $USER; // If an admin, then also a moderator. if (has_capability('moodle/site:config', $context)) { return true; } if (!is_array($participantlist)) { return false; } if (empty($userid)) { $userid = $USER->id; } $userroles = self::get_guest_role(); if (!isguestuser()) { $userroles = self::get_user_roles($context, $userid); } return self::is_moderator_validator($participantlist, $userid, $userroles); } /** * Iterates participant list rules to evaluate if a user is moderator. * * @param array $participantlist * @param int $userid * @param array $userroles * * @return bool */ protected static function is_moderator_validator(array $participantlist, int $userid, array $userroles): bool { // Iterate participant rules. foreach ($participantlist as $participant) { if (self::is_moderator_validate_rule($participant, $userid, $userroles)) { return true; } } return false; } /** * Evaluate if a user is moderator based on roles and a particular participation rule. * * @param array $participant * @param int $userid * @param array $userroles * * @return bool */ protected static function is_moderator_validate_rule(array $participant, int $userid, array $userroles): bool { if ($participant['role'] == self::ROLE_VIEWER) { return false; } // Validation for the 'all' rule. if ($participant['selectiontype'] == 'all') { return true; } // Validation for a 'user' rule. if ($participant['selectiontype'] == 'user') { if ($participant['selectionid'] == $userid) { return true; } return false; } // Validation for a 'role' rule. $role = self::get_role($participant['selectionid']); if ($role != null && array_key_exists($role->id, $userroles)) { return true; } return false; } /** * Updates the meeting info cached object when a participant has joined. * * @param string $meetingid * @param bool $ismoderator * * @return void */ public static function participant_joined(string $meetingid, bool $ismoderator): void { $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'mod_bigbluebuttonbn', 'meetings_cache'); $result = $cache->get($meetingid); $meetinginfo = json_decode($result['meeting_info']); $meetinginfo->participantCount += 1; if ($ismoderator) { $meetinginfo->moderatorCount += 1; } $cache->set($meetingid, ['creation_time' => $result['creation_time'], 'meeting_info' => json_encode($meetinginfo)]); } /** * Helper function returns a list of courses a user has access to, wrapped in an array that can be used * by a html select. * * @param instance $instance * @return array */ public static function import_get_courses_for_select(instance $instance): array { if ($instance->is_admin()) { $courses = get_courses('all', 'c.fullname ASC', 'c.id, c.shortname, c.fullname'); // It includes the name of the site as a course (category 0), so remove the first one. unset($courses['1']); } else { $courses = enrol_get_users_courses($instance->get_user_id(), false, 'id,shortname,fullname'); } $courses = array_filter($courses, function($course) { $modules = get_fast_modinfo($course->id); return !empty($modules->instances['bigbluebuttonbn']); }); $coursesforselect = []; foreach ($courses as $course) { $coursesforselect[$course->id] = format_string( get_course_display_name_for_list($course), options: ['context' => course::instance($course->id)], ); } return $coursesforselect; } } PK\lj2 #classes/local/helpers/user_info.phpnu[. namespace mod_bigbluebuttonbn\local\helpers; use cm_info; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\logger; use stdClass; /** * Utility class for all user information * * Used mainly in user_outline and user_complete * * @package mod_bigbluebuttonbn * @copyright 2022 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent [at] call-learning [dt] fr) */ class user_info { /** * Event to watch for. */ const EVENT_TO_WATCH = [ 'join' => logger::EVENT_JOIN, 'play_recording' => logger::EVENT_PLAYED ]; /** * Get user outline and complete info * * @param stdClass $course * @param stdClass $user * @param cm_info $mod * @return array[] an array of infos and timestamps (latest timestamp) */ public static function get_user_info_outline(stdClass $course, stdClass $user, cm_info $mod): array { $completion = new \completion_info($course); $cdata = $completion->get_data($mod, false, $user->id); $logtimestamps = []; $infos = []; if (!empty($cdata->viewed) && $cdata->viewed) { $infos[] = get_string('report_room_view', 'mod_bigbluebuttonbn'); $logtimestamps[] = $cdata->timemodified; } $instance = instance::get_from_cmid($mod->id); foreach (self::EVENT_TO_WATCH as $eventtype => $logtype) { $logs = logger::get_user_completion_logs($instance, $user->id, [$logtype]); if ($logs) { $infos[] = get_string("report_{$eventtype}_info", 'mod_bigbluebuttonbn', count($logs)); $latesttime = array_reduce($logs, function($acc, $log) { return ($acc > $log->timecreated) ? $acc : $log->timecreated; }, 0); $logtimestamps[] = $latesttime; } } return [$infos, $logtimestamps]; } } PK\## +classes/local/extension/mod_form_addons.phpnu[. namespace mod_bigbluebuttonbn\local\extension; use stdClass; /** * A class for the main mod form extension * * @package mod_bigbluebuttonbn * @copyright 2023 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ abstract class mod_form_addons { /** * @var \MoodleQuickForm|null moodle form */ protected $mform = null; /** * @var stdClass|null $bigbluebuttonbndata BigBlueButton data if any */ protected $bigbluebuttonbndata = null; /** * @var string|null $suffix suffix for form elements */ protected $suffix = null; /** * Constructor * * @param \MoodleQuickForm $mform * @param stdClass|null $bigbluebuttonbndata * @param string|null $suffix */ public function __construct(\MoodleQuickForm &$mform, ?stdClass $bigbluebuttonbndata = null, ?string $suffix = null) { $this->mform = $mform; $this->bigbluebuttonbndata = $bigbluebuttonbndata; $this->suffix = $suffix; } /** * Add new form field definition */ abstract public function add_fields(): void; /** * Validate form and returns an array of errors indexed by field name * * @param array $data * @param array $files * @return array */ abstract public function validation(array $data, array $files): array; /** * Allows modules to modify the data returned by form get_data(). * This method is also called in the bulk activity completion form. * * Only available on moodleform_mod. * * @param stdClass $data passed by reference */ abstract public function data_postprocessing(\stdClass &$data): void; /** * Can be overridden to add custom completion rules if the module wishes * them. If overriding this, you should also override completion_rule_enabled. *

* Just add elements to the form as needed and return the list of IDs. The * system will call disabledIf and handle other behaviour for each returned * ID. * * @return string[] Array of string IDs of added items, empty array if none */ abstract public function add_completion_rules(): array; /** * Called during validation. Override to indicate, based on the data, whether * a custom completion rule is enabled (selected). * * @param array $data Input data (not yet validated) * @return bool True if one or more rules is enabled, false if none are; * default returns false */ public function completion_rule_enabled(array $data): bool { return false; } /** * Form adjustments after setting data * * @return void */ public function definition_after_data() { // Nothing for now. } } PK\k/classes/local/extension/mod_instance_helper.phpnu[. namespace mod_bigbluebuttonbn\local\extension; use stdClass; /** * Class defining a way to deal with instance save/update/delete in extension * * @package mod_bigbluebuttonbn * @copyright 2023 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ class mod_instance_helper { /** * Runs any processes that must run before a bigbluebuttonbn insert/update. * * @param stdClass $bigbluebuttonbn BigBlueButtonBN form data */ public function add_instance(stdClass $bigbluebuttonbn) { // Nothing for now. } /** * Runs any processes that must be run after a bigbluebuttonbn insert/update. * * @param stdClass $bigbluebuttonbn BigBlueButtonBN form data */ public function update_instance(stdClass $bigbluebuttonbn): void { // Nothing for now. } /** * Runs any processes that must be run after a bigbluebuttonbn delete. * * @param int $cmid */ public function delete_instance(int $cmid): void { } /** * Get any join table name that is used to store additional data for the instance. * @return string[] */ public function get_join_tables(): array { return []; } } PK\< -8classes/local/extension/broker_meeting_events_addons.phpnu[. namespace mod_bigbluebuttonbn\local\extension; use mod_bigbluebuttonbn\instance; /** * A class to deal with broker addons in a subplugin * * @package mod_bigbluebuttonbn * @copyright 2024 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) */ abstract class broker_meeting_events_addons { /** * @var instance $instance BigBlueButton instance if any */ protected $instance; /** * @var string $data data to be processed */ protected $data; /** * Constructor * * @param instance $instance BigBlueButton instance * @param string $data data to be processed */ public function __construct(instance $instance, string $data) { $this->instance = $instance; $this->data = $data; } /** * Data processing action */ abstract public function process_action(); } PK\kg9 4classes/local/extension/custom_completion_addons.phpnu[. namespace mod_bigbluebuttonbn\local\extension; use cm_info; /** * A class to deal with completion rules addons in a subplugin * * @package mod_bigbluebuttonbn * @copyright 2023 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ abstract class custom_completion_addons { /** @var cm_info The course module information object. */ protected $cm; /** @var int The user's ID. */ protected $userid; /** @var array The current state of core completion */ protected $completionstate; /** * activity_custom_completion constructor. * * @param cm_info $cm * @param int $userid * @param array|null $completionstate The current state of the core completion criteria */ public function __construct(cm_info $cm, int $userid, ?array $completionstate = null) { $this->cm = $cm; $this->userid = $userid; $this->completionstate = $completionstate; } /** * Fetches the completion state for a given completion rule. * * @param string $rule The completion rule. * @return int The completion state. */ abstract public function get_state(string $rule): int; /** * Fetch the list of custom completion rules that this module defines. * * @return array */ abstract public static function get_defined_custom_rules(): array; /** * Returns an associative array of the descriptions of custom completion rules. * * @return array */ abstract public function get_custom_rule_descriptions(): array; /** * Returns an array of all completion rules, in the order they should be displayed to users. * * @return array */ abstract public function get_sort_order(): array; } PK\*22-classes/local/extension/action_url_addons.phpnu[. namespace mod_bigbluebuttonbn\local\extension; /** * A single action class to mutate the action URL. * * @package mod_bigbluebuttonbn * @copyright 2023 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ class action_url_addons { /** * Mutate the action URL. * * By design: * 1. we should only add parameters * 2. we cannot count on the order the subplugins are called * * @param string $action * @param array $data * @param array $metadata * @return array associative array with the additional data and metadata (indexed by 'data' and * 'metadata' keys). */ public function execute(string $action = '', array $data = [], array $metadata = []): array { return ['data' => [], 'metadata' => []]; } } PK\h++.classes/local/plugins/admin_plugin_manager.phpnu[. namespace mod_bigbluebuttonbn\local\plugins; use cache_helper; use context_system; use core_component; use core_plugin_manager; use flexible_table; use html_writer; use mod_bigbluebuttonbn\extension; use moodle_url; use pix_icon; /** * Class that handles the display and configuration of the list of extension plugins. * * This is directly taken from the mod_assign code. We might need to have a global API there for this. * * @package mod_bigbluebuttonbn * @copyright 2023 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ class admin_plugin_manager { /** @var object the url of the manage submission plugin page */ private $pageurl; /** * Constructor for this assignment plugin manager * */ public function __construct() { $this->pageurl = new moodle_url(admin_page_manage_extensions::ADMIN_PAGE_URL); } /** * This is the entry point for this controller class. * * @param string|null $action - The action to perform * @param string|null $plugin - Optional name of a plugin type to perform the action on * @return void */ public function execute(?string $action = null, ?string $plugin = null): void { if (empty($action) || empty($plugin)) { $action = 'view'; } $this->check_permissions(); $actionname = "plugins_$action"; if (method_exists($this, $actionname)) { $nextaction = $this->$actionname($plugin); if ($nextaction) { $this->execute($nextaction, $plugin); } } } /** * Check this user has permission to edit the list of installed plugins * * @return void */ private function check_permissions(): void { require_login(); $systemcontext = context_system::instance(); require_capability('moodle/site:config', $systemcontext); } /** * Write the HTML for the submission plugins table. * * @return void */ private function plugins_view(): void { global $OUTPUT, $CFG; require_once($CFG->libdir . '/tablelib.php'); $this->print_header(); $table = new flexible_table(extension::BBB_EXTENSION_PLUGIN_NAME . 'pluginsadminttable'); $table->define_baseurl($this->pageurl); $table->define_columns([ 'pluginname', 'version', 'hideshow', 'order', 'settings', 'uninstall' ]); $table->define_headers([ get_string('subplugintype_bbbext', 'mod_bigbluebuttonbn'), get_string('version'), get_string('hide') . '/' . get_string('show'), get_string('order'), get_string('settings'), get_string('uninstallplugin', 'core_admin') ]); $table->set_attribute('id', extension::BBB_EXTENSION_PLUGIN_NAME . 'plugins'); $table->set_attribute('class', 'admintable generaltable'); $table->setup(); $plugins = $this->get_sorted_plugins_list(); $instances = core_plugin_manager::instance()->get_plugins_of_type(extension::BBB_EXTENSION_PLUGIN_NAME); foreach ($plugins as $idx => $plugin) { $componentname = extension::BBB_EXTENSION_PLUGIN_NAME . '_' . $plugin; $typebasedir = ""; if (in_array($plugin, array_keys($instances))) { $typebasedir = ($instances[$plugin])->typerootdir; } $row = []; $class = ''; $pluginversion = get_config($componentname, 'version'); $row[] = get_string('pluginname', $componentname); $row[] = $pluginversion; $visible = !get_config($componentname, 'disabled'); if ($visible) { $row[] = $this->format_icon_link('hide', $plugin, 't/hide', get_string('disable')); } else { $row[] = $this->format_icon_link('show', $plugin, 't/show', get_string('enable')); $class = 'dimmed_text'; } $movelinks = ''; if (!$idx == 0) { $movelinks .= $this->format_icon_link('moveup', $plugin, 't/up', get_string('up')) . ' '; } else { $movelinks .= $OUTPUT->spacer(['width' => 16]); } if ($idx != count($plugins) - 1) { $movelinks .= $this->format_icon_link('movedown', $plugin, 't/down', get_string('down')) . ' '; } $row[] = $movelinks; $exists = file_exists($typebasedir . '/' . $plugin . '/settings.php'); // We do not display settings for plugin who have not yet been installed (so have no version yet). if (!empty($pluginversion) && $exists) { $row[] = html_writer::link( new moodle_url('/admin/settings.php', ['section' => $componentname]), get_string('settings') ); } else { $row[] = ' '; } $url = core_plugin_manager::instance()->get_uninstall_url( $componentname, 'manage' ); if ($url) { $row[] = html_writer::link($url, get_string('uninstallplugin', 'core_admin')); } else { $row[] = ' '; } $table->add_data($row, $class); } $table->finish_output(); $this->print_footer(); } /** * Write the page header * * @return void */ private function print_header(): void { global $OUTPUT; $pageidentifier = 'manage' . extension::BBB_EXTENSION_PLUGIN_NAME . 'plugins'; admin_externalpage_setup($pageidentifier); echo $OUTPUT->header(); echo $OUTPUT->heading(get_string($pageidentifier, 'mod_bigbluebuttonbn')); } /** * Return a list of plugins sorted by the order defined in the admin interface * * @return array The list of plugins */ public function get_sorted_plugins_list(): array { $names = core_component::get_plugin_list(extension::BBB_EXTENSION_PLUGIN_NAME); return extension::get_sorted_plugins_list($names); } /** * Util function for writing an action icon link * * @param string $action URL parameter to include in the link * @param string $plugin URL parameter to include in the link * @param string $icon The key to the icon to use (e.g. 't/up') * @param string $alt The string description of the link used as the title and alt text * @return string The icon/link */ private function format_icon_link(string $action, string $plugin, string $icon, string $alt): string { global $OUTPUT; return $OUTPUT->action_icon( new moodle_url( $this->pageurl, ['action' => $action, 'plugin' => $plugin, 'sesskey' => sesskey()] ), new pix_icon($icon, $alt, 'moodle', ['title' => $alt]), null, ['title' => $alt] ); } /** * Write the page footer * * @return void */ private function print_footer(): void { global $OUTPUT; echo $OUTPUT->footer(); } /** * Hide this plugin. * * @param string $plugin - The plugin to hide * @return string The next page to display */ private function plugins_hide(string $plugin): string { $class = \core_plugin_manager::resolve_plugininfo_class(extension::BBB_EXTENSION_PLUGIN_NAME); $class::enable_plugin($plugin, false); cache_helper::purge_by_event('mod_bigbluebuttonbn/pluginenabledisabled'); // Also clear the cache for all BigBlueButtonModules. rebuild_course_cache(0, true); return 'view'; } /** * Show this plugin. * * @param string $plugin - The plugin to show * @return string The next page to display */ private function plugins_show(string $plugin): string { $class = \core_plugin_manager::resolve_plugininfo_class(extension::BBB_EXTENSION_PLUGIN_NAME); $class::enable_plugin($plugin, true); cache_helper::purge_by_event('mod_bigbluebuttonbn/pluginenabledisabled'); return 'view'; } /** * Move this plugin up * * We need this function so we can call directly (without the dir parameter) * @param string $plugintomove - The plugin to move * @return string The next page to display */ private function plugins_moveup(string $plugintomove): string { return $this->move_plugin($plugintomove, 'up'); } /** * Move this plugin down * * We need this function so we can call directly (without the dir parameter) * @param string $plugintomove - The plugin to move * @return string The next page to display */ private function plugins_movedown(string $plugintomove): string { return $this->move_plugin($plugintomove, 'down'); } /** * Change the order of this plugin. * * @param string $plugintomove - The plugin to move * @param string $dir - up or down * @return string The next page to display */ private function move_plugin(string $plugintomove, string $dir): string { $plugins = $this->get_sorted_plugins_list(); $plugins = array_values($plugins); $currentindex = array_search($plugintomove, $plugins); if ($currentindex === false) { return 'view'; } // Make the switch. if ($dir === 'up') { if ($currentindex > 0) { $tempplugin = $plugins[$currentindex - 1]; $plugins[$currentindex - 1] = $plugins[$currentindex]; $plugins[$currentindex] = $tempplugin; } } else if ($dir === 'down') { if ($currentindex < (count($plugins) - 1)) { $tempplugin = $plugins[$currentindex + 1]; $plugins[$currentindex + 1] = $plugins[$currentindex]; $plugins[$currentindex] = $tempplugin; } } // Save the new normal order. foreach ($plugins as $key => $plugin) { set_config('sortorder', $key, extension::BBB_EXTENSION_PLUGIN_NAME . '_' . $plugin); } return 'view'; } } PK\6ېo o 6classes/local/plugins/admin_page_manage_extensions.phpnu[. namespace mod_bigbluebuttonbn\local\plugins; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->libdir . '/adminlib.php'); use admin_externalpage; use core_component; use core_text; use mod_bigbluebuttonbn\extension; use moodle_url; /** * Admin external page that displays a list of the installed extension plugins. * * @package mod_bigbluebuttonbn * @copyright 2023 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ class admin_page_manage_extensions extends admin_externalpage { /** * Global URL for page. */ const ADMIN_PAGE_URL = '/mod/bigbluebuttonbn/adminmanageplugins.php'; /** * The constructor - calls parent constructor * */ public function __construct() { $url = new moodle_url(self::ADMIN_PAGE_URL); $managepagename = 'manage' . extension::BBB_EXTENSION_PLUGIN_NAME . 'plugins'; parent::__construct( $managepagename, get_string($managepagename, 'mod_bigbluebuttonbn'), $url ); } /** * Search plugins for the specified string * * @param string $query The string to search for * @return array */ public function search($query): array { if ($result = parent::search($query)) { return $result; } foreach (core_component::get_plugin_list(extension::BBB_EXTENSION_PLUGIN_NAME ) as $name => $notused) { $pluginname = core_text::strtolower( get_string('pluginname', extension::BBB_EXTENSION_PLUGIN_NAME . '_' . $name) ); if (str_contains($pluginname, $query) !== false) { $result = (object)[ 'page' => $this, 'settings' => [], ]; return [$this->name => $result]; } } return []; } } PK\ڪ;classes/local/exceptions/server_not_available_exception.phpnu[. namespace mod_bigbluebuttonbn\local\exceptions; /** * Class server_not_available_exception * * This kind of error cannot be recovered and should be displayed to the user * signaling that there is an error in the configuration. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent [at] call-learning [dt] fr) */ class server_not_available_exception extends \moodle_exception { /** * Constructor * * @param string $errorcode The name of the string from error.php to print * @param string $module name of module * @param string $link The url where the user will be prompted to continue. If no url is provided the user will be directed to * the site index page. * @param mixed $a Extra words and phrases that might be required in the error string * @param string $debuginfo optional debugging information */ public function __construct($errorcode, $module = '', $link = '', $a = null, $debuginfo = null) { global $CFG; $hasdebugdeveloper = ( isset($CFG->debugdisplay) && isset($CFG->debug) && $CFG->debugdisplay && $CFG->debug === DEBUG_DEVELOPER ); if ($hasdebugdeveloper && is_null($debuginfo)) { $debuginfo = $this->getTraceAsString(); } parent::__construct($errorcode, $module, $link, $a, $debuginfo); } } PK\UúKK3classes/local/exceptions/meeting_join_exception.phpnu[. namespace mod_bigbluebuttonbn\local\exceptions; use mod_bigbluebuttonbn\plugin; /** * The mod_bigbluebuttonbn cannot join meeting exception. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent [at] call-learning [dt] fr) */ class meeting_join_exception extends \moodle_exception { /** * Constructor * * @param string $errorcode The name of the string from error.php to print */ public function __construct($errorcode) { parent::__construct($errorcode, plugin::COMPONENT); } } PK\ @4classes/local/exceptions/bigbluebutton_exception.phpnu[. namespace mod_bigbluebuttonbn\local\exceptions; use mod_bigbluebuttonbn\plugin; /** * Class bigbluebutton_exception generic exception. This is supposed to be recoverable. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent [at] call-learning [dt] fr) */ class bigbluebutton_exception extends \moodle_exception { /** * Constructor * * @param string $errorcode The name of the string from error.php to print * @param mixed $additionalinfo Extra words and phrases that might be required in the error string */ public function __construct($errorcode, $additionalinfo = null) { parent::__construct($errorcode, plugin::COMPONENT, '', $additionalinfo); } } PK\OT16(6(classes/extension.phpnu[. namespace mod_bigbluebuttonbn; use cache; use cm_info; use mod_bigbluebuttonbn\local\extension\action_url_addons; use mod_bigbluebuttonbn\local\extension\broker_meeting_events_addons; use mod_bigbluebuttonbn\local\extension\custom_completion_addons; use mod_bigbluebuttonbn\local\extension\mod_form_addons; use mod_bigbluebuttonbn\local\extension\mod_instance_helper; use stdClass; use core_plugin_manager; use core_component; /** * Generic subplugin management helper * * @package mod_bigbluebuttonbn * @copyright 2023 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ class extension { /** * Plugin name for extension */ const BBB_EXTENSION_PLUGIN_NAME = 'bbbext'; /** * Invoke a subplugin hook that will return additional parameters * * @param string $action * @param array $data * @param array $metadata * @param int|null $instanceid * @return array associative array with the additional data and metadata (indexed by 'data' and * 'metadata' keys). */ public static function action_url_addons( string $action = '', array $data = [], array $metadata = [], ?int $instanceid = null ): array { $allmutationclass = self::get_instances_implementing(action_url_addons::class); $additionaldata = []; $additionalmetadata = []; foreach ($allmutationclass as $mutationclass) { // Here we intentionally just pass data and metadata and not the result as we // do not want subplugin to assume that another subplugin is doing a modification. ['data' => $newdata, 'metadata' => $newmetadata] = $mutationclass->execute($action, $data, $metadata, $instanceid); $additionaldata = array_merge($additionaldata, $newdata ?? []); $additionalmetadata = array_merge($additionalmetadata, $newmetadata ?? []); } return [ 'data' => $additionaldata, 'metadata' => $additionalmetadata ]; } /** * Get new instance of classes that are named on the base of this classname and implementing this class * * @param string $classname * @param array|null $newparameters additional parameters for the constructor. * @return array */ protected static function get_instances_implementing(string $classname, ?array $newparameters = []): array { $classes = self::get_classes_implementing($classname); ksort($classes); // Make sure all extension classes are returned in the correct order. return array_map(function($targetclassname) use ($newparameters) { // If $newparameters is null, the constructor will be called without parameters. return new $targetclassname(...$newparameters); }, $classes); } /** * Get classes are named on the base of this classname and implementing this class * * @param string $classname * @return array */ protected static function get_classes_implementing(string $classname): array { // Get the class basename without Reflection API. $classnamecomponents = explode("\\", $classname); $classbasename = end($classnamecomponents); $allsubs = core_plugin_manager::instance()->get_plugins_of_type(self::BBB_EXTENSION_PLUGIN_NAME); $extensionclasses = []; $names = core_component::get_plugin_list(self::BBB_EXTENSION_PLUGIN_NAME); $sortedlist = self::get_sorted_plugins_list($names); // Make sure to use the most updated list. $sortedlist = array_flip($sortedlist); foreach ($allsubs as $sub) { if (!$sub->is_enabled()) { continue; } $targetclassname = "\\bbbext_{$sub->name}\\bigbluebuttonbn\\$classbasename"; if (!class_exists($targetclassname)) { continue; } if (!is_subclass_of($targetclassname, $classname)) { debugging("The class $targetclassname should extend $classname in the subplugin {$sub->name}. Ignoring."); continue; } if (!isset($sortedlist[$sub->name])) { debugging("The class $targetclassname does not belong to an existing subplugin. Ignoring"); continue; } // Return all extension classes based on subplugin order on manage extension page. $sortorder = $sortedlist[$sub->name]; $extensionclasses[$sortorder] = $targetclassname; } return $extensionclasses; } /** * Return plugin list sorted according to order from admin extension manager. * @param array $names Array of plugin names * @return array The sorted list of plugins */ public static function get_sorted_plugins_list(array $names): array { $result = []; foreach ($names as $name => $path) { $idx = get_config(self::BBB_EXTENSION_PLUGIN_NAME . '_' . $name, 'sortorder'); if (!$idx) { $idx = 0; } while (array_key_exists($idx, $result)) { $idx += 1; } $result[$idx] = $name; } ksort($result); return $result; } /** * Get all custom_completion addons classes. * * @return array of custom completion addon classes. */ public static function custom_completion_addons_classes(): array { return self::get_classes_implementing(custom_completion_addons::class); } /** * Get all custom_completion addons classes instances. * * @param cm_info $cm * @param int $userid * @param array|null $completionstate * @return array of custom completion addon instances. */ public static function custom_completion_addons_instances(cm_info $cm, int $userid, ?array $completionstate = null): array { return self::get_instances_implementing(custom_completion_addons::class, [$cm, $userid, $completionstate]); } /** * Get all mod_form addons classes instances * * @param \MoodleQuickForm $mform * @param stdClass|null $bigbluebuttondata * @param string|null $suffix * @return array of custom completion addon classes instances */ public static function mod_form_addons_instances(\MoodleQuickForm $mform, ?stdClass $bigbluebuttondata = null, ?string $suffix = null): array { return self::get_instances_implementing(mod_form_addons::class, [$mform, $bigbluebuttondata, $suffix]); } /** * Get additional join tables for instance when extension activated * * @return array of additional tables names. They all have a field called bigbluebuttonbnid that identifies the bbb instance. */ public static function get_join_tables(): array { global $DB; // We use cache here as it will be called very often. $cache = cache::make('mod_bigbluebuttonbn', 'subplugins'); if ($cache->get('additionaltables')) { return $cache->get('additionaltables'); } $additionaltablesclasses = self::get_instances_implementing(mod_instance_helper::class); $tables = []; foreach ($additionaltablesclasses as $tableclass) { $tables = array_merge($tables, $tableclass->get_join_tables() ?? []); } // Warning and removal for tables that do not have the bigbluebuttonid field. foreach ($tables as $index => $table) { $columns = $DB->get_columns($table); if (empty($columns['bigbluebuttonbnid'])) { debugging("get_instance_additional_tables: $table should have a column named bigbluebuttonid"); unset($tables[$index]); } } $cache->set('additionaltables', $tables); return $tables; } /** * Add instance processing * * @param stdClass $data data to persist * @return void */ public static function add_instance(stdClass $data): void { $formmanagersclasses = self::get_instances_implementing(mod_instance_helper::class); foreach ($formmanagersclasses as $fmclass) { $fmclass->add_instance($data); } } /** * Update instance processing * * @param stdClass $data data to persist * @return void */ public static function update_instance(stdClass $data): void { $formmanagersclasses = self::get_instances_implementing(mod_instance_helper::class); foreach ($formmanagersclasses as $fmclass) { $fmclass->update_instance($data); } } /** * Delete instance processing * * @param int $id instance id * @return void */ public static function delete_instance(int $id): void { $formmanagersclasses = self::get_instances_implementing(mod_instance_helper::class); foreach ($formmanagersclasses as $fmclass) { $fmclass->delete_instance($id); } } /** * Get all broker_meeting_events addons classes instances * * @param instance|null $instance * @param string|null $data * @return array of custom completion addon classes instances */ public static function broker_meeting_events_addons_instances(instance $instance, string $data): array { return self::get_instances_implementing(broker_meeting_events_addons::class, [$instance, $data]); } } PK\W; classes/broker.phpnu[. namespace mod_bigbluebuttonbn; use Exception; use Firebase\JWT\JWT; use Firebase\JWT\Key; use mod_bigbluebuttonbn\local\config; /** * The broker routines * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) */ class broker { /** * Validate the supplied list of parameters, providing feedback about any missing or incorrect values. * * @param array $params * @return null|string */ public static function validate_parameters(array $params): ?string { $requiredparams = [ 'recording_ready' => [ 'bigbluebuttonbn' => 'The BigBlueButtonBN instance ID must be specified.', 'signed_parameters' => 'A JWT encoded string must be included as [signed_parameters].', ], 'meeting_events' => [ 'bigbluebuttonbn' => 'The BigBlueButtonBN instance ID must be specified.', ], ]; if (!isset($params['action']) || empty($params['action'])) { return 'Parameter [' . $params['action'] . '] was not included'; } $action = strtolower($params['action']); if (!array_key_exists($action, $requiredparams)) { return "Action {$params['action']} can not be performed."; } return self::validate_parameters_message($params, $requiredparams[$action]); } /** * Check whether the specified parameter is valid. * * @param array $params * @param array $requiredparams * @return null|string */ protected static function validate_parameters_message(array $params, array $requiredparams): ?string { foreach ($requiredparams as $param => $message) { if (!array_key_exists($param, $params) || $params[$param] == '') { return $message; } } // Everything is valid. return null; } /** * Helper for responding when recording ready is performed. * * @param instance $instance * @param array $params */ public static function process_recording_ready(instance $instance, array $params): void { // Decodes the received JWT string. try { $decodedparameters = JWT::decode( $params['signed_parameters'], new Key(config::get('shared_secret'), 'HS256') ); } catch (Exception $e) { $error = 'Caught exception: ' . $e->getMessage(); header('HTTP/1.0 400 Bad Request. ' . $error); return; } // Validations. if (!isset($decodedparameters->record_id)) { header('HTTP/1.0 400 Bad request. Missing record_id parameter'); return; } $recording = recording::get_record(['recordingid' => $decodedparameters->record_id]); if (!isset($recording)) { header('HTTP/1.0 400 Bad request. Invalid record_id'); return; } // Sends the messages. try { // We make sure messages are sent only once. if ($recording->get('status') != recording::RECORDING_STATUS_NOTIFIED) { $task = new \mod_bigbluebuttonbn\task\send_recording_ready_notification(); $task->set_instance_id($instance->get_instance_id()); \core\task\manager::queue_adhoc_task($task); $recording->set('status', recording::RECORDING_STATUS_NOTIFIED); $recording->update(); } header('HTTP/1.0 202 Accepted'); } catch (Exception $e) { $error = 'Caught exception: ' . $e->getMessage(); header('HTTP/1.0 503 Service Unavailable. ' . $error); } } /** * Process meeting events for instance with provided HTTP headers. * * @param instance $instance * @return void */ public static function process_meeting_events(instance $instance) { try { // Get the HTTP headers. $authorization = self::get_authorization_token(); // Pull the Bearer from the headers. if (empty($authorization)) { $msg = 'Authorization failed'; header('HTTP/1.0 400 Bad Request. ' . $msg); return; } // Verify the authenticity of the request. $token = \Firebase\JWT\JWT::decode( $authorization[1], new Key(config::get('shared_secret'), 'HS512') ); // Get JSON string from the body. $jsonstr = file_get_contents('php://input'); // Convert JSON string to a JSON object. $jsonobj = json_decode($jsonstr); $headermsg = meeting::meeting_events($instance, $jsonobj); self::process_extension_actions($instance, $jsonstr); } catch (Exception $e) { $msg = 'Caught exception: ' . $e->getMessage(); debugging($msg, DEBUG_DEVELOPER); $headermsg = 'HTTP/1.0 400 Bad Request. ' . $msg; } header($headermsg); } /** * Process meeting events extension actions. * * @param instance $instance * @param string $jsonstr * @return void */ protected static function process_extension_actions(instance $instance, string $jsonstr) { // Hooks for extensions. $extensions = extension::broker_meeting_events_addons_instances($instance, $jsonstr); foreach ($extensions as $extension) { $extension->process_action(); } } /** * Get authorisation token * * We could use getallheaders but this is only compatible with apache types of servers * some explanations and examples here: https://www.php.net/manual/en/function.getallheaders.php#127190 * * @return array|null an array composed of the Authorization token provided in the header. */ private static function get_authorization_token(): ?array { $autorization = null; if (isset($_SERVER['Authorization'])) { $autorization = trim($_SERVER["Authorization"]); } else if (isset($_SERVER['HTTP_AUTHORIZATION'])) { $autorization = trim($_SERVER["HTTP_AUTHORIZATION"]); } else if (function_exists('apache_request_headers')) { $requestheaders = apache_request_headers(); $requestheaders = array_combine(array_map('ucwords', array_keys($requestheaders)), array_values($requestheaders)); if (isset($requestheaders['Authorization'])) { $autorization = trim($requestheaders['Authorization']); } } return empty($autorization) ? null : explode(" ", $autorization); } } PK\zclasses/plugininfo/bbbext.phpnu[. namespace mod_bigbluebuttonbn\plugininfo; use core\plugininfo\base; use mod_bigbluebuttonbn\extension; /** * Subplugin extension info class. * * @package mod_bigbluebuttonbn * @copyright 2022 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ class bbbext extends base { /** * Check if BigBlueButton plugin is enabled * @return bool */ private static function is_bbb_enabled(): bool { $enabledplugins = \core\plugininfo\mod::get_enabled_plugins(); return isset($enabledplugins['bigbluebuttonbn']); } /** * Get a list of enabled plugins * * @return array * @throws \dml_exception */ public static function get_enabled_plugins(): array { // If the mod_bigbluebuttonbn is not enabled, then all subplugin are disabled. if (!self::is_bbb_enabled()) { return []; } // Get all available plugins. $plugins = \core_plugin_manager::instance()->get_installed_plugins(extension::BBB_EXTENSION_PLUGIN_NAME); if (!$plugins) { return []; } // Check they are enabled using get_config (which is cached and hopefully fast). $enabled = []; foreach ($plugins as $plugin => $version) { $disabled = get_config(extension::BBB_EXTENSION_PLUGIN_NAME . '_' . $plugin, 'disabled'); if (empty($disabled)) { $enabled[$plugin] = $plugin; } } return $enabled; } /** * Enable the plugin * * @param string $pluginname * @param int $enabled * @return bool * @throws \dml_exception */ public static function enable_plugin(string $pluginname, int $enabled): bool { $haschanged = false; // If the mod_bigbluebuttonbn is not enabled, then all subplugin are disabled. if (!self::is_bbb_enabled()) { return false; } $plugin = extension::BBB_EXTENSION_PLUGIN_NAME. '_' . $pluginname; $oldvalue = get_config($plugin, 'disabled'); $disabled = !$enabled; // Only set value if there is no config setting or if the value is different from the previous one. if ($oldvalue == false && $disabled) { set_config('disabled', $disabled, $plugin); $haschanged = true; } else if ($oldvalue != false && !$disabled) { unset_config('disabled', $plugin); $haschanged = true; } if ($haschanged) { add_to_config_log('disabled', $oldvalue, $disabled, $plugin); \core_plugin_manager::reset_caches(); } return $haschanged; } /** * Loads plugin settings to the settings tree * * This function usually includes settings.php file in plugins folder. * Alternatively it can create a link to some settings page (instance of admin_externalpage) * * @param \part_of_admin_tree $adminroot * @param string $parentnodename * @param bool $hassiteconfig whether the current user has moodle/site:config capability */ public function load_settings(\part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) { // If the mod_bigbluebuttonbn is not enabled, then all subplugin are disabled. if (!self::is_bbb_enabled()) { return; } $ADMIN = $adminroot; $plugininfo = $this; if (!$this->is_installed_and_upgraded()) { return; } if (!$hassiteconfig || !file_exists($this->full_path('settings.php'))) { return; } $section = $this->get_settings_section_name(); $settings = new \admin_settingpage($section, $this->displayname, 'moodle/site:config', $this->is_enabled() === false); if ($adminroot->fulltree) { $shortsubtype = substr($this->type, strlen(extension::BBB_EXTENSION_PLUGIN_NAME)); include($this->full_path('settings.php')); } $adminroot->add($this->type . 'plugins', $settings); } /** * Get settings section name * * @return string */ public function get_settings_section_name() { return $this->type . '_' . $this->name; } /** * Should there be a way to uninstall the plugin via the administration UI. * * * @return bool */ public function is_uninstall_allowed() { return true; } } PK\muuclasses/instance.phpnu[. namespace mod_bigbluebuttonbn; use cm_info; use context; use context_course; use context_module; use core\dml\table; use mod_bigbluebuttonbn\local\config; use mod_bigbluebuttonbn\local\helpers\files; use mod_bigbluebuttonbn\local\helpers\roles; use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; use moodle_url; use stdClass; /** * Instance record for mod_bigbluebuttonbn. * * @package mod_bigbluebuttonbn * @copyright 2021 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class instance { /** @var int Defines an instance type that includes room and recordings */ public const TYPE_ALL = 0; /** @var int Defines an instance type that includes only room */ public const TYPE_ROOM_ONLY = 1; /** @var int Defines an instance type that includes only recordings */ public const TYPE_RECORDING_ONLY = 2; /** @var cm_info The cm_info object relating to the instance */ protected $cm; /** @var stdClass The course that the instance is in */ protected $course; /** @var stdClass The instance data for the instance */ protected $instancedata; /** @var context The current context */ protected $context; /** @var array The list of participants */ protected $participantlist; /** @var int The current groupid if set */ protected $groupid; /** @var int The course module id. */ protected $cmid; /** * Instance constructor. * * Never called directly. Use self::get_from_instanceid or self::get_from_cmid. * * @param int $cmid * @param stdClass $course * @param stdClass $instancedata * @param int|null $groupid */ private function __construct(int $cmid, stdClass $course, stdClass $instancedata, ?int $groupid = null) { $this->cmid = $cmid; $this->cm = null; // This is not retrieved later, whenever we call ::get_cm() it will be retrieved. $this->course = $course; $this->instancedata = $instancedata; $this->groupid = $groupid; } /** * Get a group instance of the specified instance. * * @param self $originalinstance * @param int $groupid * @return null|self */ public static function get_group_instance_from_instance(self $originalinstance, int $groupid): ?self { return new self( $originalinstance->get_cm_id(), $originalinstance->get_course(), $originalinstance->get_instance_data(), $groupid ); } /** * Get the instance information from an instance id. * * @param int $instanceid The id from the bigbluebuttonbn table * @return null|self */ public static function get_from_instanceid(int $instanceid): ?self { return self::get_instance_info_retriever($instanceid, self::IDTYPE_INSTANCEID); } /** * Get the instance information from a cmid. * * @param int $cmid * @return null|self */ public static function get_from_cmid(int $cmid): ?self { return self::get_instance_info_retriever($cmid, self::IDTYPE_CMID); } /** * Get the instance information from a cmid. */ const IDTYPE_CMID = 0; /** * Get the instance information from an id. */ const IDTYPE_INSTANCEID = 1; /** * Helper to get the instance information from an id. * * Used by self::get_from_id and self::get_cmid. * * @param int $id The id to look for. * @param int $idtype self::IDTYPE_CMID or self::IDTYPE_INSTANCEID * @return null|self * @throws \moodle_exception */ private static function get_instance_info_retriever(int $id, int $idtype = self::IDTYPE_INSTANCEID): ?self { global $DB; if (!in_array($idtype, [self::IDTYPE_CMID, self::IDTYPE_INSTANCEID])) { throw new \moodle_exception('Invalid idtype'); } [ 'coursetable' => $coursetable, 'courseselect' => $courseselect, 'coursefrom' => $coursefrom, 'cmfrom' => $cmfrom, 'cmselect' => $cmselect, 'bbbtable' => $bbbtable, 'bbbselect' => $bbbselect, 'bbbfrom' => $bbbfrom, 'subplugintables' => $subplugintables, 'subpluginselects' => $subpluginselects, 'subpluginfroms' => $subpluginfroms ] = self::get_tables_info(); $select = implode(', ', array_merge([$courseselect, $bbbselect, $cmselect], $subpluginselects)); $subpluginsleftjoins = ''; foreach ($subpluginfroms as $tablealias => $subpluginfrom) { $subpluginsleftjoins .= "LEFT JOIN {$subpluginfrom} ON bbb.id = {$tablealias}.bigbluebuttonbnid\n"; } $params = [ 'modname' => 'bigbluebuttonbn', 'bbbid' => $id, ]; $where = 'bbb.id = :bbbid'; $from = <<get_record_sql($sql, $params); if (empty($result)) { return null; } $course = $coursetable->extract_from_result($result); $instancedata = $bbbtable->extract_from_result($result); self::extract_plugin_table_info($instancedata, $result, $subplugintables); if ($idtype == self::IDTYPE_INSTANCEID) { $cmid = $result->cmid; } else { $cmid = $id; } return new self($cmid, $course, $instancedata); } /** * Get the instance information from a meetingid. * * If a group is specified in the meetingid then this will also be set. * * @param string $meetingid * @return null|self */ public static function get_from_meetingid(string $meetingid): ?self { global $DB; // Here we try to manage cases where the meetingid was actually produced by the old plugin or we have actually // changed the identifiers for the instance. $matches = self::parse_meetingid($meetingid); $existinginstanceid = $DB->get_field('bigbluebuttonbn', 'id', ['meetingid' => $matches['meetingid']]); if (empty($existinginstanceid)) { debugging("The meeting id with ID ($meetingid) was not found in the bigbluebuttonbn table", DEBUG_DEVELOPER); $existinginstanceid = $matches['instanceid']; // We try to "guess" the meeting id from its instance id. We should // not really do that as this changes simply if we move the course elsewhere. debugging("Trying to get the instanceid from the meeting ID. This will soon be deprecated", DEBUG_DEVELOPER); } $instance = self::get_from_instanceid($existinginstanceid); // Check for the group if any. if ($instance && array_key_exists('groupid', $matches)) { $instance->set_group_id($matches['groupid']); } return $instance; } /** * Parse a meetingID for key data. * * @param string $meetingid * @return array * @throws \moodle_exception */ public static function parse_meetingid(string $meetingid): array { $result = preg_match( '@(?P[^-]*)-(?P[^-]*)-(?P\d+)(\[(?P\d*)\])?@', $meetingid, $matches ); if ($result !== 1) { throw new \moodle_exception("The supplied meeting id '{$meetingid}' is invalid found."); } return $matches; } /** * Get all instances in the specified course. * * @param int $courseid * @return self[] */ public static function get_all_instances_in_course(int $courseid): array { global $DB; [ 'coursetable' => $coursetable, 'courseselect' => $courseselect, 'coursefrom' => $coursefrom, 'cmfrom' => $cmfrom, 'bbbtable' => $bbbtable, 'bbbselect' => $bbbselect, 'bbbfrom' => $bbbfrom, 'subplugintables' => $subplugintables, 'subpluginselects' => $subpluginselects, 'subpluginfroms' => $subpluginfroms ] = self::get_tables_info(); $selects = implode(', ', array_merge([$courseselect, $bbbselect], $subpluginselects)); $subpluginsleftjoins = ''; foreach ($subpluginfroms as $tablealias => $subpluginfrom) { $subpluginsleftjoins .= "LEFT JOIN {$subpluginfrom} ON bbb.id = {$tablealias}.bigbluebuttonbnid\n"; } $sql = <<get_records_sql($sql, [ 'modname' => 'bigbluebuttonbn', 'courseid' => $courseid, ]); $instances = []; foreach ($results as $result) { $course = $coursetable->extract_from_result($result); $instancedata = $bbbtable->extract_from_result($result); self::extract_plugin_table_info($instancedata, $result, $subplugintables); $instances[$result->cmid] = new self($result->cmid, $course, $instancedata); } return $instances; } /** * Helper method to extract result from subplugin tables. * @param object $instancedata instance data * @param object $result result from sql query * @param array $subplugintables array of subplugin tables */ private static function extract_plugin_table_info(object &$instancedata, object $result, array $subplugintables) { foreach ($subplugintables as $subplugintable) { $subplugindata = (array) $subplugintable->extract_from_result($result); if (isset($subplugindata['id'])) { unset($subplugindata['id']); // Make sure that from the subplugin we don't conflict with the bigbluebutton id. } $instancedata = (object) array_merge($subplugindata, (array) $instancedata); } } /** * Get the additional tables returned from the subplugin. * * @return array */ private static function get_tables_info(): array { $coursetable = new table('course', 'c', 'c'); $courseselect = $coursetable->get_field_select(); $coursefrom = $coursetable->get_from_sql(); $cmtable = new table('course_modules', 'cm', 'cm'); $cmselect = $cmtable->get_field_select(); $cmfrom = $cmtable->get_from_sql(); $bbbtable = new table('bigbluebuttonbn', 'bbb', 'b'); $bbbselect = $bbbtable->get_field_select(); $bbbfrom = $bbbtable->get_from_sql(); // Look now for additional tables returned from the subplugin. $subpluginselects = []; $subpluginfroms = []; $subplugintables = []; $subplugintablesnames = extension::get_join_tables(); foreach ($subplugintablesnames as $index => $subplugintablename) { $tablealias = 'ext'.$index; $subplugintable = new table($subplugintablename, $tablealias, 'ext'.$index); $subpluginselects[$tablealias] = $subplugintable->get_field_select(); $subpluginfroms[$tablealias] = $subplugintable->get_from_sql(); $subplugintables[$tablealias] = $subplugintable; } return compact( 'coursetable', 'courseselect', 'coursefrom', 'cmtable', 'cmselect', 'cmfrom', 'bbbtable', 'bbbselect', 'bbbfrom', 'subplugintables', 'subpluginselects', 'subpluginfroms', ); } /** * Set the current group id of the activity. * * @param int $groupid */ public function set_group_id(int $groupid): void { $this->groupid = $groupid; } /** * Get the current groupid if set. * * @return int */ public function get_group_id(): int { return empty($this->groupid) ? 0 : $this->groupid; } /** * Check whether this instance is configured to use a group. * * @return bool */ public function uses_groups(): bool { $groupmode = groups_get_activity_groupmode($this->get_cm()); return $groupmode != NOGROUPS; } /** * Get the group name for the current group, if a group has been set. * * @return null|string */ public function get_group_name(): ?string { $groupid = $this->get_group_id(); if (!$this->uses_groups()) { return null; } if ($groupid == 0) { return get_string('allparticipants'); } return format_string(groups_get_group_name($groupid), true, ['context' => $this->get_context()]); } /** * Get the course object for the instance. * * @return stdClass */ public function get_course(): stdClass { return $this->course; } /** * Get the course id of the course that the instance is in. * * @return int */ public function get_course_id(): int { return $this->course->id; } /** * Get the cm_info object for the instance. * * @return cm_info */ public function get_cm(): cm_info { if ($this->cm === null) { // We do a sort of late binding here as if we call get_cm on a disabled module or in a call stack where // get_cm was already called, we will get an exception or infinite loop. $modinfo = get_fast_modinfo($this->course); $this->cm = $modinfo->get_cm($this->cmid); } return $this->cm; } /** * Get the id of the course module. * * @return int */ public function get_cm_id(): int { return $this->cmid; } /** * Get the context. * * @return context_module */ public function get_context(): context_module { if ($this->context === null) { $this->context = context_module::instance($this->get_cm()->id); } return $this->context; } /** * Get the context ID of the module context. * * @return int */ public function get_context_id(): int { return $this->get_context()->id; } /** * Get the course context. * * @return context_course */ public function get_course_context(): context_course { return $this->get_context()->get_course_context(); } /** * Get the big blue button instance data. * * @return stdClass */ public function get_instance_data(): stdClass { return $this->instancedata; } /** * Get the instance id. * * @return int */ public function get_instance_id(): int { return $this->instancedata->id; } /** * Helper to get an instance var. * * @param string $name * @return mixed|null */ public function get_instance_var(string $name) { $instance = $this->get_instance_data(); if (property_exists($instance, $name)) { return $instance->{$name}; } return null; } /** * Get the meeting id for this meeting. * * @param null|int $groupid * @return string */ public function get_meeting_id(?int $groupid = null): string { $baseid = sprintf( '%s-%s-%s', $this->get_instance_var('meetingid'), $this->get_course_id(), $this->get_instance_var('id') ); if ($groupid === null) { $groupid = $this->get_group_id(); } return sprintf('%s[%s]', $baseid, $groupid); } /** * Get the name of the meeting, considering any group if set. * * @return string */ public function get_meeting_name(): string { $meetingname = $this->get_instance_var('name'); $groupname = $this->get_group_name(); if ($groupname !== null) { $meetingname .= " ({$groupname})"; } return $meetingname; } /** * Get the meeting description with the pluginfile URLs optionally rewritten. * * @param bool $rewritepluginfileurls * @return string */ public function get_meeting_description(bool $rewritepluginfileurls = false): string { $description = $this->get_instance_var('intro'); if ($rewritepluginfileurls) { $description = file_rewrite_pluginfile_urls( $description, 'pluginfile.php', $this->get_context_id(), 'mod_bigbluebuttonbn', 'intro', null ); } return $description; } /** * Get the meeting type if set. * * @return null|string */ public function get_type(): ?string { return $this->get_instance_var('type'); } /** * Whether this instance is includes both a room, and recordings. * * @return bool */ public function is_type_room_and_recordings(): bool { return $this->get_type() == self::TYPE_ALL; } /** * Whether this instance is one that only includes a room. * * @return bool */ public function is_type_room_only(): bool { return $this->get_type() == self::TYPE_ROOM_ONLY; } /** * Whether this instance is one that only includes recordings. * * @return bool */ public function is_type_recordings_only(): bool { return $this->get_type() == self::TYPE_RECORDING_ONLY; } /** * Get the participant list for the session. * * @return array */ public function get_participant_list(): array { if ($this->participantlist === null) { $this->participantlist = roles::get_participant_list( $this->get_instance_data(), $this->get_context() ); } return $this->participantlist; } /** * Get the user. * * @return stdClass */ public function get_user(): stdClass { global $USER; return $USER; } /** * Get the id of the user. * * @return int */ public function get_user_id(): int { $user = $this->get_user(); return $user->id ?? 0; } /** * Get the fullname of the current user. * * @return string */ public function get_user_fullname(): string { $user = $this->get_user(); return fullname($user); } /** * Whether the current user is an administrator. * * @return bool */ public function is_admin(): bool { global $USER; return is_siteadmin($USER->id); } /** * Whether the user is a session moderator. * * @return bool */ public function is_moderator(): bool { return roles::is_moderator( $this->get_context(), $this->get_participant_list() ); } /** * Whether this user can join the conference. * * This checks the user right for access against capabilities and group membership * * @return bool */ public function can_join(): bool { $groupid = $this->get_group_id(); $context = $this->get_context(); $inrightgroup = groups_group_visible($groupid, $this->get_course(), $this->get_cm()); $hascapability = has_capability('moodle/category:manage', $context) || (has_capability('mod/bigbluebuttonbn:join', $context) && $inrightgroup); $canjoin = $this->get_type() != self::TYPE_RECORDING_ONLY && $hascapability; // Recording only cannot be joined ever. return $canjoin; } /** * Whether this user can manage recordings. * * @return bool */ public function can_manage_recordings(): bool { // Note: This will include site administrators. // The has_capability() function returns truthy for admins unless otherwise directed. return has_capability('mod/bigbluebuttonbn:managerecordings', $this->get_context()); } /** * Whether this user can publish/unpublish/protect/unprotect/delete recordings. * * @param string $action * @return bool */ public function can_perform_on_recordings($action): bool { // Note: This will include site administrators. // The has_capability() function returns truthy for admins unless otherwise directed. return has_capability("mod/bigbluebuttonbn:{$action}recordings", $this->get_context()); } /** * Get the configured user limit. * * @return int */ public function get_user_limit(): int { if ((boolean) config::get('userlimit_editable')) { return intval($this->get_instance_var('userlimit')); } return intval((int) config::get('userlimit_default')); } /** * Check whether the user limit has been reached. * * @param int $currentusercount The user count to check * @return bool */ public function has_user_limit_been_reached(int $currentusercount): bool { $userlimit = $this->get_user_limit(); if (empty($userlimit)) { return false; } return $currentusercount >= $userlimit; } /** * Check whether the current user counts towards the user limit. * * @return bool */ public function does_current_user_count_towards_user_limit(): bool { if ($this->is_admin()) { return false; } if ($this->is_moderator()) { return false; } return true; } /** * Get the voice bridge details. * * @return null|int */ public function get_voice_bridge(): ?int { $voicebridge = (int) $this->get_instance_var('voicebridge'); if ($voicebridge > 0) { return 70000 + $voicebridge; } return null; } /** * Whether participants are muted on entry. * * @return bool */ public function get_mute_on_start(): bool { return $this->get_instance_var('muteonstart'); } /** * Get the moderator password. * * @return string */ public function get_moderator_password(): string { return $this->get_instance_var('moderatorpass'); } /** * Get the viewer password. * * @return string */ public function get_viewer_password(): string { return $this->get_instance_var('viewerpass'); } /** * Get the appropriate password for the current user. * * @return string */ public function get_current_user_password(): string { if ($this->is_admin() || $this->is_moderator()) { return $this->get_moderator_password(); } return $this->get_viewer_password(); } /** * Get the appropriate designated role for the current user. * * @return string */ public function get_current_user_role(): string { if ($this->is_admin() || $this->is_moderator()) { return 'MODERATOR'; } return 'VIEWER'; } /** * Whether to show the recording button * * @return bool */ public function should_show_recording_button(): bool { global $CFG; if (!empty($CFG->bigbluebuttonbn_recording_hide_button_editable)) { $recordhidebutton = (bool) $this->get_instance_var('recordhidebutton'); $recordallfromstart = (bool) $this->get_instance_var('recordallfromstart'); return !($recordhidebutton || $recordallfromstart); } return !$CFG->bigbluebuttonbn_recording_hide_button_default; } /** * Whether this instance is recorded. * * @return bool */ public function is_recorded(): bool { return (bool) $this->get_instance_var('record'); } /** * Moderator approval required ? * * By default we leave it as false as "ALWAYS_ACCEPT" is the default value for * the guestPolicy create parameter (https://docs.bigbluebutton.org/dev/api.html) * @return bool */ public function is_moderator_approval_required(): bool { return $this->get_instance_var('mustapproveuser') ?? false; } /** * Whether this instance can import recordings from another instance. * * @return bool */ public function can_import_recordings(): bool { if (!config::get('importrecordings_enabled')) { return false; } if (!$this->can_manage_recordings()) { return false; } return $this->is_feature_enabled('importrecordings'); } /** * Get recordings_imported from instancedata. * * @return bool */ public function get_recordings_imported(): bool { if (config::get('recordings_imported_editable')) { return (bool) $this->get_instance_var('recordings_imported'); } return config::get('recordings_imported_default'); } /** * Whether this instance is recorded from the start. * * @return bool */ public function should_record_from_start(): bool { if (!$this->is_recorded()) { // This meeting is not recorded. return false; } return (bool) $this->get_instance_var('recordallfromstart'); } /** * Whether recording can be started and stopped. * * @return bool */ public function allow_recording_start_stop(): bool { if (!$this->is_recorded()) { // If the meeting is not configured for recordings, do not allow it to be recorded. return false; } return $this->should_show_recording_button(); } /** * Get the welcome message to display. * * @return string */ public function get_welcome_message(): string { $welcomestring = $this->get_instance_var('welcome'); if (!config::get('welcome_editable') || empty($welcomestring)) { $welcomestring = config::get('welcome_default'); } if (empty($welcomestring)) { $welcomestring = get_string('mod_form_field_welcome_default', 'bigbluebuttonbn'); } $welcome = [$welcomestring]; if ($this->is_recorded()) { if ($this->should_record_from_start()) { $welcome[] = get_string('bbbrecordallfromstartwarning', 'bigbluebuttonbn'); } else { $welcome[] = get_string('bbbrecordwarning', 'bigbluebuttonbn'); } } return implode('

', $welcome); } /** * Get the presentation data for internal use. * * The URL returned for the presentation will be accessible through moodle with checks about user being logged in. * * @return array|null */ public function get_presentation(): ?array { return $this->do_get_presentation_with_nonce(false); } /** * Get the presentation data for external API url. * * The URL returned for the presentation will be accessible publicly but once and with a specific URL. * * @return array|null */ public function get_presentation_for_bigbluebutton_upload(): ?array { return $this->do_get_presentation_with_nonce(true); } /** * Generate Presentation URL. * * @param bool $withnonce The generated url will have a nonce included * @return array|null */ protected function do_get_presentation_with_nonce(bool $withnonce): ?array { if ($this->has_ended()) { return files::get_presentation( $this->get_context(), $this->get_instance_var('presentation'), null, $withnonce ); } else if ($this->is_currently_open()) { return files::get_presentation( $this->get_context(), $this->get_instance_var('presentation'), $this->get_instance_id(), $withnonce ); } else { return []; } } /** * Whether to show the preuploaded presentation on the activity page. * * @return bool */ public function should_show_presentation(): bool { // Users with the correct capability should always be able to see presentation file. if (has_capability('mod/bigbluebuttonbn:seepresentation', $this->get_context())) { return true; } if (get_config('mod_bigbluebuttonbn', 'showpresentation_editable')) { return (bool) $this->get_instance_var('showpresentation'); } return (bool) get_config('mod_bigbluebuttonbn', 'showpresentation_default'); } /** * Whether the current time is before the scheduled start time. * * @return bool */ public function before_start_time(): bool { $openingtime = $this->get_instance_var('openingtime'); if (empty($openingtime)) { return false; } return $openingtime >= time(); } /** * Whether the meeting time has passed. * * @return bool */ public function has_ended(): bool { $closingtime = $this->get_instance_var('closingtime'); if (empty($closingtime)) { return false; } return $closingtime < time(); } /** * Whether this session is currently open. * * @return bool */ public function is_currently_open(): bool { if ($this->before_start_time()) { return false; } if ($this->has_ended()) { return false; } return true; } /** * Whether the user must wait to join the session. * * @return bool */ public function user_must_wait_to_join(): bool { if ($this->is_admin() || $this->is_moderator()) { return false; } return (bool) $this->get_instance_var('wait'); } /** * Whether the user can force join in all cases * * @return bool */ public function user_can_force_join(): bool { return $this->is_admin() || $this->is_moderator(); } /** * Whether the user can end a meeting * * @return bool */ public function user_can_end_meeting(): bool { return $this->is_admin() || $this->is_moderator(); } /** * Get information about the origin. * * @return stdClass */ public function get_origin_data(): stdClass { global $CFG; $parsedurl = parse_url($CFG->wwwroot); return (object) [ 'origin' => 'Moodle', 'originVersion' => $CFG->release, 'originServerName' => $parsedurl['host'], 'originServerUrl' => $CFG->wwwroot, 'originServerCommonName' => '', 'originTag' => sprintf('moodle-mod_bigbluebuttonbn (%s)', get_config('mod_bigbluebuttonbn', 'version')), ]; } /** * Whether this is a server belonging to blindside networks. * * @return bool */ public function is_blindside_network_server(): bool { return bigbluebutton_proxy::is_bn_server(); } /** * Get the URL used to access the course that the instance is in. * * @return moodle_url */ public function get_course_url(): moodle_url { return new moodle_url('/course/view.php', ['id' => $this->get_course_id()]); } /** * Get the URL used to view the instance as a user. * * @return moodle_url */ public function get_view_url(): moodle_url { return new moodle_url('/mod/bigbluebuttonbn/view.php', [ 'id' => $this->get_cm()->id, ]); } /** * Get the logout URL used to log out of the meeting. * * @return moodle_url */ public function get_logout_url(): moodle_url { return new moodle_url('/mod/bigbluebuttonbn/bbb_view.php', [ 'action' => 'logout', 'id' => $this->get_cm()->id, 'courseid' => $this->get_cm()->course, // Used to find the course if ever the activity is deleted // while the meeting is running. ]); } /** * Get the URL that the remote server will use to notify that the recording is ready. * * @return moodle_url */ public function get_record_ready_url(): moodle_url { return new moodle_url('/mod/bigbluebuttonbn/bbb_broker.php', [ 'action' => 'recording_ready', 'bigbluebuttonbn' => $this->instancedata->id, ]); } /** * Get the URL that the remote server will use to notify of meeting events. * * @return moodle_url */ public function get_meeting_event_notification_url(): moodle_url { return new moodle_url('/mod/bigbluebuttonbn/bbb_broker.php', [ 'action' => 'meeting_events', 'bigbluebuttonbn' => $this->instancedata->id, ]); } /** * Get the URL used to join a meeting. * * @return moodle_url */ public function get_join_url(): moodle_url { return new moodle_url('/mod/bigbluebuttonbn/bbb_view.php', [ 'action' => 'join', 'id' => $this->get_cm()->id, 'bn' => $this->instancedata->id, ]); } /** * Get the URL used for the import page. * * @return moodle_url */ public function get_import_url(): moodle_url { return new moodle_url('/mod/bigbluebuttonbn/import_view.php', [ 'destbn' => $this->instancedata->id, ]); } /** * Get the list of enabled features for this instance. * * @return array */ public function get_enabled_features(): array { return config::get_enabled_features( bigbluebutton_proxy::get_instance_type_profiles(), $this->get_instance_var('type') ?? null ); } /** * Check whetherthe named features is enabled. * * @param string $feature * @return bool */ public function is_feature_enabled(string $feature): bool { $features = $this->get_enabled_features(); return !empty($features[$feature]); } /** * Check if meeting is recorded. * * @return bool */ public function should_record() { return (boolean) config::recordings_enabled() && $this->is_recorded(); } /** * Get recordings for this instance * * @param string[] $excludedid * @param bool $viewdeleted view deleted recordings ? * @return recording[] */ public function get_recordings(array $excludedid = [], bool $viewdeleted = false): array { // Fetch the list of recordings depending on the status of the instance. // show room is enabled for TYPE_ALL and TYPE_ROOM_ONLY. if ($this->is_feature_enabled('showroom')) { // Not in the import page. return recording::get_recordings_for_instance( $this, $this->is_feature_enabled('importrecordings'), $this->get_instance_var('recordings_imported'), ); } // We show all recording from this course as this is TYPE_RECORDING. return recording::get_recordings_for_course( $this->get_course_id(), $excludedid, $this->is_feature_enabled('importrecordings'), false, $viewdeleted ); } /** * Check if this is a valid group for this user/instance, * * * @param stdClass $user * @param int $groupid * @return bool */ public function user_has_group_access($user, $groupid) { $cm = $this->get_cm(); $context = $this->get_context(); // Then validate group. $groupmode = groups_get_activity_groupmode($cm); if ($groupmode && $groupid) { $accessallgroups = has_capability('moodle/site:accessallgroups', $context); if ($accessallgroups || $groupmode == VISIBLEGROUPS) { $allowedgroups = groups_get_all_groups($cm->course, 0, $cm->groupingid); } else { $allowedgroups = groups_get_all_groups($cm->course, $user->id, $cm->groupingid); } if (!array_key_exists($groupid, $allowedgroups)) { return false; } if (!groups_group_visible($groupid, $this->get_course(), $this->get_cm())) { return false; } } return true; } /** * Get current guest link url * * @return moodle_url */ public function get_guest_access_url(): moodle_url { $guestlinkuid = $this->get_instance_var('guestlinkuid'); if (empty($guestlinkuid)) { $this->generate_guest_credentials(); $guestlinkuid = $this->get_instance_var('guestlinkuid'); } return new moodle_url('/mod/bigbluebuttonbn/guest.php', ['uid' => $guestlinkuid]); } /** * Is guest access allowed in this instance. * * @return bool */ public function is_guest_allowed(): bool { return !$this->is_type_recordings_only() && config::get('guestaccess_enabled') && $this->get_instance_var('guestallowed'); } /** * Get current meeting password * * @return string */ public function get_guest_access_password(): string { $guestpassword = $this->get_instance_var('guestpassword'); if (empty($guestpassword)) { $this->generate_guest_credentials(); $guestpassword = $this->get_instance_var('guestpassword'); } return $guestpassword; } /** * Generate credentials for this instance and persist the value in the database * * @return void */ private function generate_guest_credentials(): void { global $DB; [$this->instancedata->guestlinkuid, $this->instancedata->guestpassword] = \mod_bigbluebuttonbn\plugin::generate_guest_meeting_credentials(); $DB->update_record('bigbluebuttonbn', $this->instancedata); } /** * Is this meeting configured to display avatars of the users ? * * Note: this is for now a global setting. * * @return bool */ public function is_profile_picture_enabled(): bool { return (bool) config::get('profile_picture_enabled'); } } PK\İclasses/search/activity.phpnu[. namespace mod_bigbluebuttonbn\search; /** * Search area for mod_bigbluebuttonbn activities. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class activity extends \core_search\base_activity { /** * Returns true if this area uses file indexing. * * @return bool */ public function uses_file_indexing() { return true; } } PK\#vclasses/search/tags.phpnu[. namespace mod_bigbluebuttonbn\search; use stdClass; /** * Search area for mod_bigbluebuttonbn tags. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tags extends \core_search\base_activity { /** * Returns true if this area uses file indexing. * * @return bool */ public function uses_file_indexing() { return false; } /** * Overwritting get_document_recordset() * In this search implementation, we need to re-index all instances (and not only the last modified) because we * are working with core tags and these can be removed from "manage tags" without change the timemodified in * BBB instances. * @param int $modifiedfrom * @param \context|null $context * @return \moodle_recordset|null */ public function get_document_recordset($modifiedfrom = 0, ?\context $context = null) { global $DB; [$contextjoin, $contextparams] = $this->get_context_restriction_sql($context, $this->get_module_name(), 'modtable'); if ($contextjoin === null) { return null; } $result = $DB->get_recordset_sql( 'SELECT modtable.* FROM {' . $this->get_module_name() . '} modtable ' . $contextjoin, array_merge($contextparams) ); return($result); } /** * Overriding method to index tags of module as string separated by comma. * * @param stdClass $record * @param array $options * @return \core_search\document|bool */ public function get_document($record, $options = []) { try { $cm = $this->get_cm($this->get_module_name(), $record->id, $record->course); $context = \context_module::instance($cm->id); $tags = \core_tag_tag::get_tags_by_area_in_contexts("core", "course_modules", [$context]); $tagsstring = ""; if (!empty($tags)) { $res = []; foreach ($tags as $t) { $res[] = $t->name; } $tagsstring = implode(", ", $res); } } catch (\dml_missing_record_exception $ex) { // Notify it as we run here as admin, we should see everything. debugging('Error retrieving ' . $this->areaid . ' ' . $record->id . ' document, not all required data is available: ' . $ex->getMessage(), DEBUG_DEVELOPER); return false; } catch (\dml_exception $ex) { // Notify it as we run here as admin, we should see everything. debugging('Error retrieving ' . $this->areaid . ' ' . $record->id . ' document: ' . $ex->getMessage(), DEBUG_DEVELOPER); return false; } // Prepare array with data from DB. $doc = \core_search\document_factory::instance($record->id, $this->componentname, $this->areaname); $doc->set('title', content_to_text($record->name, false)); $doc->set('content', $tagsstring); $doc->set('contextid', $context->id); $doc->set('courseid', $record->course); $doc->set('owneruserid', \core_search\manager::NO_OWNER_ID); $doc->set('modified', $record->{static::MODIFIED_FIELD_NAME}); // Check if this document should be considered new. if (isset($options['lastindexedtime'])) { $createdfield = static::CREATED_FIELD_NAME; if (!empty($createdfield) && ($options['lastindexedtime'] < $record->{$createdfield})) { // If the document was created after the last index time, it must be new. $doc->set_is_new(true); } } return $doc; } } PK\]*classes/output/recording_name_editable.phpnu[. namespace mod_bigbluebuttonbn\output; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\recording; /** * Renderer for recording name in place editable. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent.david [at] call-learning [dt] fr) */ class recording_name_editable extends recording_editable { /** * Specific constructor with the right label/hint for this editable * * @param recording $rec * @param instance $instance */ public function __construct(recording $rec, instance $instance) { parent::__construct($rec, $instance, get_string('view_recording_name_editlabel', 'mod_bigbluebuttonbn'), get_string('view_recording_name_edithint', 'mod_bigbluebuttonbn')); } /** * Get the value to display * * @param recording $recording * @return string */ public function get_recording_value(recording $recording): string { $metaname = $recording->get('name'); return \html_writer::span($metaname); } /** * Get the type of editable */ protected static function get_type() { return 'name'; } } PK\ѣ׻ +classes/output/instance_updated_message.phpnu[. namespace mod_bigbluebuttonbn\output; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\local\helpers\roles; use moodle_url; use renderable; use renderer_base; use stdClass; use templatable; /** * Renderable for the instance notification updated message * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Darko Miletic (darko.miletic [at] gmail [dt] com) */ class instance_updated_message implements renderable, templatable { /** @var int The activity was created */ const TYPE_CREATED = 0; /** @var int The activity was updated */ const TYPE_UPDATED = 1; /** * @var instance $instance */ protected $instance; /** @var int activity type. */ protected $type; /** * Instance updated constructor * * @param instance $instance * @param int $type */ public function __construct(instance $instance, int $type) { $this->instance = $instance; $this->type = $type; } /** * Defer to template. * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output): stdClass { return (object) [ 'is_update' => $this->type === self::TYPE_UPDATED, 'is_create' => $this->type === self::TYPE_CREATED, 'name' => $this->instance->get_meeting_name(), 'link' => $this->instance->get_view_url()->out(), 'description' => $this->instance->get_meeting_description(), 'openingtime' => $this->instance->get_instance_var('openingtime'), 'closingtime' => $this->instance->get_instance_var('closingtime'), ]; } } PK\q q classes/output/renderer.phpnu[. namespace mod_bigbluebuttonbn\output; use core\notification; use core\output\inplace_editable; use html_table; use html_writer; use mod_bigbluebuttonbn\instance; use plugin_renderer_base; /** * Renderer for the mod_bigbluebuttonbn plugin. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Darko Miletic (darko.miletic [at] gmail [dt] com) */ class renderer extends plugin_renderer_base { /** * Render the index table. * * @param index $index * @return string */ protected function render_index(index $index): string { $this->page->requires->js_call_amd('mod_bigbluebuttonbn/index', 'init'); return html_writer::table($index->get_table($this)); } /** * Render the groups selector. * * @param instance $instance * @return string */ public function render_groups_selector(instance $instance): string { $groupmode = groups_get_activity_groupmode($instance->get_cm()); if ($groupmode == NOGROUPS) { return ''; } // Separate or visible group mode. $groups = groups_get_activity_allowed_groups($instance->get_cm()); if (empty($groups)) { // No groups in this course. notification::add(get_string('view_groups_nogroups_warning', 'bigbluebuttonbn'), notification::INFO); return ''; } // Assign group default values. if (count($groups) == 0) { // Only the All participants group exists. notification::add(get_string('view_groups_notenrolled_warning', 'bigbluebuttonbn'), notification::INFO); return ''; } if (count($groups) > 1) { notification::add(get_string('view_groups_selection_warning', 'bigbluebuttonbn'), notification::INFO); } $groupsmenu = groups_print_activity_menu( $instance->get_cm(), $instance->get_view_url(), true ); return $groupsmenu . '

'; } /** * Render the view page. * * @param view_page $page * @return string */ public function render_view_page(view_page $page): string { return $this->render_from_template( 'mod_bigbluebuttonbn/view_page', $page->export_for_template($this) ); } /** * Render inplace editable * * @param inplace_editable $e * @return bool|string */ public function render_inplace_editable(inplace_editable $e) { return $this->render_from_template('core/inplace_editable', $e->export_for_template($this)); } } PK\C̺1classes/output/recording_description_editable.phpnu[. namespace mod_bigbluebuttonbn\output; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\recording; /** * Renderer for recording name in place editable. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent.david [at] call-learning [dt] fr) */ class recording_description_editable extends recording_editable { /** * Specific constructor with the right label/hint for this editable * * @param recording $rec * @param instance $instance */ public function __construct(recording $rec, instance $instance) { parent::__construct($rec, $instance, get_string('view_recording_description_editlabel', 'mod_bigbluebuttonbn'), get_string('view_recording_description_edithint', 'mod_bigbluebuttonbn')); } /** * Get the value to display * * @param recording $recording a recording * @return string */ public function get_recording_value(recording $recording): string { $metadescription = $recording->get('description'); return \html_writer::span($metadescription); } /** * Get the type of editable */ protected static function get_type() { return 'description'; } } PK\ZM *classes/output/recording_row_actionbar.phpnu[. namespace mod_bigbluebuttonbn\output; use mod_bigbluebuttonbn\recording; use pix_icon; use renderable; use renderer_base; use stdClass; use templatable; /** * Renderer for recording row actionbar column * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent.david [at] call-learning [dt] fr) */ class recording_row_actionbar implements renderable, templatable { /** * @var $recording */ protected $recording; /** * @var $tools */ protected $tools; /** * @var array TOOLS_DEFINITION a list of definition for the the specific tools */ const TOOL_ACTION_DEFINITIONS = [ 'protect' => [ 'action' => 'unprotect', 'icon' => 'lock', 'hidewhen' => '!protected', 'requireconfirmation' => true, 'disablewhen' => 'imported' ], 'unprotect' => [ 'action' => 'protect', 'icon' => 'unlock', 'hidewhen' => 'protected', 'requireconfirmation' => true, 'disablewhen' => 'imported' ], 'publish' => [ 'action' => 'publish', 'icon' => 'show', 'hidewhen' => 'published', 'requireconfirmation' => true, 'disablewhen' => 'imported' ], 'unpublish' => [ 'action' => 'unpublish', 'icon' => 'hide', 'hidewhen' => '!published', 'requireconfirmation' => true, 'disablewhen' => 'imported' ], 'delete' => [ 'action' => 'delete', 'icon' => 'trash', 'requireconfirmation' => true ], 'import' => [ 'action' => 'import', 'icon' => 'import', ] ]; /** * recording_row_actionbar constructor. * * @param recording $recording * @param array $tools */ public function __construct(recording $recording, array $tools) { $this->recording = $recording; $this->tools = $tools; } /** * Export for template * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output): stdClass { $context = new stdClass(); $context->id = 'recording-actionbar-' . $this->recording->get('id'); $context->recordingid = $this->recording->get('id'); $context->tools = []; foreach ($this->tools as $tool) { if (!empty(self::TOOL_ACTION_DEFINITIONS[$tool])) { $buttonpayload = self::TOOL_ACTION_DEFINITIONS[$tool]; $conditionalhiding = $buttonpayload['hidewhen'] ?? null; $disabledwhen = $buttonpayload['disablewhen'] ?? null; $this->actionbar_update_diplay($buttonpayload, $disabledwhen, $this->recording, 'disabled'); $this->actionbar_update_diplay($buttonpayload, $conditionalhiding, $this->recording); if (!empty($buttonpayload)) { $iconortext = ''; $target = $buttonpayload['action']; if (isset($buttonpayload['target'])) { $target .= '-' . $buttonpayload['target']; } $id = 'recording-' . $target . '-' . $this->recording->get('recordingid'); $iconattributes = [ 'id' => $id, 'class' => 'iconsmall', ]; $linkattributes = [ 'id' => $id, 'data-action' => $buttonpayload['action'], 'data-require-confirmation' => !empty($buttonpayload['requireconfirmation']), 'class' => 'action-icon' ]; if ($this->recording->get('imported')) { $linkattributes['data-links'] = recording::count_records( [ 'recordingid' => $this->recording->get('recordingid'), 'imported' => true, ] ); } if (isset($buttonpayload['disabled'])) { $iconattributes['class'] .= ' fa-' . $buttonpayload['disabled']; $linkattributes['class'] .= ' disabled'; } $icon = new pix_icon( 'i/' . $buttonpayload['icon'], get_string('view_recording_list_actionbar_' . $buttonpayload['action'], 'bigbluebuttonbn'), 'moodle', $iconattributes ); $iconortext = $output->render($icon); $actionlink = new \action_link(new \moodle_url('#'), $iconortext, null, $linkattributes); $context->tools[] = $actionlink->export_for_template($output); } } } return $context; } /** * Read the settings for this action and disable or hide the tool from the toolbar * * @param array $buttonpayload * @param string $condition * @param recording $rec * @param string $value */ private function actionbar_update_diplay(&$buttonpayload, $condition, $rec, $value = 'invisible') { if ($condition) { $negates = $condition[0] === '!'; $conditionalvariable = ltrim($condition, '!'); if ($rec->get($conditionalvariable) xor $negates) { $buttonpayload['disabled'] = $value; } } } } PK\classes/output/import_view.phpnu[. namespace mod_bigbluebuttonbn\output; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\local\config; use mod_bigbluebuttonbn\local\helpers\roles; use renderable; use renderer_base; use stdClass; use templatable; /** * Renderable for the import page. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Darko Miletic (darko.miletic [at] gmail [dt] com) */ class import_view implements renderable, templatable { /** * @var instance $destinationinstance */ protected $destinationinstance; /** * @var int|null $sourceinstanceid the source instance id or null if it is not yet set. */ protected $sourceinstanceid; /** * @var int|null $sourcecourseid the source instance id or null if it is not yet set. */ protected $sourcecourseid; /** * import_view constructor. * * @param instance $destinationinstance * @param int $sourcecourseid * @param int $sourceinstanceid */ public function __construct(instance $destinationinstance, int $sourcecourseid, int $sourceinstanceid) { $this->destinationinstance = $destinationinstance; $this->sourcecourseid = $sourcecourseid >= 0 ? $sourcecourseid : null; $this->sourceinstanceid = $sourceinstanceid >= 0 ? $sourceinstanceid : null; } /** * Defer to template. * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output): stdClass { $courses = roles::import_get_courses_for_select($this->destinationinstance); if (config::get('importrecordings_from_deleted_enabled')) { $courses[0] = get_string('recordings_from_deleted_activities', 'mod_bigbluebuttonbn'); ksort($courses); } $context = (object) [ 'bbbid' => $this->destinationinstance->get_instance_id(), 'has_recordings' => true, 'bbbsourceid' => 0 ]; if (!empty($this->sourceinstanceid)) { $context->sourceid = $this->sourceinstanceid; $context->search = [ 'value' => '' ]; $sourceinstance = instance::get_from_instanceid($this->sourceinstanceid); if ($sourceinstance->is_type_room_only()) { $context->has_recordings = false; } $context->bbbsourceid = $sourceinstance->get_instance_id(); } // Now the selects. if (!empty($this->sourcecourseid)) { $selectrecords = []; $cms = get_fast_modinfo($this->sourcecourseid)->instances['bigbluebuttonbn']; foreach ($cms as $cm) { if ($cm->id == $this->destinationinstance->get_cm_id()) { // Skip the target instance. continue; } if ($cm->deletioninprogress) { // Check if the BBB is not currently scheduled for deletion. continue; } $selectrecords[$cm->instance] = $cm->name; } if (config::get('importrecordings_from_deleted_enabled')) { $selectrecords[0] = get_string('recordings_from_deleted_activities', 'mod_bigbluebuttonbn'); } $actionurl = $this->destinationinstance->get_import_url(); $actionurl->param('sourcecourseid', $this->sourcecourseid); $select = new \single_select( $actionurl, 'sourcebn', $selectrecords, $this->sourceinstanceid ?? "" ); $context->bbb_select = $select->export_for_template($output); } $context->sourcecourseid = $this->sourcecourseid ?? 0; // Course selector. $context->course_select = (new \single_select( $this->destinationinstance->get_import_url(), 'sourcecourseid', $courses, $this->sourcecourseid ?? "" ))->export_for_template($output); if (!is_null($this->sourcecourseid)) { $context->has_selected_course = true; } // Back button. $context->back_button = (new \single_button( $this->destinationinstance->get_view_url(), get_string('view_recording_button_return', 'mod_bigbluebuttonbn') ))->export_for_template($output); return $context; } } PK\ AAclasses/output/view_page.phpnu[. namespace mod_bigbluebuttonbn\output; use core\check\result; use core\output\notification; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\local\config; use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; use mod_bigbluebuttonbn\meeting; use renderable; use renderer_base; use stdClass; use templatable; use tool_task\check\cronrunning; /** * View Page template renderable. * * @package mod_bigbluebuttonbn * @copyright 2021 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class view_page implements renderable, templatable { /** @var instance The instance being rendered */ protected $instance; /** * Constructor for the View Page. * * @param instance $instance */ public function __construct(instance $instance) { $this->instance = $instance; } /** * Export the content required to render the template. * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output): stdClass { $pollinterval = bigbluebutton_proxy::get_poll_interval(); $templatedata = (object) [ 'instanceid' => $this->instance->get_instance_id(), 'pollinterval' => $pollinterval * 1000, // Javascript poll interval is in miliseconds. 'groupselector' => $output->render_groups_selector($this->instance), 'meetingname' => $this->instance->get_meeting_name(), 'description' => $this->instance->get_meeting_description(true), 'joinurl' => $this->instance->get_join_url(), ]; $viewwarningmessage = config::get('general_warning_message'); if ($this->show_view_warning() && !empty($viewwarningmessage)) { $templatedata->sitenotification = (object) [ 'message' => $viewwarningmessage, 'type' => config::get('general_warning_box_type'), 'icon' => [ 'pix' => 'i/bullhorn', 'component' => 'core', ], ]; if ($url = config::get('general_warning_button_href')) { $templatedata->sitenotification->actions = [[ 'url' => $url, 'title' => config::get('general_warning_button_text'), ]]; } } if ($this->instance->is_feature_enabled('showroom')) { $showpresentation = $this->instance->should_show_presentation(); $roomdata = meeting::get_meeting_info_for_instance($this->instance); $roomdata->haspresentations = false; $roomdata->showpresentations = $showpresentation; if (!empty($roomdata->presentations)) { $roomdata->haspresentations = true; } $templatedata->room = $roomdata; } $templatedata->recordingwarnings = []; $check = new cronrunning(); $result = $check->get_result(); if ($result->get_status() != result::OK && $this->instance->is_moderator()) { $templatedata->recordingwarnings[] = (new notification( get_string('view_message_cron_disabled', 'mod_bigbluebuttonbn', $result->get_summary()), notification::NOTIFY_ERROR, false ))->export_for_template($output); } if ($this->instance->is_feature_enabled('showrecordings') && $this->instance->is_recorded()) { $recordings = new recordings_session($this->instance); $templatedata->recordings = $recordings->export_for_template($output); } else if ($this->instance->is_type_recordings_only()) { $templatedata->recordingwarnings[] = (new notification( get_string('view_message_recordings_disabled', 'mod_bigbluebuttonbn'), notification::NOTIFY_WARNING, false ))->export_for_template($output); } return $templatedata; } /** * Whether to show the view warning. * * @return bool */ protected function show_view_warning(): bool { if ($this->instance->is_admin()) { return true; } $generalwarningroles = explode(',', config::get('general_warning_roles')); $userroles = \mod_bigbluebuttonbn\local\helpers\roles::get_user_roles( $this->instance->get_context(), $this->instance->get_user_id() ); foreach ($userroles as $userrole) { if (in_array($userrole->shortname, $generalwarningroles)) { return true; } } return false; } } PK\b%classes/output/recording_editable.phpnu[. namespace mod_bigbluebuttonbn\output; use lang_string; use mod_bigbluebuttonbn\local\bigbluebutton; use moodle_exception; use core\output\inplace_editable; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; use mod_bigbluebuttonbn\local\proxy\recording_proxy; use mod_bigbluebuttonbn\recording; use stdClass; /** * Renderer for recording in place editable. * * Generic class * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent.david [at] call-learning [dt] fr) */ abstract class recording_editable extends \core\output\inplace_editable { /** @var instance The bbb instance */ protected $instance; /** * Constructor. * * @param recording $rec * @param instance $instance * @param string $edithint * @param string $editlabel */ public function __construct(recording $rec, instance $instance, string $edithint, string $editlabel) { $this->instance = $instance; $editable = $this->check_capability(); $displayvalue = format_string( $this->get_recording_value($rec), true, [ 'context' => $instance->get_context(), ] ); // Hack here: the ID is the recordID and the meeting ID. parent::__construct( 'mod_bigbluebuttonbn', static::get_type(), $rec->get('id'), $editable, $displayvalue, $displayvalue, $edithint, $editlabel ); } /** * Check user can access and or modify this item. * * @return bool * @throws \moodle_exception */ protected function check_capability() { global $USER; if (!can_access_course($this->instance->get_course(), $USER)) { throw new moodle_exception('noaccess', 'mod_bigbluebuttonbn'); } return $this->instance->can_manage_recordings(); } /** * Get the type of editable */ protected static function get_type() { return ''; } /** * Get the real recording value * * @param recording $rec * @return mixed */ abstract public function get_recording_value(recording $rec): string; /** * Update the recording with the new value * * @param int $itemid * @param mixed $value * @return recording_editable */ public static function update($itemid, $value) { $recording = recording::get_record(['id' => $itemid]); $instance = instance::get_from_instanceid($recording->get('bigbluebuttonbnid')); require_login($instance->get_course(), true, $instance->get_cm()); require_capability('mod/bigbluebuttonbn:managerecordings', $instance->get_context()); $recording->set(static::get_type(), $value); $recording->update(); return new static($recording, $instance); } /** * Helper function evaluates if a row for the data used by the recording table is editable. * * @return bool */ protected function row_editable() { // Since the request to BBB are cached, it is safe to use the wrapper to check the server version. return $this->instance->can_manage_recordings() && (bigbluebutton_proxy::get_server_version() >= 1.0 || $this->instance->is_blindside_network_server()); } } PK\zz)classes/output/recording_row_playback.phpnu[. namespace mod_bigbluebuttonbn\output; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\local\bigbluebutton\recordings\recording_data; use mod_bigbluebuttonbn\local\config; use mod_bigbluebuttonbn\local\helpers\roles; use mod_bigbluebuttonbn\recording; use renderable; use renderer_base; use stdClass; use templatable; /** * Renderer for recording row playback column * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent.david [at] call-learning [dt] fr) */ class recording_row_playback implements renderable, templatable { /** * @var $instance */ protected $instance; /** * @var $recording */ protected $recording; /** * recording_row_playback constructor. * * @param recording $rec * @param instance|null $instance $instance */ public function __construct(recording $rec, ?instance $instance) { $this->instance = $instance ?? null; $this->recording = $rec; } /** * Export for template * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output): stdClass { $ispublished = $this->recording->get('published'); $recordingid = $this->recording->get('id'); $context = (object) [ 'dataimported' => $this->recording->get('imported'), 'id' => 'playbacks-' . $this->recording->get('id'), 'recordingid' => $recordingid, 'additionaloptions' => '', 'playbacks' => [], ]; $playbacks = $this->recording->get('playbacks'); if ($ispublished && $playbacks) { foreach ($playbacks as $playback) { if ($this->should_be_included($playback)) { $linkattributes = [ 'id' => "recording-play-{$playback['type']}-{$recordingid}", 'class' => 'btn btn-sm btn-default', 'data-action' => 'play', 'data-target' => $playback['type'], ]; $actionlink = new \action_link( $playback['url'], recording_data::type_text($playback['type']), null, $linkattributes ); $context->playbacks[] = $actionlink->export_for_template($output); } } } return $context; } /** * Helper function renders the link used for recording type in row for the data used by the recording table. * * @param array $playback * @return bool */ protected function should_be_included(array $playback): bool { // All types that are not restricted are included. if (array_key_exists('restricted', $playback) && strtolower($playback['restricted']) == 'false') { return true; } $canmanagerecordings = roles::has_capability_in_course( $this->recording->get('courseid'), 'mod/bigbluebuttonbn:managerecordings'); $canviewallformats = roles::has_capability_in_course( $this->recording->get('courseid'), 'mod/bigbluebuttonbn:viewallrecordingformats'); $issafeformat = false; // Now check the list of safe formats. if ($safeformats = config::get('recording_safe_formats')) { $safeformatarray = str_getcsv($safeformats, escape: '\\'); $issafeformat = in_array($playback['type'], $safeformatarray); } return ($canmanagerecordings && $canviewallformats) || $issafeformat; } } PK\U>>>classes/output/index.phpnu[. namespace mod_bigbluebuttonbn\output; use html_table; use html_writer; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\meeting; use mod_bigbluebuttonbn\plugin; use renderable; use renderer_base; use stdClass; /** * Renderer for the Index page. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class index implements renderable { /** @var stdClass */ protected $course; /** @var stdClass[] */ protected $instances; /** * Constructor for the index renderable. * * @param stdClass $course * @param instance[] List of bbbbn instances */ public function __construct(stdClass $course, array $instances) { $this->course = $course; $this->instances = $instances; } /** * Get the table for the index page. * * @param renderer_base $output * @return html_table */ public function get_table(renderer_base $output): html_table { // Print the list of instances. $table = new html_table(); if (course_format_uses_sections($this->course->format)) { $sectionheading = course_get_format($this->course)->get_generic_section_name(); } else { $sectionheading = ''; } $table->head = [ $sectionheading, get_string('index_heading_name', plugin::COMPONENT), get_string('index_heading_group', plugin::COMPONENT), get_string('index_heading_users', plugin::COMPONENT), get_string('index_heading_viewer', plugin::COMPONENT), get_string('index_heading_moderator', plugin::COMPONENT), get_string('index_heading_recording', plugin::COMPONENT), get_string('index_heading_actions', plugin::COMPONENT), ]; $table->align = ['center', 'left', 'center', 'center', 'center', 'center', 'center']; $modinfo = get_fast_modinfo($this->course); foreach ($modinfo->instances['bigbluebuttonbn'] as $cm) { $instance = $this->instances[$cm->id]; $this->add_instance_to_table($output, $table, $instance); } return $table; } /** * Add details of the bigbluebuttonbn instance to the table. * * @param renderer_base $output * @param html_table $table * @param instance $instance */ protected function add_instance_to_table(renderer_base $output, html_table $table, instance $instance): void { $cm = $instance->get_cm(); if (!$cm->uservisible) { return; } if (groups_get_activity_groupmode($cm) == 0) { $table->data[] = $this->add_room_row_to_table($output, $instance); } else { // Add 'All participants' room information. $table->data[] = $this->add_room_row_to_table($output, $instance, 0); // Add data for the groups belonging to the bbb instance, if any. $groups = groups_get_activity_allowed_groups($cm); foreach ($groups as $group) { $table->data[] = $this->add_room_row_to_table($output, $instance, $group->id); } } } /** * Displays the general view. * * @param renderer_base $output * @param instance $instance * @param int|null $group * @return array */ protected function add_room_row_to_table(renderer_base $output, instance $instance, ?int $group = null): array { if ($group) { $instance = instance::get_group_instance_from_instance($instance, $group); } $meeting = new meeting($instance); if (course_format_uses_sections($this->course->format)) { $sectionname = get_section_name($this->course, $instance->get_cm()->sectionnum); } else { $sectionname = ''; } $viewurl = $instance->get_view_url(); if ($groupid = $instance->get_group_id()) { $viewurl->param('group', $groupid); } $joinurl = html_writer::link($viewurl, format_string($instance->get_meeting_name())); // The meeting info was returned. if ($meeting->is_running()) { return [ $sectionname, $joinurl, $instance->get_group_name(), $this->get_room_usercount($meeting), $this->get_room_attendee_list($meeting, 'VIEWER'), $this->get_room_attendee_list($meeting, 'MODERATOR'), $this->get_room_record_info($output, $instance), $this->get_room_actions($output, $instance, $meeting), ]; } return [$sectionname, $joinurl, $instance->get_group_name(), '', '', '', '', '']; } /** * Count the number of users in the meeting. * * @param meeting $meeting * @return int */ protected function get_room_usercount(meeting $meeting): int { return count($meeting->get_attendees()); } /** * Returns attendee list. * * @param meeting $meeting * @param string $role * @return string */ protected function get_room_attendee_list(meeting $meeting, string $role): string { $attendees = []; // Iterate attendees, matching by their "role" property. foreach ($meeting->get_attendees() as $attendee) { if (strcmp((string) $attendee['role'], $role) === 0) { $attendees[] = $attendee['fullName']; } } return implode(', ', $attendees); } /** * Returns indication of recording enabled. * * @param renderer_base $output * @param instance $instance * @return string */ protected function get_room_record_info(renderer_base $output, instance $instance): string { if ($instance->is_recorded()) { // If it has been set when meeting created, set the variable on/off. return get_string('index_enabled', 'bigbluebuttonbn'); } return ''; } /** * Returns room actions. * * @param renderer_base $output * @param instance $instance * @param meeting $meeting * @return string */ protected function get_room_actions(renderer_base $output, instance $instance, meeting $meeting): string { if ($instance->is_moderator()) { return $output->render_from_template('mod_bigbluebuttonbn/end_session_button', (object) [ 'bigbluebuttonbnid' => $instance->get_instance_id(), 'groupid' => $instance->get_group_id(), 'statusrunning' => $meeting->is_running(), ]); } return ''; } } PK\Gr (classes/output/recording_row_preview.phpnu[. namespace mod_bigbluebuttonbn\output; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\local\bigbluebutton\recordings\recording_data; use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; use mod_bigbluebuttonbn\recording; use renderable; use renderer_base; use stdClass; use templatable; /** * Renderer for recording_row_preview column * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent.david [at] call-learning [dt] fr) */ class recording_row_preview implements renderable, templatable { /** * @var $recording */ protected $recording; /** * recording_row_playback constructor. * * @param recording $rec */ public function __construct(recording $rec) { $this->recording = $rec; } /** * Export for template * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output): stdClass { global $CFG; $context = (object) [ 'id' => 'preview-' . $this->recording->get('id'), 'hidden' => !$this->recording->get('published'), 'recordingpreviews' => [], ]; $playbacks = $this->recording->get('playbacks'); foreach ($playbacks as $playback) { $thumbnails = []; if (isset($playback['preview'])) { foreach ($playback['preview'] as $image) { $url = trim($image['url']); $validated = bigbluebutton_proxy::is_remote_resource_valid($url); if ($validated) { $thumbnails[] = $url . '?' . time(); } } if (!empty($thumbnails)) { $context->recordingpreviews[] = (object) [ 'thumbnails' => $thumbnails, ]; } } } return $context; } } PK\)\. namespace mod_bigbluebuttonbn\output; use mod_bigbluebuttonbn\instance; use renderable; use renderer_base; use stdClass; use templatable; /** * Renderer for recording section. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent.david [at] call-learning [dt] fr) */ class recordings_session implements renderable, templatable { /** * @var instance */ protected $instance; /** * recording_section constructor. * * @param instance $instance */ public function __construct(instance $instance) { $this->instance = $instance; } /** * Export for template * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output): stdClass { $isrecordedtype = $this->instance->is_type_room_and_recordings() || $this->instance->is_type_recordings_only(); $context = (object) [ 'bbbid' => $this->instance->get_instance_id(), 'groupid' => $this->instance->get_group_id(), 'has_recordings' => $this->instance->is_recorded() && $isrecordedtype, 'searchbutton' => [ 'value' => '', ], ]; if ($this->instance->can_import_recordings()) { $button = new \single_button( $this->instance->get_import_url(), get_string('view_recording_button_import', 'mod_bigbluebuttonbn') ); $context->import_button = $button->export_for_template($output); } return $context; } } PK\'\\classes/meeting.phpnu[. namespace mod_bigbluebuttonbn; use cache; use cache_store; use context_course; use core_tag_tag; use Exception; use Firebase\JWT\Key; use mod_bigbluebuttonbn\local\config; use mod_bigbluebuttonbn\local\exceptions\bigbluebutton_exception; use mod_bigbluebuttonbn\local\exceptions\meeting_join_exception; use mod_bigbluebuttonbn\local\helpers\roles; use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; use stdClass; /** * Class to describe a BBB Meeting. * * @package mod_bigbluebuttonbn * @copyright 2021 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class meeting { /** @var instance The bbb instance */ protected $instance; /** @var stdClass Info about the meeting */ protected $meetinginfo = null; /** * Constructor for the meeting object. * * @param instance $instance */ public function __construct(instance $instance) { $this->instance = $instance; } /** * Helper to join a meeting. * * * It will create the meeting if not already created. * * @param instance $instance * @param int $origin * @return string * @throws meeting_join_exception this is sent if we cannot join (meeting full, user needs to wait...) */ public static function join_meeting(instance $instance, $origin = logger::ORIGIN_BASE): string { // See if the session is in progress. $meeting = new meeting($instance); // As the meeting doesn't exist, try to create it. if (empty($meeting->get_meeting_info(true)->createtime)) { $meeting->create_meeting(); } return $meeting->join($origin); } /** * Get currently stored meeting info * * @return stdClass */ public function get_meeting_info() { if (!$this->meetinginfo) { $this->meetinginfo = $this->do_get_meeting_info(); } return $this->meetinginfo; } /** * Return meeting information for the specified instance. * * @param instance $instance * @param bool $updatecache Whether to update the cache when fetching the information * @return stdClass */ public static function get_meeting_info_for_instance(instance $instance, bool $updatecache = false): stdClass { $meeting = new self($instance); return $meeting->do_get_meeting_info($updatecache); } /** * Helper function returns a sha1 encoded string that is unique and will be used as a seed for meetingid. * * @return string */ public static function get_unique_meetingid_seed() { global $DB; do { $encodedseed = sha1(plugin::random_password(12)); $meetingid = (string) $DB->get_field('bigbluebuttonbn', 'meetingid', ['meetingid' => $encodedseed]); } while ($meetingid == $encodedseed); return $encodedseed; } /** * Is meeting running ? * * @return bool */ public function is_running() { return $this->get_meeting_info()->statusrunning ?? false; } /** * Force update the meeting in cache. */ public function update_cache() { $this->meetinginfo = $this->do_get_meeting_info(true); } /** * Get meeting attendees * * @return array[] */ public function get_attendees(): array { return $this->get_meeting_info()->attendees ?? []; } /** * Can the meeting be joined ? * * @return bool */ public function can_join() { return $this->get_meeting_info()->canjoin; } /** * Total number of moderators and viewers. * * @return int */ public function get_participant_count() { return $this->get_meeting_info()->totalusercount; } /** * Creates a bigbluebutton meeting, send the message to BBB and returns the response in an array. * * @return array */ public function create_meeting() { $data = $this->create_meeting_data(); $metadata = $this->create_meeting_metadata(); $presentation = $this->instance->get_presentation_for_bigbluebutton_upload(); // The URL must contain nonce. $presentationname = $presentation['name'] ?? null; $presentationurl = $presentation['url'] ?? null; $response = bigbluebutton_proxy::create_meeting( $data, $metadata, $presentationname, $presentationurl, $this->instance->get_instance_id() ); // New recording management: Insert a recordingID that corresponds to the meeting created. if ($this->instance->is_recorded()) { $recording = new recording(0, (object) [ 'courseid' => $this->instance->get_course_id(), 'bigbluebuttonbnid' => $this->instance->get_instance_id(), 'recordingid' => $response['internalMeetingID'], 'groupid' => $this->instance->get_group_id()] ); $recording->create(); } return $response; } /** * Send an end meeting message to BBB server */ public function end_meeting() { bigbluebutton_proxy::end_meeting( $this->instance->get_meeting_id(), $this->instance->get_moderator_password(), $this->instance->get_instance_id() ); } /** * Get meeting join URL * * @return string */ public function get_join_url(): string { return bigbluebutton_proxy::get_join_url($this->instance, $this->get_meeting_info()->createtime); } /** * Get meeting join URL for guest * * @param string $userfullname * @return string */ public function get_guest_join_url(string $userfullname): string { return bigbluebutton_proxy::get_guest_join_url($this->instance, $this->get_meeting_info()->createtime, $userfullname); } /** * Return meeting information for this meeting. * * @param bool $updatecache Whether to update the cache when fetching the information * @return stdClass */ protected function do_get_meeting_info(bool $updatecache = false): stdClass { $instance = $this->instance; $meetinginfo = (object) [ 'instanceid' => $instance->get_instance_id(), 'bigbluebuttonbnid' => $instance->get_instance_id(), 'groupid' => $instance->get_group_id(), 'meetingid' => $instance->get_meeting_id(), 'cmid' => $instance->get_cm_id(), 'ismoderator' => $instance->is_moderator(), 'joinurl' => $instance->get_join_url()->out(), 'userlimit' => $instance->get_user_limit(), 'presentations' => [], ]; if ($instance->get_instance_var('openingtime')) { $meetinginfo->openingtime = intval($instance->get_instance_var('openingtime')); } if ($instance->get_instance_var('closingtime')) { $meetinginfo->closingtime = intval($instance->get_instance_var('closingtime')); } $activitystatus = bigbluebutton_proxy::view_get_activity_status($instance); // This might raise an exception if info cannot be retrieved. // But this might be totally fine as the meeting is maybe not yet created on BBB side. $totalusercount = 0; // This is the default value for any meeting that has not been created. $meetinginfo->statusrunning = false; $meetinginfo->createtime = null; $info = self::retrieve_cached_meeting_info($this->instance, $updatecache); if (!empty($info)) { $meetinginfo->statusrunning = $info['running'] === 'true'; $meetinginfo->createtime = $info['createTime'] ?? null; $totalusercount = isset($info['participantCount']) ? $info['participantCount'] : 0; } $meetinginfo->statusclosed = $activitystatus === 'ended'; $meetinginfo->statusopen = !$meetinginfo->statusrunning && $activitystatus === 'open'; $meetinginfo->totalusercount = $totalusercount; $canjoin = !$instance->user_must_wait_to_join() || $meetinginfo->statusrunning; // Limit has not been reached. $canjoin = $canjoin && (!$instance->has_user_limit_been_reached($totalusercount)); // User should only join during scheduled session start and end time, if defined. $canjoin = $canjoin && ($instance->is_currently_open()); // Double check that the user has the capabilities to join. $canjoin = $canjoin && $instance->can_join(); $meetinginfo->canjoin = $canjoin; // If user is administrator, moderator or if is viewer and no waiting is required, join allowed. if ($meetinginfo->statusrunning) { $meetinginfo->startedat = floor(intval($info['startTime']) / 1000); // Milliseconds. $meetinginfo->moderatorcount = $info['moderatorCount']; $meetinginfo->moderatorplural = $info['moderatorCount'] > 1; $meetinginfo->participantcount = $totalusercount - $meetinginfo->moderatorcount; $meetinginfo->participantplural = $meetinginfo->participantcount > 1; } $meetinginfo->statusmessage = $this->get_status_message($meetinginfo, $instance); $presentation = $instance->get_presentation(); // This is for internal use. if (!empty($presentation)) { $meetinginfo->presentations[] = $presentation; $meetinginfo->showpresentations = $instance->should_show_presentation(); } $meetinginfo->attendees = []; if (!empty($info['attendees'])) { // Ensure each returned attendee is cast to an array, rather than a simpleXML object. foreach ($info['attendees'] as $attendee) { $meetinginfo->attendees[] = (array) $attendee; } } $meetinginfo->guestaccessenabled = $instance->is_guest_allowed(); if ($meetinginfo->guestaccessenabled && $instance->is_moderator()) { $meetinginfo->guestjoinurl = $instance->get_guest_access_url()->out(); $meetinginfo->guestpassword = $instance->get_guest_access_password(); } $meetinginfo->features = $instance->get_enabled_features(); return $meetinginfo; } /** * Deduce status message from the current meeting info and the instance * * Returns the human-readable message depending on if the user must wait to join, the meeting has not * yet started ... * @param object $meetinginfo * @param instance $instance * @return string */ protected function get_status_message(object $meetinginfo, instance $instance): string { if ($instance->has_user_limit_been_reached($meetinginfo->totalusercount)) { return get_string('view_message_conference_user_limit_reached', 'bigbluebuttonbn'); } if ($meetinginfo->statusrunning) { return get_string('view_message_conference_in_progress', 'bigbluebuttonbn'); } if ($instance->user_must_wait_to_join() && !$instance->user_can_force_join()) { return get_string('view_message_conference_wait_for_moderator', 'bigbluebuttonbn'); } if ($instance->before_start_time()) { return get_string('view_message_conference_not_started', 'bigbluebuttonbn'); } if ($instance->has_ended()) { return get_string('view_message_conference_has_ended', 'bigbluebuttonbn'); } return get_string('view_message_conference_room_ready', 'bigbluebuttonbn'); } /** * Gets a meeting info object cached or fetched from the live session. * * @param instance $instance * @param bool $updatecache * * @return array */ protected static function retrieve_cached_meeting_info(instance $instance, $updatecache = false) { $meetingid = $instance->get_meeting_id(); $cachettl = (int) config::get('waitformoderator_cache_ttl'); $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'mod_bigbluebuttonbn', 'meetings_cache'); $result = $cache->get($meetingid); $now = time(); if (!$updatecache && !empty($result) && $now < ($result['creation_time'] + $cachettl)) { // Use the value in the cache. return (array) json_decode($result['meeting_info']); } // We set the cache to an empty value so then if get_meeting_info raises an exception we still have the // info about the last creation_time, so we don't ask the server again for a bit. $defaultcacheinfo = ['creation_time' => time(), 'meeting_info' => '[]']; // Pings again and refreshes the cache. try { $meetinginfo = bigbluebutton_proxy::get_meeting_info($meetingid); $cache->set($meetingid, ['creation_time' => time(), 'meeting_info' => json_encode($meetinginfo)]); } catch (bigbluebutton_exception $e) { // The meeting is not created on BBB side, so we set the value in the cache so we don't poll again // and return an empty array. $cache->set($meetingid, $defaultcacheinfo); return []; } return $meetinginfo; } /** * Conversion between form settings and lockSettings as set in BBB API. */ const LOCK_SETTINGS_MEETING_DATA = [ 'disablecam' => 'lockSettingsDisableCam', 'disablemic' => 'lockSettingsDisableMic', 'disableprivatechat' => 'lockSettingsDisablePrivateChat', 'disablepublicchat' => 'lockSettingsDisablePublicChat', 'disablenote' => 'lockSettingsDisableNotes', 'hideuserlist' => 'lockSettingsHideUserList' ]; /** * Helper to prepare data used for create meeting. * @todo moderatorPW and attendeePW will be removed from create after release of BBB v2.6. * * @return array */ protected function create_meeting_data() { $data = ['meetingID' => $this->instance->get_meeting_id(), 'name' => \mod_bigbluebuttonbn\plugin::html2text($this->instance->get_meeting_name(), 64), 'attendeePW' => $this->instance->get_viewer_password(), 'moderatorPW' => $this->instance->get_moderator_password(), 'logoutURL' => $this->instance->get_logout_url()->out(false), ]; $data['record'] = $this->instance->should_record() ? 'true' : 'false'; // Check if auto_start_record is enable. if ($data['record'] == 'true' && $this->instance->should_record_from_start()) { $data['autoStartRecording'] = 'true'; } // Check if hide_record_button is enable. if (!$this->instance->should_show_recording_button()) { $data['allowStartStopRecording'] = 'false'; } $data['welcome'] = trim($this->instance->get_welcome_message()); $voicebridge = intval($this->instance->get_voice_bridge()); if ($voicebridge > 0 && $voicebridge < 79999) { $data['voiceBridge'] = $voicebridge; } $maxparticipants = intval($this->instance->get_user_limit()); if ($maxparticipants > 0) { $data['maxParticipants'] = $maxparticipants; } if ($this->instance->get_mute_on_start()) { $data['muteOnStart'] = 'true'; } // Here a bit of a change compared to the API default behaviour: we should not allow guest to join // a meeting managed by Moodle by default. if ($this->instance->is_guest_allowed()) { $data['guestPolicy'] = $this->instance->is_moderator_approval_required() ? 'ASK_MODERATOR' : 'ALWAYS_ACCEPT'; } // Locks settings. foreach (self::LOCK_SETTINGS_MEETING_DATA as $instancevarname => $lockname) { $instancevar = $this->instance->get_instance_var($instancevarname); if (!is_null($instancevar)) { $data[$lockname] = $instancevar ? 'true' : 'false'; if ($instancevar) { $data['lockSettingsLockOnJoin'] = 'true'; // This will be locked whenever one settings is locked. } } } return $data; } /** * Helper for preparing metadata used while creating the meeting. * * @return array */ protected function create_meeting_metadata() { global $USER; // Create standard metadata. $origindata = $this->instance->get_origin_data(); $metadata = [ 'bbb-origin' => $origindata->origin, 'bbb-origin-version' => $origindata->originVersion, 'bbb-origin-server-name' => $origindata->originServerName, 'bbb-origin-server-common-name' => $origindata->originServerCommonName, 'bbb-origin-tag' => $origindata->originTag, 'bbb-context' => $this->instance->get_course()->fullname, 'bbb-context-id' => $this->instance->get_course_id(), 'bbb-context-name' => trim(html_to_text($this->instance->get_course()->fullname, 0)), 'bbb-context-label' => trim(html_to_text($this->instance->get_course()->shortname, 0)), 'bbb-recording-name' => plugin::html2text($this->instance->get_meeting_name(), 64), 'bbb-recording-description' => plugin::html2text($this->instance->get_meeting_description(), 64), 'bbb-recording-tags' => implode(',', core_tag_tag::get_item_tags_array('core', 'course_modules', $this->instance->get_cm_id())), // Same as $id. ]; // Special metadata for recording processing. if ((boolean) config::get('recordingstatus_enabled')) { $metadata["bn-recording-status"] = json_encode( [ 'email' => ['"' . fullname($USER) . '" <' . $USER->email . '>'], 'context' => $this->instance->get_view_url(), ] ); } if ((boolean) config::get('recordingready_enabled')) { $metadata['bbb-recording-ready-url'] = $this->instance->get_record_ready_url()->out(false); } if ((boolean) config::get('meetingevents_enabled')) { $metadata['analytics-callback-url'] = $this->instance->get_meeting_event_notification_url()->out(false); } return $metadata; } /** * Helper for responding when storing live meeting events is requested. * * The callback with a POST request includes: * - Authentication: Bearer } encoded with HS512> * - Content Type: application/json * - Body: * * @param instance $instance * @param object $data * @return string */ public static function meeting_events(instance $instance, object $data): string { $bigbluebuttonbn = $instance->get_instance_data(); // Validate that the bigbluebuttonbn activity corresponds to the meeting_id received. $meetingidelements = explode('[', $data->{'meeting_id'}); $meetingidelements = explode('-', $meetingidelements[0]); if (!isset($bigbluebuttonbn) || $bigbluebuttonbn->meetingid != $meetingidelements[0]) { return 'HTTP/1.0 410 Gone. The activity may have been deleted'; } // We make sure events are processed only once. $overrides = ['meetingid' => $data->{'meeting_id'}]; $meta['internalmeetingid'] = $data->{'internal_meeting_id'}; $meta['callback'] = 'meeting_events'; $meta['meetingid'] = $data->{'meeting_id'}; // Remove attendees from data to avoid duplicating callback logs; they are stored as summary logs. $meta['data'] = clone $data->{'data'}; unset($meta['data']->{'attendees'}); $eventcount = logger::log_event_callback($instance, $overrides, $meta); if ($eventcount === 1) { // Process the events. self::process_meeting_events($instance, $data); return 'HTTP/1.0 200 Accepted. Enqueued.'; } else { return 'HTTP/1.0 202 Accepted. Already processed.'; } } /** * Helper function enqueues list of meeting events to be stored and processed as for completion. * * @param instance $instance * @param stdClass $jsonobj */ protected static function process_meeting_events(instance $instance, stdClass $jsonobj) { $meetingid = $jsonobj->{'meeting_id'}; $recordid = $jsonobj->{'internal_meeting_id'}; $attendees = $jsonobj->{'data'}->{'attendees'}; foreach ($attendees as $attendee) { $userid = $attendee->{'ext_user_id'}; $overrides['meetingid'] = $meetingid; $overrides['userid'] = $userid; $meta['recordid'] = $recordid; $meta['data'] = $attendee; // Stores the log. logger::log_event_summary($instance, $overrides, $meta); // Enqueue a task for processing the completion. bigbluebutton_proxy::enqueue_completion_event($instance->get_instance_data(), $userid); } } /** * Prepare join meeting action * * @param int $origin * @return void */ protected function prepare_meeting_join_action(int $origin) { $this->do_get_meeting_info(true); if ($this->is_running()) { if ($this->instance->has_user_limit_been_reached($this->get_participant_count())) { throw new meeting_join_exception('userlimitreached'); } } else if ($this->instance->user_must_wait_to_join()) { // If user is not administrator nor moderator (user is student) and waiting is required. throw new meeting_join_exception('waitformoderator'); } // Moodle event logger: Create an event for meeting joined. logger::log_meeting_joined_event($this->instance, $origin); // Before executing the redirect, increment the number of participants. roles::participant_joined($this->instance->get_meeting_id(), $this->instance->is_moderator()); } /** * Join a meeting. * * @param int $origin The spec * @return string The URL to redirect to * @throws meeting_join_exception */ public function join(int $origin): string { $this->prepare_meeting_join_action($origin); return $this->get_join_url(); } /** * Join a meeting as a guest. * * @param int $origin The spec * @param string $userfullname Fullname for the guest user * @return string The URL to redirect to * @throws meeting_join_exception */ public function guest_join(int $origin, string $userfullname): string { $this->prepare_meeting_join_action($origin); return $this->get_join_url(); } } PK\E classes/setting_validator.phpnu[. namespace mod_bigbluebuttonbn; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->libdir.'/adminlib.php'); /** * Helper class for validating settings used HTML for settings.php. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class setting_validator { /** * Validate if general section will be shown. * * @return bool */ public static function section_general_shown() { global $CFG; return (!isset($CFG->bigbluebuttonbn['server_url']) || !isset($CFG->bigbluebuttonbn['shared_secret']) || !isset($CFG->bigbluebuttonbn['checksum_algorithm']) ); } /** * Validate if default messages section will be shown. * * @return bool */ public static function section_default_messages_shown() { global $CFG; return (!isset($CFG->bigbluebuttonbn['welcome_default']) || !isset($CFG->bigbluebuttonbn['welcome_editable'])); } /** * Validate if record meeting section will be shown. * * @return bool */ public static function section_record_meeting_shown() { global $CFG; return (!isset($CFG->bigbluebuttonbn['recording_default']) || !isset($CFG->bigbluebuttonbn['recording_editable']) || !isset($CFG->bigbluebuttonbn['recording_all_from_start_default']) || !isset($CFG->bigbluebuttonbn['recording_all_from_start_editable']) || !isset($CFG->bigbluebuttonbn['recording_hide_button_default']) || !isset($CFG->bigbluebuttonbn['recording_hide_button_editable']) ); } /** * Validate if import recording section will be shown. * * @return bool */ public static function section_import_recordings_shown() { global $CFG; return (!isset($CFG->bigbluebuttonbn['importrecordings_enabled']) || !isset($CFG->bigbluebuttonbn['importrecordings_from_deleted_enabled'])); } /** * Validate if show recording section will be shown. * * @return bool */ public static function section_show_recordings_shown() { global $CFG; return (!isset($CFG->bigbluebuttonbn['recordings_deleted_default']) || !isset($CFG->bigbluebuttonbn['recordings_deleted_editable']) || !isset($CFG->bigbluebuttonbn['recordings_imported_default']) || !isset($CFG->bigbluebuttonbn['recordings_imported_editable']) || !isset($CFG->bigbluebuttonbn['recordings_preview_default']) || !isset($CFG->bigbluebuttonbn['recordings_preview_editable']) || !isset($CFG->bigbluebuttonbn['recording_protect_editable']) ); } /** * Validate if wait moderator section will be shown. * * @return bool */ public static function section_wait_moderator_shown() { global $CFG; return (!isset($CFG->bigbluebuttonbn['waitformoderator_default']) || !isset($CFG->bigbluebuttonbn['waitformoderator_editable']) || !isset($CFG->bigbluebuttonbn['waitformoderator_ping_interval']) || !isset($CFG->bigbluebuttonbn['waitformoderator_cache_ttl'])); } /** * Validate if static voice bridge section will be shown. * * @return bool */ public static function section_static_voice_bridge_shown() { global $CFG; return (!isset($CFG->bigbluebuttonbn['voicebridge_editable'])); } /** * Validate if preupload presentation section will be shown. * * @return bool */ public static function section_preupload_presentation_shown() { global $CFG; return (!isset($CFG->bigbluebuttonbn['preuploadpresentation_editable'])); } /** * Validate if user limit section will be shown. * * @return bool */ public static function section_user_limit_shown() { global $CFG; return (!isset($CFG->bigbluebuttonbn['userlimit_default']) || !isset($CFG->bigbluebuttonbn['userlimit_editable'])); } /** * Validate if moderator default section will be shown. * * @return bool */ public static function section_moderator_default_shown() { global $CFG; return (!isset($CFG->bigbluebuttonbn['participant_moderator_default'])); } /** * Validate if settings extended section will be shown. * * @return bool */ public static function section_settings_extended_shown() { global $CFG; return (!isset($CFG->bigbluebuttonbn['recordingready_enabled']) || !isset($CFG->bigbluebuttonbn['meetingevents_enabled'])); } /** * Validate if muteonstart section will be shown. * * @return bool */ public static function section_muteonstart_shown() { global $CFG; return (!isset($CFG->bigbluebuttonbn['muteonstart_default']) || !isset($CFG->bigbluebuttonbn['muteonstart_editable'])); } /** * Validate if disablecam section will be shown. * * @return bool */ public static function section_disablecam_shown() { global $CFG; return (!isset($CFG->bigbluebuttonbn['disablecam_default']) || !isset($CFG->bigbluebuttonbn['disablecam_editable'])); } /** * Validate if disablemic section will be shown. * * @return bool */ public static function section_disablemic_shown() { global $CFG; return (!isset($CFG->bigbluebuttonbn['disablemic_default']) || !isset($CFG->bigbluebuttonbn['disablemic_editable'])); } /** * Validate if disableprivatechat section will be shown. * * @return bool */ public static function section_disableprivatechat_shown() { global $CFG; return (!isset($CFG->bigbluebuttonbn['disableprivatechat_default']) || !isset($CFG->bigbluebuttonbn['disableprivatechat_editable'])); } /** * Validate if disablepublicchat section will be shown. * * @return bool */ public static function section_disablepublicchat_shown() { global $CFG; return (!isset($CFG->bigbluebuttonbn['disablepublicchat_default']) || !isset($CFG->bigbluebuttonbn['disablepublicchat_editable'])); } /** * Validate if disablenote section will be shown. * * @return bool */ public static function section_disablenote_shown() { global $CFG; return (!isset($CFG->bigbluebuttonbn['disablenote_default']) || !isset($CFG->bigbluebuttonbn['disablenote_editable'])); } /** * Validate if hideuserlist section will be shown. * * @return bool */ public static function section_hideuserlist_shown() { global $CFG; return (!isset($CFG->bigbluebuttonbn['hideuserlist_default']) || !isset($CFG->bigbluebuttonbn['hideuserlist_editable'])); } /** * Validate that session lock settings is shown or not * @return bool */ public static function section_lock_shown() { return self::section_disablecam_shown() || self::section_disablemic_shown() || self::section_disablenote_shown() || self::section_disableprivatechat_shown() || self::section_disablepublicchat_shown() || self::section_disablenote_shown() || self::section_hideuserlist_shown(); } } PK\Oyj& & classes/plugin.phpnu[. namespace mod_bigbluebuttonbn; /** * Class plugin. * * @package mod_bigbluebuttonbn * @copyright 2019 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Darko Miletic (darko.miletic [at] gmail [dt] com) */ abstract class plugin { /** * Component name. */ const COMPONENT = 'mod_bigbluebuttonbn'; /** * Helper function to convert an html string to plain text. * * @param string $html * @param int $len * * @return string */ public static function html2text($html, $len = 0) { $text = strip_tags($html); $text = str_replace(' ', ' ', $text); $textlen = strlen($text); $text = mb_substr($text, 0, $len); if ($textlen > $len) { $text .= '...'; } return $text; } /** * Helper generates a random password. * * @param int $length * @param string $unique * * @return string */ public static function random_password($length = 8, $unique = "") { $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; do { $password = substr(str_shuffle($chars), 0, $length); } while ($unique == $password); return $password; } /** * Generate random credentials for guest access * * @return array */ public static function generate_guest_meeting_credentials(): array { $password = self::random_password(); $guestlinkuid = sha1(self::random_password(1024)); return [$guestlinkuid, $password]; } } PK\lf0'classes/event/recording_unpublished.phpnu[. namespace mod_bigbluebuttonbn\event; /** * The mod_bigbluebuttonbn recording unpublished event. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class recording_unpublished extends base { /** * Init method. * * @param string $crud * @param int $edulevel */ protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { parent::init($crud, $edulevel); $this->description = "The user with id '##userid' has unpublished a recording with id " . "'##other' in the course id '##courseid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('event_recording_unpublished', 'bigbluebuttonbn'); } /** * Return objectid mapping. * * @return array */ public static function get_objectid_mapping() { return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; } } PK\uLA$classes/event/recording_imported.phpnu[. namespace mod_bigbluebuttonbn\event; /** * The mod_bigbluebuttonbn recording imported event. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class recording_imported extends base { /** * Init method. * * @param string $crud * @param int $edulevel */ protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { parent::init($crud, $edulevel); $this->description = "The user with id '##userid' has imported a recording with id " . "'##other' in the course id '##courseid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('event_recording_imported', 'bigbluebuttonbn'); } /** * Return objectid mapping. * * @return array */ public static function get_objectid_mapping() { return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; } } PK\ڂG*%classes/event/recording_published.phpnu[. namespace mod_bigbluebuttonbn\event; /** * The mod_bigbluebuttonbn recording published event. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class recording_published extends base { /** * Init method. * * @param string $crud * @param int $edulevel */ protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { parent::init($crud, $edulevel); $this->description = "The user with id '##userid' has published a recording with id " . "'##other' in the course id '##courseid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('event_recording_published', 'bigbluebuttonbn'); } /** * Return objectid mapping. * * @return array */ public static function get_objectid_mapping() { return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; } } PK\. namespace mod_bigbluebuttonbn\event; /** * The mod_bigbluebuttonbn recording protected event. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class recording_protected extends base { /** * Init method. * * @param string $crud * @param int $edulevel */ protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { parent::init($crud, $edulevel); $this->description = "The user with id '##userid' has protected a recording with id " . "'##other' in the course id '##courseid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('event_recording_protected', 'bigbluebuttonbn'); } /** * Return objectid mapping. * * @return array */ public static function get_objectid_mapping() { return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; } } PK\,TSS classes/event/meeting_joined.phpnu[. namespace mod_bigbluebuttonbn\event; /** * The mod_bigbluebuttonbn meeting joined event, triggered when the user joins the session. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class meeting_joined extends base { /** * Init method. * * @param string $crud * @param int $edulevel */ protected function init($crud = 'r', $edulevel = self::LEVEL_PARTICIPATING) { parent::init($crud, $edulevel); $this->description = "The user with id '##userid' has joined a bigbluebutton meeting for " . "the bigbluebuttonbn activity with id '##objectid' for the course id " . "'##courseid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('event_meeting_joined', 'bigbluebuttonbn'); } /** * Return objectid mapping. * * @return array */ public static function get_objectid_mapping() { return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; } } PK\+*ZBB!classes/event/meeting_created.phpnu[. namespace mod_bigbluebuttonbn\event; /** * The mod_bigbluebuttonbn meeting created event, triggered when the meeting is created before join. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class meeting_created extends base { /** * Init method. * * @param string $crud * @param int $edulevel */ protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { parent::init($crud, $edulevel); $this->description = "The user with id '##userid' created a bigbluebutton meeting for " . "the bigbluebuttonbn activity with id '##objectid' for the course id '##courseid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('event_meeting_created', 'bigbluebuttonbn'); } /** * Return objectid mapping. * * @return array */ public static function get_objectid_mapping() { return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; } } PK\%e`<__classes/event/meeting_ended.phpnu[. namespace mod_bigbluebuttonbn\event; /** * The mod_bigbluebuttonbn meeting ended event, triggered when the meeting is ended by the user. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class meeting_ended extends base { /** * Init method. * * @param string $crud * @param int $edulevel */ protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { parent::init($crud, $edulevel); $this->description = "A bigbluebutton meeting for the bigbluebuttonbn activity with id " . "'##objectid' for the course id '##courseid' has been forcibly " . "ended by the user with id '##userid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('event_meeting_ended', 'bigbluebuttonbn'); } /** * Return objectid mapping. * * @return array */ public static function get_objectid_mapping() { return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; } } PK\.3:&classes/event/course_module_viewed.phpnu[. namespace mod_bigbluebuttonbn\event; /** * The mod_bigbluebuttonbn activity viewed event. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_module_viewed extends \core\event\course_module_viewed { /** * Init method. * */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'bigbluebuttonbn'; } /** * Return objectid mapping. * * @return array */ public static function get_objectid_mapping() { return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; } } PK\XQ  ,classes/event/activity_management_viewed.phpnu[. namespace mod_bigbluebuttonbn\event; /** * The mod_bigbluebuttonbn activity management viewed event. * * @package mod_bigbluebuttonbn * @copyright 2010-2017 Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v2 or later */ class activity_management_viewed extends base { /** * Init method. * * @param string $crud * @param int $edulevel */ protected function init($crud = 'r', $edulevel = self::LEVEL_PARTICIPATING) { parent::init($crud, $edulevel); $this->description = "The user with id '##userid' viewed the bigbluebuttonbn activity management page for " . "the course module id '##contextinstanceid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return 'BigBlueButtonBN activity management viewed'; } /** * Return objectid mapping. * * @return array */ public static function get_objectid_mapping() { return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; } } PK\'classes/event/recording_unprotected.phpnu[. namespace mod_bigbluebuttonbn\event; /** * The mod_bigbluebuttonbn recording unprotected event. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class recording_unprotected extends base { /** * Init method. * * @param string $crud * @param int $edulevel */ protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { parent::init($crud, $edulevel); $this->description = "The user with id '##userid' has unprotected a recording with id " . "'##other' in the course id '##courseid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('event_recording_unprotected', 'bigbluebuttonbn'); } /** * Return objectid mapping. * * @return array */ public static function get_objectid_mapping() { return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; } } PK\m~+"classes/event/recording_viewed.phpnu[. namespace mod_bigbluebuttonbn\event; /** * The mod_bigbluebuttonbn recording viewed event. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class recording_viewed extends base { /** * Init method. * * @param string $crud * @param int $edulevel */ protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { parent::init($crud, $edulevel); $this->description = "The user with id '##userid' has viewed a recording with id " . "'##other' from the course id '##courseid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('event_recording_viewed', 'bigbluebuttonbn'); } /** * Return objectid mapping. * * @return array */ public static function get_objectid_mapping() { return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; } } PK\ܑp#classes/event/recording_deleted.phpnu[. namespace mod_bigbluebuttonbn\event; /** * The mod_bigbluebuttonbn recording deleted event. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class recording_deleted extends base { /** * Init method. * * @param string $crud * @param int $edulevel */ protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { parent::init($crud, $edulevel); $this->description = "The user with id '##userid' has deleted a recording with id " . "'##other' from the course id '##courseid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('event_recording_deleted', 'bigbluebuttonbn'); } /** * Return objectid mapping. * * @return array */ public static function get_objectid_mapping() { return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; } } PK\ȿYYclasses/event/events.phpnu[. namespace mod_bigbluebuttonbn\event; /** * The mod_bigbluebuttonbn class for event name definition. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class events { /** * Event name matcher. * * @var $events */ public static $events = [ 'create' => 'activity_created', 'view' => 'course_module_viewed', 'update' => 'activity_updated', 'delete' => 'activity_deleted', 'meeting_create' => 'meeting_created', 'meeting_end' => 'meeting_ended', 'meeting_join' => 'meeting_joined', 'meeting_left' => 'meeting_left', 'recording_delete' => 'recording_deleted', 'recording_import' => 'recording_imported', 'recording_protect' => 'recording_protected', 'recording_publish' => 'recording_published', 'recording_unprotect' => 'recording_unprotected', 'recording_unpublish' => 'recording_unpublished', 'recording_edit' => 'recording_edited', 'recording_play' => 'recording_viewed', 'live_session' => 'live_session' ]; } PK\2qq$classes/event/live_session_event.phpnu[. namespace mod_bigbluebuttonbn\event; /** * The mod_bigbluebuttonbn live_session (Experimental: for being triggered when external events are received). * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class live_session_event extends base { /** * Init method. * * @param string $crud * @param int $edulevel */ protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { parent::init($crud, $edulevel); $this->description = "The user with id '##userid' triggered action ##other in a " . "bigbluebutton meeting for the bigbluebuttonbn activity with id " . "'##objectid' for the course id '##courseid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('event_live_session', 'bigbluebuttonbn'); } /** * Return objectid mapping. * * @return array */ public static function get_objectid_mapping() { return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; } } PK\0`"classes/event/recording_edited.phpnu[. namespace mod_bigbluebuttonbn\event; /** * The mod_bigbluebuttonbn recording edited event. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class recording_edited extends base { /** * Init method. * * @param string $crud * @param int $edulevel */ protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { parent::init($crud, $edulevel); $this->description = "The user with id '##userid' has edited a recording with id " . "'##other' in the course id '##courseid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('event_recording_edited', 'bigbluebuttonbn'); } /** * Return objectid mapping. * * @return array */ public static function get_objectid_mapping() { return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; } } PK\ъQccclasses/event/meeting_left.phpnu[. namespace mod_bigbluebuttonbn\event; /** * The mod_bigbluebuttonbn meeting left event, triggered when the user lefts the meeting using the logout button. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class meeting_left extends base { /** * Init method. * * @param string $crud * @param int $edulevel */ protected function init($crud = 'r', $edulevel = self::LEVEL_PARTICIPATING) { parent::init($crud, $edulevel); $this->description = "The user with id '##userid' has left a bigbluebutton meeting for " . "the bigbluebuttonbn activity with id '##objectid' for the course id " . "'##courseid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('event_meeting_left', 'bigbluebuttonbn'); } /** * Return objectid mapping. * * @return array */ public static function get_objectid_mapping() { return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; } } PK\P classes/event/base.phpnu[. namespace mod_bigbluebuttonbn\event; use coding_exception; use moodle_url; /** * The mod_bigbluebuttonbn abstract base event class. Most mod_bigbluebuttonbn events can extend this class. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class base extends \core\event\base { /** * Object Id Mapping. * * @var array */ protected static $objectidmapping = ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; /** @var $bigbluebuttonbn */ protected $bigbluebuttonbn; /** * Description. * * @var string */ protected $description; /** * Legacy log data. * * @var array */ protected $legacylogdata; /** * Returns description of what happened. * * @return string */ public function get_description() { $vars = [ 'userid' => $this->userid, 'courseid' => $this->courseid, 'objectid' => $this->objectid, 'contextinstanceid' => $this->contextinstanceid, 'other' => $this->other ]; $string = $this->description; foreach ($vars as $key => $value) { if ($value !== null) { $string = str_replace("##" . $key, $value, $string); } } return $string; } /** * Returns relevant URL. * * @return moodle_url */ public function get_url() { return new moodle_url('/mod/bigbluebuttonbn/view.php', ['id' => $this->contextinstanceid]); } /** * Init method. * * @param string $crud * @param int $edulevel */ protected function init($crud = 'r', $edulevel = self::LEVEL_PARTICIPATING) { $this->data['crud'] = $crud; $this->data['edulevel'] = $edulevel; $this->data['objecttable'] = 'bigbluebuttonbn'; } /** * Custom validation. * * @throws coding_exception */ protected function validate_data() { parent::validate_data(); if ($this->contextlevel != CONTEXT_MODULE) { throw new coding_exception('Context level must be CONTEXT_MODULE.'); } } } PK\v+classes/task/check_dismissed_recordings.phpnu[. namespace mod_bigbluebuttonbn\task; use core\task\scheduled_task; use mod_bigbluebuttonbn\recording; /** * Synchronise pending and dismissed recordings from the server. * * @package mod_bigbluebuttonbn * @copyright 2022 Laurent David Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class check_dismissed_recordings extends scheduled_task { /** * Run the migration task. */ public function execute() { recording::sync_pending_recordings_from_server(true); } /** * Get the name of the task for use in the interface. * * @return string */ public function get_name(): string { return get_string('taskname:check_dismissed_recordings', 'mod_bigbluebuttonbn'); } } PK\Fm"classes/task/send_guest_emails.phpnu[. namespace mod_bigbluebuttonbn\task; use core_user; use html_writer; /** * This adhoc task will send emails to guest users with the meeting's details * * @package mod_bigbluebuttonbn * @copyright 2022 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent [at] call-learning [dt] fr) */ class send_guest_emails extends base_send_notification { /** * Get the notification type. * * @return string */ protected function get_notification_type(): string { return 'guest_invited'; } /** * Send all the emails */ protected function send_all_notifications(): void { $customdata = $this->get_custom_data(); if (!empty($customdata->emails)) { foreach ($customdata->emails as $email) { $user = core_user::get_noreply_user(); $user->email = $email; $user->mailformat = 1; // HTML format. email_to_user( $user, core_user::get_noreply_user(), $this->get_subject(), $this->get_small_message(), $this->get_html_message() ); } } } /** * Get the subject of the notification. * * @return string */ protected function get_subject(): string { return get_string('guest_invitation_subject', 'mod_bigbluebuttonbn', $this->get_string_vars()); } /** * Get variables to make available to strings. * * @return array */ protected function get_string_vars(): array { $customdata = $this->get_custom_data(); $sender = core_user::get_user($customdata->useridfrom); return [ 'course_fullname' => $this->get_instance()->get_course()->fullname, 'course_shortname' => $this->get_instance()->get_course()->shortname, 'name' => $this->get_instance()->get_cm()->name, 'guestjoinurl' => $this->get_instance()->get_guest_access_url()->out(false), 'guestpassword' => $this->get_instance()->get_guest_access_password(), 'sender' => fullname($sender) ]; } /** * Get the short summary message. * * @return string */ protected function get_small_message(): string { return get_string('guest_invitation_small_message', 'mod_bigbluebuttonbn', $this->get_string_vars()); } /** * Get the HTML message content. * * @return string */ protected function get_html_message(): string { return html_writer::tag( 'p', get_string('guest_invitation_full_message', 'mod_bigbluebuttonbn', $this->get_string_vars()) ); } } PK\M M !classes/task/reset_recordings.phpnu[. namespace mod_bigbluebuttonbn\task; use core\task\adhoc_task; use mod_bigbluebuttonbn\recording; /** * Class containing the scheduled task for converting recordings for the BigBlueButton version 2.5 in Moodle 4.0. * * @package mod_bigbluebuttonbn * @copyright 2021 Jesus Federico, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class reset_recordings extends adhoc_task { /** @var int Chunk size to use when resetting recordings */ protected static $chunksize = 100; /** * Run the migration task. */ public function execute() { if ($this->process_reset_recordings()) { \core\task\manager::queue_adhoc_task(new static()); } } /** * Process all bigbluebuttonbn_recordings looking for entries which should be reset to be fetched again. * * @return bool Whether any more recodgins are waiting to be processed */ protected function process_reset_recordings(): bool { global $DB; $classname = static::class; mtrace("Executing {$classname}..."); // Read a block of recordings to be updated. $recs = $this->get_recordngs_to_reset(); if (empty($recs)) { mtrace("No recordings were found for reset..."); // No more logs. Stop queueing. return false; } // Reset status of {chunksize} recordings. mtrace("Reset status of " . self::$chunksize . " recordings..."); $sql = "UPDATE {bigbluebuttonbn_recordings} SET status = :status_reset WHERE id = " . implode(' OR id = ', array_keys($recs)); $DB->execute($sql, ['status_reset' => recording::RECORDING_STATUS_RESET] ); return true; } /** * Get the list of recordings to be reset. * * @return array */ protected function get_recordngs_to_reset(): array { global $DB; return $DB->get_records_sql( 'SELECT * FROM {bigbluebuttonbn_recordings} WHERE status = :status_processed OR status = :status_notified ORDER BY timecreated DESC', [ 'status_processed' => recording::RECORDING_STATUS_PROCESSED, 'status_notified' => recording::RECORDING_STATUS_NOTIFIED ], 0, self::$chunksize ); } } PK\)classes/task/check_pending_recordings.phpnu[. namespace mod_bigbluebuttonbn\task; use core\task\scheduled_task; use mod_bigbluebuttonbn\recording; /** * Synchronise pending recordings from the server. * * @package mod_bigbluebuttonbn * @copyright 2021 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class check_pending_recordings extends scheduled_task { /** * Run the migration task. */ public function execute() { recording::sync_pending_recordings_from_server(); } /** * Get the name of the task for use in the interface. * * @return string */ public function get_name(): string { return get_string('taskname:check_pending_recordings', 'mod_bigbluebuttonbn'); } } PK\(Ph h (classes/task/upgrade_recordings_task.phpnu[. namespace mod_bigbluebuttonbn\task; use core\task\adhoc_task; use core\task\manager; use Matrix\Exception; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\local\proxy\recording_proxy; use mod_bigbluebuttonbn\logger; use mod_bigbluebuttonbn\recording; use moodle_exception; /** * Class containing the scheduled task for converting recordings for the BigBlueButton version 2.5 in Moodle 4.0. * * @package mod_bigbluebuttonbn * @copyright 2021 Jesus Federico, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class upgrade_recordings_task extends adhoc_task { /** * Run the migration task. */ public function execute() { $info = $this->get_custom_data(); $meetingid = $info->meetingid; $isimported = $info->isimported ?? 0; $this->process_bigbluebuttonbn_logs($meetingid, $isimported); } /** * Process all bigbluebuttonbn logs looking for entries which should be converted to meetings. * * @param string $meetingid * @param bool $isimported * @return bool Whether any more logs are waiting to be processed * @throws \dml_exception * @throws moodle_exception */ protected function process_bigbluebuttonbn_logs(string $meetingid, bool $isimported): bool { global $DB; $classname = static::class; mtrace("Executing {$classname} for meeting {$meetingid}..."); // Fetch the logs queued for upgrade. mtrace("Fetching logs for conversion"); // Each log is ordered by timecreated. [$select, $params] = $this->get_sql_query_for_logs($meetingid, $isimported); $logsrs = $DB->get_recordset_select('bigbluebuttonbn_logs', $select, $params, 'timecreated DESC', 'id, meetingid, timecreated, log'); if (!$logsrs->valid()) { mtrace("No logs were found for conversion."); // No more logs. Stop queueing. return false; } // Retrieve recordings from the servers for this meeting. $recordings = recording_proxy::fetch_recording_by_meeting_id([$meetingid]); // Sort recordings by meetingId, then startTime. uasort($recordings, function($a, $b) { return $b['startTime'] - $a['startTime']; }); // Create an instance of bigbluebuttonbn_recording per valid recording. mtrace("Creating new recording records..."); $recordingcount = 0; foreach ($recordings as $recordingid => $recording) { $importeddata = $isimported ? '' : json_encode($recording); try { $instance = instance::get_from_meetingid($recording['meetingID']); } catch (Exception $e) { mtrace("Unable to parse meetingID " . $e->getMessage()); continue; } if ($instance) { $newrecording = [ 'courseid' => $instance->get_course_id(), 'bigbluebuttonbnid' => $instance->get_instance_id(), 'groupid' => $instance->get_group_id(), // The groupid should be taken from the meetingID. 'recordingid' => $recordingid, 'status' => recording::RECORDING_STATUS_PROCESSED, ]; } else { mtrace("Unable to find an activity for {$recording['meetingID']}. This recording is headless."); // This instance does not exist any more. // Use the data in the log instead of the instance. $meetingdata = instance::parse_meetingid($recording['meetingID']); $newrecording = [ 'courseid' => $meetingdata['courseid'], 'bigbluebuttonbnid' => $meetingdata['instanceid'], 'groupid' => 0, 'recordingid' => $recordingid, 'status' => recording::RECORDING_STATUS_PROCESSED, ]; if (array_key_exists('groupid', $meetingdata)) { $newrecording['groupid'] = $meetingdata['groupid']; } } if ($DB->record_exists('bigbluebuttonbn_recordings', $newrecording)) { mtrace("A recording already exists for {$recording['recordID']}. Skipping."); // A recording matching these characteristics alreay exists. continue; } // Recording has not been imported, check if we still have more logs. // We try to guess which logs matches which recordings are they are classed in the same order. // But this is just an attempt. $log = null; if ($logsrs->valid()) { $log = $logsrs->current(); $logsrs->next(); } $timecreated = empty($log) ? time() : $log->timecreated; $newrecording['imported'] = $isimported; $newrecording['headless'] = 0; $newrecording['importeddata'] = $importeddata; $newrecording['timecreated'] = $newrecording['timemodified'] = $timecreated; // If we could not match with a log, we still create the recording. $DB->insert_record('bigbluebuttonbn_recordings', $newrecording); $recordingcount++; } mtrace("Migrated {$recordingcount} recordings."); // Now deactivate logs by marking all of them as migrated. // Reason for this is that we don't want to run another migration here and we don't know // which logs matches which recordings. $DB->set_field_select('bigbluebuttonbn_logs', 'log', $isimported ? logger::EVENT_IMPORT_MIGRATED : logger::EVENT_CREATE_MIGRATED, $select, $params ); $logsrs->close(); return true; } /** * Get the query (records_select) for the logs to convert. * * Each log is ordered by timecreated. * * @param string $meetingid * @param bool $isimported * @return array */ protected function get_sql_query_for_logs(string $meetingid, bool $isimported): array { global $DB; if ($isimported) { return [ 'log = :logmatch AND meetingid = :meetingid', ['logmatch' => logger::EVENT_IMPORT, 'meetingid' => $meetingid], ]; } return [ 'log = :logmatch AND meetingid = :meetingid AND ' . $DB->sql_like('meta', ':match'), [ 'logmatch' => logger::EVENT_CREATE, 'match' => '%true%', 'meetingid' => $meetingid ], ]; } /** * Schedule all upgrading tasks. * * @param bool $importedrecordings * @return void * @throws \dml_exception */ public static function schedule_upgrade_per_meeting($importedrecordings = false) { global $DB; $meetingids = $DB->get_fieldset_sql( 'SELECT DISTINCT meetingid FROM {bigbluebuttonbn_logs} WHERE log = :createorimport', ['createorimport' => $importedrecordings ? logger::EVENT_IMPORT : logger::EVENT_CREATE] ); foreach ($meetingids as $mid) { $createdrecordingtask = new static(); $createdrecordingtask->set_custom_data((object) ['meetingid' => $mid, 'isimported' => $importedrecordings]); manager::queue_adhoc_task($createdrecordingtask); } } } PK\ W(classes/task/completion_update_state.phpnu[. namespace mod_bigbluebuttonbn\task; use core\task\adhoc_task; use core_user; use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; /** * Class containing the scheduled task for updating the completion state. * * @package mod_bigbluebuttonbn * @copyright 2019 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class completion_update_state extends adhoc_task { /** * Run bigbluebuttonbn cron. */ public function execute() { // Get the custom data. $data = $this->get_custom_data(); // Ensure the customdata structure is corect. if (empty($data->bigbluebuttonbn->id) || empty($data->userid)) { throw new \coding_exception("Task customdata was missing bigbluebuttonbn->id or userid"); } // If coursemodule does not exist, ignore (likely has been deleted). if (get_coursemodule_from_instance('bigbluebuttonbn', $data->bigbluebuttonbn->id) === false) { mtrace("Course module does not exist, ignoring."); return; } // If user does not exist, ignore (likely has been deleted). if (core_user::get_user($data->userid) === false) { mtrace("User does not exist, ignoring."); return; } mtrace("Task completion_update_state running for user {$data->userid}"); // Process the completion. bigbluebutton_proxy::update_completion_state($data->bigbluebuttonbn, $data->userid); } } PK\r EE'classes/task/base_send_notification.phpnu[. namespace mod_bigbluebuttonbn\task; use core\message\message; use core\task\adhoc_task; use mod_bigbluebuttonbn\instance; use moodle_exception; use stdClass; /** * Class containing the abstract class for notification processes in BBB. * * @package mod_bigbluebuttonbn * @copyright 2023 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class base_send_notification extends adhoc_task { /** @var instance */ protected $instance = null; /** @var object */ protected $coursecontact = null; /** * Execute the task. */ public function execute() { $this->send_all_notifications(); } /** * Append additional elements of custom data * * @param array $newdata */ protected function append_custom_data(array $newdata): void { if ($currentdata = (array) $this->get_custom_data()) { $newdata = array_merge($currentdata, $newdata); } $this->set_custom_data($newdata); } /** * Set the instanceid in the custom data. * * @param int $instanceid */ public function set_instance_id(int $instanceid): void { $this->append_custom_data(['instanceid' => $instanceid]); } /** * Get the bigbluebutton instance that this notification is for. * * @return instance|null null if the instance could not be loaded. */ protected function get_instance(): ?instance { // This means the customdata is broken, and needs to be fixed. if (empty($this->get_custom_data()->instanceid)) { throw new \coding_exception("Task custom data was missing instanceid"); } if ($this->instance === null) { $this->instance = instance::get_from_instanceid($this->get_custom_data()->instanceid); } return $this->instance; } /** * Get the preferred course contact for this notification. * * @return stdClass */ protected function get_course_contact(): stdClass { global $DB; if ($this->coursecontact === null) { // Get course managers so they can be highlighted in the list. $coursecontext = $this->get_instance()->get_course_context(); if ($managerroles = get_config('', 'coursecontact')) { $coursecontactroles = explode(',', $managerroles); foreach ($coursecontactroles as $roleid) { $contacts = get_role_users($roleid, $coursecontext, true, 'u.id', 'u.id ASC'); foreach ($contacts as $contact) { $this->coursecontact = $contact; break; } } } if ($this->coursecontact === null) { $this->coursecontact = \core_user::get_noreply_user(); } } return $this->coursecontact; } /** * Get the list of recipients for the notification. * * @return stdClass[] */ protected function get_recipients(): array { // Potential users should be active users only. return get_enrolled_users( $this->get_instance()->get_course_context(), 'mod/bigbluebuttonbn:view', 0, 'u.*', null, 0, 0, true ); } /** * Get the HTML message content. * * @return string */ abstract protected function get_html_message(): string; /** * Get the plain text message content. * * @return string */ protected function get_message(): string { return html_to_text($this->get_html_message()); } /** * Get the short summary message. * * @return string */ abstract protected function get_small_message(): string; /** * Get the preferred message format * * @return string */ protected function get_message_format(): string { return FORMAT_HTML; } /** * Get the notification type. * * @return string */ abstract protected function get_notification_type(): string; /** * Get the subject of the notification. * * @return string */ abstract protected function get_subject(): string; /** * Send all of the notifications */ protected function send_all_notifications(): void { $instance = $this->get_instance(); // Cannot do anything without a valid instance. if (empty($instance)) { mtrace("Instance was empty, skipping"); return; } foreach ($this->get_recipients() as $recipient) { try { \core_user::require_active_user($recipient, true, true); \core\cron::setup_user($recipient); } catch (moodle_exception $e) { // Skip sending. continue; } $this->send_notification_to_current_user(); } \core\cron::setup_user(); } /** * Send the notificiation to the current user. */ protected function send_notification_to_current_user(): void { global $USER; $instance = $this->get_instance(); $eventdata = new message(); $eventdata->courseid = $instance->get_course_id(); $eventdata->component = 'mod_bigbluebuttonbn'; $eventdata->name = $this->get_notification_type(); $eventdata->userfrom = $this->get_course_contact(); $eventdata->userto = $USER; $eventdata->subject = $this->get_subject(); $eventdata->smallmessage = $this->get_small_message(); $eventdata->fullmessage = $this->get_message(); $eventdata->fullmessageformat = $this->get_message_format(); $eventdata->fullmessagehtml = $this->get_html_message(); $eventdata->notification = 1; $eventdata->contexturl = $this->get_instance()->get_view_url(); $eventdata->contexturlname = $this->get_instance()->get_meeting_name(); message_send($eventdata); } } PK\4m 2classes/task/send_recording_ready_notification.phpnu[. namespace mod_bigbluebuttonbn\task; use html_writer; /** * Class containing the adhoc task to send a recording ready notification. * * @package mod_bigbluebuttonbn * @copyright 2021 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class send_recording_ready_notification extends base_send_notification { /** * Get the notification type. * * @return string */ protected function get_notification_type(): string { return 'recording_ready'; } /** * Get the subject of the notification. * * @return string */ protected function get_subject(): string { $instance = $this->get_instance(); return get_string('notification_recording_ready_subject', 'mod_bigbluebuttonbn', $this->get_string_vars()); } /** * Get the short summary message. * * @return string */ protected function get_small_message(): string { return get_string('notification_recording_ready_small', 'mod_bigbluebuttonbn', $this->get_string_vars()); } /** * Get the HTML message content. * * @return string */ protected function get_html_message(): string { return html_writer::tag( 'p', get_string('notification_recording_ready_html', 'mod_bigbluebuttonbn', $this->get_string_vars()) ); } /** * Get variables to make available to strings. * * @return array */ protected function get_string_vars(): array { return [ 'course_fullname' => $this->instance->get_course()->fullname, 'course_shortname' => $this->instance->get_course()->shortname, 'name' => $this->instance->get_cm()->name, 'link' => $this->instance->get_view_url()->out(), ]; } } PK\AnA 0 0(classes/completion/custom_completion.phpnu[. namespace mod_bigbluebuttonbn\completion; use cm_info; use core_completion\activity_custom_completion; use mod_bigbluebuttonbn\extension; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\logger; use moodle_exception; use stdClass; /** * Class custom_completion * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ class custom_completion extends activity_custom_completion { /** * Filters for logs */ const FILTERS = [ 'completionattendance' => [logger::EVENT_SUMMARY], 'completionengagementchats' => [logger::EVENT_SUMMARY], 'completionengagementtalks' => [logger::EVENT_SUMMARY], 'completionengagementraisehand' => [logger::EVENT_SUMMARY], 'completionengagementpollvotes' => [logger::EVENT_SUMMARY], 'completionengagementemojis' => [logger::EVENT_SUMMARY], ]; /** * @var array $completionaddons array of extension class for the completion */ private $completionaddons; /** * activity_custom_completion constructor. * * @param cm_info $cm * @param int $userid * @param array|null $completionstate The current state of the core completion criteria */ public function __construct(cm_info $cm, int $userid, ?array $completionstate = null) { parent::__construct($cm, $userid, $completionstate); $completionaddonsclasses = extension::custom_completion_addons_instances($cm, $userid, $completionstate); $this->completionaddons = array_map(function($targetclassname) use ($cm, $userid, $completionstate) { return new $targetclassname($cm, $userid, $completionstate); }, $completionaddonsclasses); } /** * Get current state * * @param string $rule * @return int */ public function get_state(string $rule): int { // Get instance details. $instance = instance::get_from_cmid($this->cm->id); if (empty($instance)) { throw new moodle_exception("Can't find bigbluebuttonbn instance {$this->cm->instance}"); } // Default return value. $returnedvalue = COMPLETION_INCOMPLETE; $filters = self::FILTERS[$rule] ?? [logger::EVENT_SUMMARY]; $logs = logger::get_user_completion_logs($instance, $this->userid, $filters); if (method_exists($this, "get_{$rule}_value")) { $completionvalue = $this->aggregate_values($logs, self::class . "::get_{$rule}_value"); if ($completionvalue) { // So in this case we check the value set in the module setting. If we go over the threshold, then // this is complete. $rulevalue = $instance->get_instance_var($rule); if (!is_null($rulevalue)) { if ($rulevalue <= $completionvalue) { $returnedvalue = COMPLETION_COMPLETE; } } else { // If there is at least a hit, we consider it as complete. $returnedvalue = $completionvalue ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE; } } } // Check for any completion for this rule in addons / extensions. foreach ($this->completionaddons as $customcompletion) { if (in_array($rule, $customcompletion->get_defined_custom_rules())) { $returnedvalue = $returnedvalue || $customcompletion->get_state($rule); } } return $returnedvalue; } /** * Compute current state from logs. * * @param array $logs * @param callable $logvaluegetter * @return int the sum of all values for this particular event (it can be a duration or a number of hits) */ protected function aggregate_values(array $logs, callable $logvaluegetter): int { if (empty($logs)) { // As completion by engagement with $rulename hand was required, the activity hasn't been completed. return 0; } $value = 0; foreach ($logs as $log) { $value += $logvaluegetter($log); } return $value; } /** * Fetch the list of custom completion rules that this module defines. * * @return array */ public static function get_defined_custom_rules(): array { $rules = [ 'completionattendance', 'completionengagementchats', 'completionengagementtalks', 'completionengagementraisehand', 'completionengagementpollvotes', 'completionengagementemojis', ]; $completionaddonsclasses = extension::custom_completion_addons_classes(); foreach ($completionaddonsclasses as $customcompletion) { $rules = array_merge($rules, $customcompletion::get_defined_custom_rules()); } return $rules; } /** * Returns an associative array of the descriptions of custom completion rules. * * @return array */ public function get_custom_rule_descriptions(): array { $completionengagementchats = $this->cm->customdata['customcompletionrules']['completionengagementchats'] ?? 1; $completionengagementtalks = $this->cm->customdata['customcompletionrules']['completionengagementtalks'] ?? 1; $completionengagementraisehand = $this->cm->customdata['customcompletionrules']['completionengagementraisehand'] ?? 1; $completionengagementpollvotes = $this->cm->customdata['customcompletionrules']['completionengagementpollvotes'] ?? 1; $completionengagementemojis = $this->cm->customdata['customcompletionrules']['completionengagementemojis'] ?? 1; $completionattendance = $this->cm->customdata['customcompletionrules']['completionattendance'] ?? 1; $descriptions = [ 'completionengagementchats' => get_string('completionengagementchats_desc', 'mod_bigbluebuttonbn', $completionengagementchats), 'completionengagementtalks' => get_string('completionengagementtalks_desc', 'mod_bigbluebuttonbn', $completionengagementtalks), 'completionengagementraisehand' => get_string('completionengagementraisehand_desc', 'mod_bigbluebuttonbn', $completionengagementraisehand), 'completionengagementpollvotes' => get_string('completionengagementpollvotes_desc', 'mod_bigbluebuttonbn', $completionengagementpollvotes), 'completionengagementemojis' => get_string('completionengagementemojis_desc', 'mod_bigbluebuttonbn', $completionengagementemojis), 'completionattendance' => get_string('completionattendance_desc', 'mod_bigbluebuttonbn', $completionattendance), ]; // Check for any completion for this rule in addons / extensions. foreach ($this->completionaddons as $customcompletion) { $descriptions = array_merge($descriptions, $customcompletion->get_custom_rule_descriptions()); } return $descriptions; } /** * Returns an array of all completion rules, in the order they should be displayed to users. * * @return array */ public function get_sort_order(): array { $rules = self::get_defined_custom_rules(); array_unshift($rules, 'completionview', 'completionusegrade', 'completionpassgrade'); return $rules; } /** * Get current states of completion in a human-friendly version * * @return string[] */ public function get_printable_states(): array { $result = []; foreach ($this->get_available_custom_rules() as $rule) { $result[] = $this->get_printable_state($rule); } return $result; } /** * Get current states of completion for a rule in a human-friendly version * * @param string $rule * @return string */ private function get_printable_state(string $rule): string { // Get instance details. $instance = instance::get_from_cmid($this->cm->id); if (empty($instance)) { throw new moodle_exception("Can't find bigbluebuttonbn instance {$this->cm->instance}"); } $summary = ""; $filters = self::FILTERS[$rule] ?? [logger::EVENT_SUMMARY]; $logs = logger::get_user_completion_logs($instance, $this->userid, $filters); if (method_exists($this, "get_{$rule}_value")) { $summary = get_string( $rule . '_event_desc', 'mod_bigbluebuttonbn', $this->aggregate_values($logs, self::class . "::get_{$rule}_value") ); } return $summary; } /** * Get current state in a friendly version * * @param string $rule * @return string */ public function get_last_log_timestamp(string $rule): string { // Get instance details. $instance = instance::get_from_cmid($this->cm->id); if (empty($instance)) { throw new moodle_exception("Can't find bigbluebuttonbn instance {$this->cm->instance}"); } $filters = self::FILTERS[$rule] ?? [logger::EVENT_SUMMARY]; return logger::get_user_completion_logs_max_timestamp($instance, $this->userid, $filters); } /** * Get attendance summary value * * @param stdClass $log * @return int */ protected static function get_completionattendance_value(stdClass $log): int { $summary = json_decode($log->meta); return empty($summary->data->duration) ? 0 : (int)($summary->data->duration / 60); } /** * Get general completion engagement value * * @param stdClass $log * @return int */ protected static function get_completionengagementchats_value(stdClass $log): int { return self::get_completionengagement_value($log, 'chats'); } /** * Get general completion engagement value * * @param stdClass $log * @return int */ protected static function get_completionengagementtalks_value(stdClass $log): int { return self::get_completionengagement_value($log, 'talks'); } /** * Get general completion engagement value * * @param stdClass $log * @return int */ protected static function get_completionengagementraisehand_value(stdClass $log): int { return self::get_completionengagement_value($log, 'raisehand'); } /** * Get general completion engagement value * * @param stdClass $log * @return int */ protected static function get_completionengagementpollvotes_value(stdClass $log): int { return self::get_completionengagement_value($log, 'poll_votes'); } /** * Get general completion engagement value * * @param stdClass $log * @return int */ protected static function get_completionengagementemojis_value(stdClass $log): int { return self::get_completionengagement_value($log, 'emojis'); } /** * Get general completion engagement value * * @param stdClass $log * @param string $type * @return int */ protected static function get_completionengagement_value(stdClass $log, string $type): int { $summary = json_decode($log->meta); return intval($summary->data->engagement->$type ?? 0); } } PK\򲽢/classes/analytics/indicator/cognitive_depth.phpnu[. /** * Cognitive depth indicator - BigBlueButtonBN. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) */ namespace mod_bigbluebuttonbn\analytics\indicator; use cm_info; use lang_string; /** * Cognitive depth indicator - bigbluebuttonbn. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cognitive_depth extends activity_base { /** * Returns the name. * * If there is a corresponding '_help' string this will be shown as well. * * @return lang_string */ public static function get_name(): lang_string { return new lang_string('indicator:cognitivedepth', 'mod_bigbluebuttonbn'); } /** * Returns the indicator type. * * @return string */ public function get_indicator_type() { return self::INDICATOR_COGNITIVE; } /** * Returns the cognitive depth level. * * @param cm_info $cm * * @return int */ public function get_cognitive_depth_level(cm_info $cm) { return self::COGNITIVE_LEVEL_4; } } PK\斈^-classes/analytics/indicator/activity_base.phpnu[. /** * Activity base class. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) */ namespace mod_bigbluebuttonbn\analytics\indicator; use core_analytics\local\indicator\community_of_inquiry_activity; /** * Activity base class. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class activity_base extends community_of_inquiry_activity { /** * No need to fetch grades for resources. * * @return bool */ public function feedback_check_grades() { // BigBlueButtonBN's feedback is not contained in grades. return false; } } PK\H{k.classes/analytics/indicator/social_breadth.phpnu[. /** * Social breadth indicator - BigBlueButtonBN. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) */ namespace mod_bigbluebuttonbn\analytics\indicator; use cm_info; use lang_string; /** * Social breadth indicator - BigBlueButtonBN. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class social_breadth extends activity_base { /** * Returns the name. * * If there is a corresponding '_help' string this will be shown as well. * * @return lang_string */ public static function get_name(): lang_string { return new lang_string('indicator:socialbreadth', 'mod_bigbluebuttonbn'); } /** * Returns the indicator type. * * @return string */ public function get_indicator_type() { return self::INDICATOR_SOCIAL; } /** * Returns the social breadth level. * * @param cm_info $cm * * @return int */ public function get_social_breadth_level(cm_info $cm) { return self::SOCIAL_LEVEL_1; } } PK\>66&classes/test/testcase_helper_trait.phpnu[. /** * BBB Library tests class trait. * * @package mod_bigbluebuttonbn * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ namespace mod_bigbluebuttonbn\test; use context_module; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\local\config; use mod_bigbluebuttonbn\local\proxy\recording_proxy; use mod_bigbluebuttonbn\meeting; use stdClass; use testing_data_generator; use core\plugininfo\mod; trait testcase_helper_trait { /** @var testing_data_generator|null */ protected $generator = null; /** @var object|null */ protected $course = null; /** * Convenience function to create an instance of a bigbluebuttonactivty. * * @param stdClass|null $course course to add the module to * @param array $params Array of parameters to pass to the generator * @param array $options Array of options to pass to the generator * @return array($context, $cm, $instance) Testable wrapper around the assign class. */ protected function create_instance(?stdClass $course = null, array $params = [], array $options = []): array { // Prior to creating the instance, make sure that the BigBlueButton module is enabled. $modules = \core_plugin_manager::instance()->get_plugins_of_type('mod'); if (!$modules['bigbluebuttonbn']->is_enabled()) { mod::enable_plugin('bigbluebuttonbn', true); } if (!$course) { $course = $this->get_course(); } $params['course'] = $course->id; $options['visible'] = 1; $instance = $this->getDataGenerator()->create_module('bigbluebuttonbn', $params, $options); list($course, $cm) = get_course_and_cm_from_instance($instance, 'bigbluebuttonbn'); $context = context_module::instance($cm->id); return [$context, $cm, $instance]; } /** * Get the matching form data * * @param stdClass $bbactivity the current bigbluebutton activity * @param stdClass|null $course the course or null (taken from $this->get_course() if null) * @return mixed */ protected function get_form_data_from_instance(stdClass $bbactivity, ?stdClass $course = null): object { global $USER; if (!$course) { $course = $this->get_course(); } $this->setAdminUser(); $bbactivitycm = get_coursemodule_from_instance('bigbluebuttonbn', $bbactivity->id); list($cm, $context, $module, $data, $cw) = get_moduleinfo_data($bbactivitycm, $course); $this->setUser($USER); return $data; } /** * Get or create course if it does not exist * * @return stdClass|null */ protected function get_course(): stdClass { if (!$this->course) { $this->course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]); } return $this->course; } /** * Generate a course, several students and several groups * * @param stdClass $courserecord * @param int $numstudents * @param int $numteachers * @param int $groupsnum * @return array */ protected function setup_course_students_teachers(stdClass $courserecord, int $numstudents, int $numteachers, int $groupsnum): array { global $DB; $generator = $this->getDataGenerator(); $course = $generator->create_course($courserecord); $groups = []; for ($i = 0; $i < $groupsnum; $i++) { $groups[] = $generator->create_group(['courseid' => $course->id]); } $generator->create_group(['courseid' => $course->id]); $generator->create_group(['courseid' => $course->id]); $roleids = $DB->get_records_menu('role', null, '', 'shortname, id'); $students = []; for ($i = 0; $i < $numstudents; $i++) { $student = $generator->create_user(); $generator->enrol_user($student->id, $course->id, $roleids['student']); $groupid = $groups[$i % $groupsnum]->id; groups_add_member($groupid, $student->id); $students[] = $student; } $teachers = []; for ($i = 0; $i < $numteachers; $i++) { $teacher = $generator->create_user(); $generator->enrol_user($teacher->id, $course->id, $roleids['teacher']); $groupid = $groups[$i % $groupsnum]->id; groups_add_member($groupid, $teacher->id); $teachers[] = $teacher; } $bbactivity = $generator->create_module( 'bigbluebuttonbn', ['course' => $course->id], ['visible' => true]); get_fast_modinfo(0, 0, true); return [$course, $groups, $students, $teachers, $bbactivity, $roleids]; } /** * This test requires mock server to be present. */ protected function initialise_mock_server(): void { if (!defined('TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER')) { $this->markTestSkipped( 'The TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER constant must be defined to run mod_bigbluebuttonbn tests' ); } try { $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn')->reset_mock(); // Mock server expects a value. By default this field is empty. set_config('bigbluebuttonbn_shared_secret', config::DEFAULT_SHARED_SECRET); } catch (\moodle_exception $e) { $this->markTestSkipped( 'Cannot connect to the mock server for this test. Make sure that TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER points to an active Mock server' ); } } /** * Create an return an array of recordings * * @param instance $instance * @param array $recordingdata array of recording information * @param array $additionalmeetingdata * @return array */ protected function create_recordings_for_instance(instance $instance, array $recordingdata = [], $additionalmeetingdata = []): array { $recordings = []; $bbbgenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); // Create the meetings on the mock server, so like this we can find the recordings. $meeting = new meeting($instance); $meeting->update_cache(); // The meeting has just been created but we need to force fetch info from the server. if (!$meeting->is_running()) { $additionalmeetingdata = array_merge([ 'instanceid' => $instance->get_instance_id(), 'groupid' => $instance->get_group_id() ], $additionalmeetingdata); $bbbgenerator->create_meeting($additionalmeetingdata); } foreach ($recordingdata as $rindex => $data) { $recordings[] = $bbbgenerator->create_recording( array_merge([ 'bigbluebuttonbnid' => $instance->get_instance_id(), 'groupid' => $instance->get_group_id() ], $data) ); } return $recordings; } /** * Create an activity which includes a set of recordings. * * @param stdClass $course * @param int $type * @param array $recordingdata array of recording information * @param int $groupid * @return array */ protected function create_activity_with_recordings(stdClass $course, int $type, array $recordingdata, int $groupid = 0): array { $generator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); $activity = $generator->create_instance([ 'course' => $course->id, 'type' => $type ]); $instance = instance::get_from_instanceid($activity->id); $instance->set_group_id($groupid); $recordings = $this->create_recordings_for_instance($instance, $recordingdata); return [ 'course' => $course, 'activity' => $activity, 'recordings' => $recordings, ]; } /** * Create a course, users and recording from dataset given in an array form * * @param array $dataset * @return mixed */ protected function create_from_dataset(array $dataset) { list('type' => $type, 'recordingsdata' => $recordingsdata, 'groups' => $groups, 'users' => $users) = $dataset; $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); $coursedata = empty($groups) ? [] : ['groupmodeforce' => true, 'groupmode' => $dataset['coursemode'] ?? VISIBLEGROUPS]; $this->course = $this->getDataGenerator()->create_course($coursedata); foreach ($users as $userdata) { $this->getDataGenerator()->create_and_enrol($this->course, $userdata['role'], ['username' => $userdata['username']]); } if ($groups) { foreach ($groups as $groupname => $students) { $group = $this->getDataGenerator()->create_group(['name' => $groupname, 'courseid' => $this->course->id]); foreach ($students as $username) { $user = \core_user::get_user_by_username($username); $this->getDataGenerator()->create_group_member(['userid' => $user->id, 'groupid' => $group->id]); } } } $instancesettings = [ 'course' => $this->course->id, 'type' => $type, 'name' => 'Example', ]; if (!empty($dataset['additionalsettings'])) { $instancesettings = array_merge($instancesettings, $dataset['additionalsettings']); } $activity = $plugingenerator->create_instance($instancesettings); $instance = instance::get_from_instanceid($activity->id); foreach ($recordingsdata as $groupname => $recordings) { if ($groups) { $groupid = groups_get_group_by_name($this->course->id, $groupname); $instance->set_group_id($groupid); } $this->create_recordings_for_instance($instance, $recordings); } return $activity->id; } /** * Create the legacy log entries for this task. * * @param instance $instance * @param int $userid * @param int $count * @param bool $importedrecordings * @param bool $withremoterecordings create recording on the mock server ? * @return array */ protected function create_log_entries( instance $instance, int $userid, int $count = 30, bool $importedrecordings = false, bool $withremoterecordings = true ): array { // Create log entries for each (30 for the ungrouped, 30 for the grouped). $baselogdata = [ 'courseid' => $instance->get_course_id(), 'userid' => $userid, 'log' => $importedrecordings ? 'Import' : 'Create', 'meta' => json_encode(['record' => true]), 'imported' => $importedrecordings, ]; $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); for ($i = 0; $i < $count; $i++) { if ($withremoterecordings) { // Create a recording. $starttime = time() - random_int(HOURSECS, WEEKSECS); $recording = $plugingenerator->create_recording([ 'bigbluebuttonbnid' => $instance->get_instance_id(), 'groupid' => $instance->get_group_id(), 'starttime' => $starttime, 'endtime' => $starttime + HOURSECS, ], true); // Create them on the server only. $baselogdata['meetingid'] = $instance->get_meeting_id(); if ($importedrecordings) { // Fetch the data. $data = recording_proxy::fetch_recordings([$recording->recordingid]); $data = end($data); if ($data) { $metaonly = array_filter($data, function($key) { return strstr($key, 'meta_'); }, ARRAY_FILTER_USE_KEY); } else { $data = []; } $baselogdata['meta'] = json_encode(array_merge([ 'recording' => array_diff_key($data, $metaonly), ], $metaonly)); } else { $baselogdata['meta'] = json_encode((object) ['record' => true]); } } // Insert the legacy log entry. $logs[] = $plugingenerator->create_log(array_merge($baselogdata, [ 'bigbluebuttonbnid' => $instance->get_instance_id(), 'timecreated' => time() - random_int(HOURSECS, WEEKSECS) + (HOURSECS * $i), ])); } return $logs; } } PK\-classes/test/subplugins_test_helper_trait.phpnu[. /** * Subplugin test helper trait * * @package mod_bigbluebuttonbn * @copyright 2023 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ namespace mod_bigbluebuttonbn\test; use core_component; use core_h5p\core; use core_plugin_manager; use mod_bigbluebuttonbn\extension; use ReflectionClass; trait subplugins_test_helper_trait { /** * Setup a fake extension plugin * * This is intended to behave in most case like a real subplugina and will * allow most functionalities to be tested. * * @param string $pluginname plugin name * @return void */ protected function setup_fake_plugin(string $pluginname): void { global $CFG; require_once("$CFG->libdir/upgradelib.php"); $bbbextpath = "{$CFG->dirroot}/mod/bigbluebuttonbn/tests/fixtures/extension"; // This is similar to accesslib_test::setup_fake_plugin. $mockedcomponent = new ReflectionClass(core_component::class); $mockedplugins = $mockedcomponent->getProperty('plugins'); $plugins = $mockedplugins->getValue(); $plugins[extension::BBB_EXTENSION_PLUGIN_NAME] = [$pluginname => $bbbextpath . "/$pluginname"]; $mockedplugins->setValue(null, $plugins); $mockedplugintypes = $mockedcomponent->getProperty('plugintypes'); $pluginstypes = $mockedplugintypes->getValue(); $pluginstypes[extension::BBB_EXTENSION_PLUGIN_NAME] = $bbbextpath; $mockedplugintypes->setValue(null, $pluginstypes); $fillclassmap = $mockedcomponent->getMethod('fill_classmap_cache'); $fillclassmap->invoke(null); $fillfilemap = $mockedcomponent->getMethod('fill_filemap_cache'); $fillfilemap->invoke(null); $mockedsubplugins = $mockedcomponent->getProperty('subplugins'); $subplugins = $mockedsubplugins->getValue(); $subplugins['mod_bigbluebuttonbn'][extension::BBB_EXTENSION_PLUGIN_NAME][] = $pluginname; $mockedsubplugins->setValue(null, $subplugins); // Now write the content of the cache in a file so we can use it later. $content = core_component::get_cache_content(); self::write_fake_component_cache($content); // Make sure the plugin is installed. ob_start(); upgrade_noncore(false); upgrade_finished(); ob_end_clean(); // Cache has been cleared so let's write it again. self::write_fake_component_cache($content); } /** * Write the content of the cache in a file for later use. * * This is used exclusively in behat test as the cache is filled with new values at each session/page load. * * @param string $content content of the cache * @return void */ protected function write_fake_component_cache($content) { global $CFG; $cachefile = "$CFG->cachedir/core_component.php"; if (file_exists($cachefile)) { // Stale cache detected! unlink($cachefile); } // Permissions might not be setup properly in installers. $dirpermissions = !isset($CFG->directorypermissions) ? 02777 : $CFG->directorypermissions; $filepermissions = !isset($CFG->filepermissions) ? ($dirpermissions & 0666) : $CFG->filepermissions; clearstatcache(); $cachedir = dirname($cachefile); if (!is_dir($cachedir)) { mkdir($cachedir, $dirpermissions, true); } if ($fp = @fopen($cachefile . '.tmp', 'xb')) { fwrite($fp, $content); fclose($fp); @rename($cachefile . '.tmp', $cachefile); @chmod($cachefile, $filepermissions); } @unlink($cachefile . '.tmp'); // Just in case anything fails (race condition). core_component::invalidate_opcode_php_cache($cachefile); } /** * Uninstall a fake extension plugin * * This is intended to behave in most case like a real subplugina and will * allow most functionalities to be tested. * * @param string $pluginname plugin name * @return void */ protected function uninstall_fake_plugin(string $pluginname): void { global $CFG; require_once("$CFG->libdir/adminlib.php"); // We just need access to fill_all_caches so everything goes back to normal. // If we don't do this, there are some side effects that will make other test fails // (such as mod_bigbluebuttonbn\task\upgrade_recordings_task_test::test_upgrade_recordings_imported_basic). $cachefile = "$CFG->cachedir/core_component.php"; if (file_exists($cachefile)) { // Stale cache detected! unlink($cachefile); } $mockedcomponent = new ReflectionClass(core_component::class); // Here we reset the plugin caches. $mockedplugintypes = $mockedcomponent->getProperty('plugintypes'); $mockedplugintypes->setValue(null, null); $fillclassmap = $mockedcomponent->getMethod('init'); $fillclassmap->invoke(null); // Now uninstall the plugin and clean everything up for other tests. $pluginman = core_plugin_manager::instance(); $plugininfo = $pluginman->get_plugins(); foreach ($plugininfo as $type => $plugins) { foreach ($plugins as $name => $plugin) { if ($name === $pluginname) { ob_start(); uninstall_plugin($type, $name); ob_end_clean(); } } } } } PK\h>D>Dclasses/logger.phpnu[. namespace mod_bigbluebuttonbn; use mod_bigbluebuttonbn\event\events; use stdClass; /** * Utility class for all logs routines helper. * * @package mod_bigbluebuttonbn * @copyright 2021 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent [at] call-learning [dt] fr) */ class logger { /** @var string The bigbluebuttonbn Add event */ public const EVENT_ADD = 'Add'; /** @var string The bigbluebuttonbn Edit event */ public const EVENT_EDIT = 'Edit'; /** @var string The bigbluebuttonbn Create event */ public const EVENT_CREATE = 'Create'; /** @var string The bigbluebuttonbn Join event */ public const EVENT_JOIN = 'Join'; /** @var string The bigbluebuttonbn Playback event */ public const EVENT_PLAYED = 'Played'; /** @var string The bigbluebuttonbn Logout event */ public const EVENT_LOGOUT = 'Logout'; /** @var string The bigbluebuttonbn Import event */ public const EVENT_IMPORT = 'Import'; /** @var string The bigbluebuttonbn Delete event */ public const EVENT_DELETE = 'Delete'; /** @var string The bigbluebuttonbn Callback event */ public const EVENT_CALLBACK = 'Callback'; /** @var string The bigbluebuttonbn Summary event */ public const EVENT_SUMMARY = 'Summary'; /** @var string This is a specific log to mark this log as upgraded: used only in the upgrade process from 2.4 * * Note: Migrated event name change: once a log has been migrated we mark * it as migrated by changing its log name. This will help to recover * manually if we have an issue in the migration process. */ public const EVENT_IMPORT_MIGRATED = 'import-migrated'; /** @var string This is a specific log to mark this log as upgraded: used only in the upgrade process from 2.4 */ public const EVENT_CREATE_MIGRATED = 'create-migrated'; /** @var string The bigbluebuttonbn meeting_start event */ public const EVENT_MEETING_START = 'meeting_start'; /** @var int The user accessed the session from activity page */ public const ORIGIN_BASE = 0; /** @var int The user accessed the session from Timeline */ public const ORIGIN_TIMELINE = 1; /** @var int The user accessed the session from Index */ public const ORIGIN_INDEX = 2; /** * Get the user event logs related to completion, for the specified user in the named instance. * * @param instance $instance * @param int|null $userid * @param array|null $filters * @param int|null $timestart * @return array */ public static function get_user_completion_logs( instance $instance, ?int $userid, ?array $filters, ?int $timestart = 0 ): array { global $DB; $filters = $filters ?? [self::EVENT_JOIN, self::EVENT_PLAYED, self::EVENT_SUMMARY]; [$wheresql, $params] = static::get_user_completion_sql_params($instance, $userid, $filters, $timestart); return $DB->get_records_select('bigbluebuttonbn_logs', $wheresql, $params); } /** * Get the user event logs related to completion, for the specified user in the named instance. * * @param instance $instance * @param int|null $userid * @param array|null $filters * @param int|null $timestart * @return array */ public static function get_user_completion_logs_with_userfields( instance $instance, ?int $userid, ?array $filters, ?int $timestart = 0 ): array { global $DB; $filters = $filters ?? [self::EVENT_JOIN, self::EVENT_PLAYED, self::EVENT_SUMMARY]; [$wheresql, $params] = static::get_user_completion_sql_params($instance, $userid, $filters, $timestart, 'l'); $userfieldsapi = \core_user\fields::for_userpic(); $userfields = $userfieldsapi->get_sql('u', false, '', 'userid', false)->selects; $logtable = new \core\dml\table('bigbluebuttonbn_logs', 'l', ''); $logtableselect = $logtable->get_field_select(); $logtablefrom = $logtable->get_from_sql(); $usertable = new \core\dml\table('user', 'u', ''); $usertablefrom = $usertable->get_from_sql(); $sql = <<get_records_sql($sql, $params); } /** * Get the latest timestamp for any event logs related to completion, for the specified user in the named instance. * * @param instance $instance * @param int|null $userid * @param array|null $filters * @param int|null $timestart * @return int */ public static function get_user_completion_logs_max_timestamp( instance $instance, ?int $userid, ?array $filters, ?int $timestart = 0 ): int { global $DB; [$wheresql, $params] = static::get_user_completion_sql_params($instance, $userid, $filters, $timestart); $select = "SELECT MAX(timecreated) "; $lastlogtime = $DB->get_field_sql($select . ' FROM {bigbluebuttonbn_logs} WHERE ' . $wheresql, $params); return $lastlogtime ?? 0; } /** * Helper method to get the right SQL query for completion * * @param instance $instance * @param int|null $userid * @param array|null $filters * @param int|null $timestart * @param string|null $logtablealias * @return array */ protected static function get_user_completion_sql_params(instance $instance, ?int $userid, ?array $filters, ?int $timestart, ?string $logtablealias = null) { global $DB; $filters = $filters ?? [self::EVENT_JOIN, self::EVENT_PLAYED, self::EVENT_SUMMARY]; [$insql, $params] = $DB->get_in_or_equal($filters, SQL_PARAMS_NAMED); $wheres = []; $wheres['bigbluebuttonbnid'] = '= :instanceid'; $wheres['courseid'] = '= :courseid'; // This speeds up the requests masively as courseid is an index. if ($timestart) { $wheres['timecreated'] = ' > :timestart'; $params['timestart'] = $timestart; } if ($userid) { $wheres['userid'] = ' = :userid'; $params['userid'] = $userid; } $params['instanceid'] = $instance->get_instance_id(); $params['courseid'] = $instance->get_course_id(); $wheres['log'] = " $insql"; $wheresqls = []; foreach ($wheres as $key => $val) { $prefix = !empty($logtablealias) ? "$logtablealias." : ""; $wheresqls[] = "$prefix$key $val"; } return [join(' AND ', $wheresqls), $params]; } /** * Log that an instance was created. * * Note: This event cannot take the instance class as it is typically called before the cm has been configured. * * @param stdClass $instancedata */ public static function log_instance_created(stdClass $instancedata): void { self::raw_log( self::EVENT_ADD, $instancedata->id, $instancedata->course, $instancedata->meetingid ); } /** * Log that an instance was updated. * * @param instance $instance */ public static function log_instance_updated(instance $instance): void { self::log($instance, self::EVENT_EDIT); } /** * Log an instance deleted event. * * @param instance $instance */ public static function log_instance_deleted(instance $instance): void { global $DB; $wheresql = 'bigbluebuttonbnid = :instanceid AND log = :logtype AND ' . $DB->sql_compare_text('meta') . ' = :meta'; $logs = $DB->get_records_select('bigbluebuttonbn_logs', $wheresql, [ 'instanceid' => $instance->get_instance_id(), 'logtype' => self::EVENT_CREATE, 'meta' => "{\"record\":true}" ]); $meta = "{\"has_recordings\":" . empty($logs) ? "true" : "false" . "}"; self::log($instance, self::EVENT_DELETE, [], $meta); } /** * Log an event callback. * * @param instance $instance * @param array $overrides * @param array $meta * @return int The new count of callback events */ public static function log_event_callback(instance $instance, array $overrides, array $meta): int { self::log( $instance, self::EVENT_CALLBACK, $overrides, json_encode($meta) ); return self::count_callback_events($meta['internalmeetingid'], 'meeting_events'); } /** * Log an event summary event. * * @param instance $instance * @param array $overrides * @param array $meta */ public static function log_event_summary(instance $instance, array $overrides = [], array $meta = []): void { self::log( $instance, self::EVENT_SUMMARY, $overrides, json_encode($meta) ); } /** * Log that an instance was viewed. * * @param instance $instance */ public static function log_instance_viewed(instance $instance): void { self::log_moodle_event($instance, events::$events['view']); } /** * Log the events for when a meeting was ended. * * @param instance $instance */ public static function log_meeting_ended_event(instance $instance): void { // Moodle event logger: Create an event for meeting ended. self::log_moodle_event($instance, events::$events['meeting_end']); } /** * Log the relevant events for when a meeting was joined. * * @param instance $instance * @param int $origin */ public static function log_meeting_joined_event(instance $instance, int $origin): void { // Moodle event logger: Create an event for meeting joined. self::log_moodle_event($instance, events::$events['meeting_join']); // Internal logger: Instert a record with the meeting created. self::log( $instance, self::EVENT_JOIN, ['meetingid' => $instance->get_meeting_id()], json_encode((object) ['origin' => $origin]) ); } /** * Log the relevant events for when a user left a meeting. * * @param instance $instance */ public static function log_meeting_left_event(instance $instance): void { // Moodle event logger: Create an event for meeting left. self::log_moodle_event($instance, events::$events['meeting_left']); } /** * Log the relevant events for when a recording has been played. * * @param instance $instance * @param int $rid RecordID */ public static function log_recording_played_event(instance $instance, int $rid): void { // Moodle event logger: Create an event for recording played. self::log_moodle_event($instance, events::$events['recording_play'], ['other' => $rid]); // Internal logger: Insert a record with the playback played. self::log( $instance, self::EVENT_PLAYED, [ 'meetingid' => $instance->get_meeting_id(), ], json_encode(['recordingid' => $rid]) ); } /** * Register a bigbluebuttonbn event from an instance. * * @param instance $instance * @param string $event * @param array $overrides * @param string|null $meta * @return bool */ protected static function log(instance $instance, string $event, array $overrides = [], ?string $meta = null): bool { return self::raw_log( $event, $instance->get_instance_id(), $instance->get_course_id(), $instance->get_meeting_id(), $overrides, $meta ); } /** * Register a bigbluebuttonbn event from raw data. * * @param string $event * @param int $instanceid * @param int $courseid * @param string $meetingid * @param array $overrides * @param string|null $meta * @return bool */ protected static function raw_log( string $event, int $instanceid, int $courseid, string $meetingid, array $overrides = [], ?string $meta = null ): bool { global $DB, $USER; $log = (object) array_merge([ // Default values. 'courseid' => $courseid, 'bigbluebuttonbnid' => $instanceid, 'userid' => $USER->id, 'meetingid' => $meetingid, 'timecreated' => time(), 'log' => $event, 'meta' => $meta, ], $overrides); return !!$DB->insert_record('bigbluebuttonbn_logs', $log); } /** * Helper register a bigbluebuttonbn event. * * @param instance $instance * @param string $type * @param array $options [timecreated, userid, other] */ protected static function log_moodle_event(instance $instance, string $type, array $options = []): void { if (!in_array($type, \mod_bigbluebuttonbn\event\events::$events)) { // No log will be created. return; } $params = [ 'context' => $instance->get_context(), 'objectid' => $instance->get_instance_id(), ]; if (array_key_exists('timecreated', $options)) { $params['timecreated'] = $options['timecreated']; } if (array_key_exists('userid', $options)) { $params['userid'] = $options['userid']; } if (array_key_exists('other', $options)) { $params['other'] = $options['other']; } $event = call_user_func_array("\\mod_bigbluebuttonbn\\event\\{$type}::create", [$params]); $event->add_record_snapshot('course_modules', $instance->get_cm()); $event->add_record_snapshot('course', $instance->get_course()); $event->add_record_snapshot('bigbluebuttonbn', $instance->get_instance_data()); $event->trigger(); } /** * Helper function to count the number of callback logs matching the supplied specifications. * * @param string $id * @param string $callbacktype * @return int */ protected static function count_callback_events(string $id, string $callbacktype = 'recording_ready'): int { global $DB; // Look for a log record that is of "Callback" type and is related to the given event. $conditions = [ "log = :logtype", $DB->sql_like('meta', ':cbtypelike') ]; $params = [ 'logtype' => self::EVENT_CALLBACK, 'cbtypelike' => "%meeting_events%" // All callbacks are meeting events, even recording events. ]; $basesql = 'SELECT COUNT(DISTINCT id) FROM {bigbluebuttonbn_logs}'; switch ($callbacktype) { case 'recording_ready': $conditions[] = $DB->sql_like('meta', ':isrecordid'); $params['isrecordid'] = '%recordid%'; // The recordid field in the meta field (json encoded). break; case 'meeting_events': $conditions[] = $DB->sql_like('meta', ':idlike'); $params['idlike'] = "%$id%"; // The unique id of the meeting is the meta field (json encoded). break; } $wheresql = join(' AND ', $conditions); return $DB->count_records_sql($basesql . ' WHERE ' . $wheresql, $params); } /** * Log event to string that can be internationalised via get_string. */ const LOG_TO_STRING = [ self::EVENT_JOIN => 'event_meeting_joined', self::EVENT_PLAYED => 'event_recording_viewed', self::EVENT_IMPORT => 'event_recording_imported', self::EVENT_ADD => 'event_activity_created', self::EVENT_DELETE => 'event_activity_deleted', self::EVENT_EDIT => 'event_activity_updated', self::EVENT_SUMMARY => 'event_meeting_summary', self::EVENT_LOGOUT => 'event_meeting_left', self::EVENT_MEETING_START => 'event_meeting_joined', ]; /** * Get the event name (human friendly version) * * @param object $log object as returned by get_user_completion_logs_with_userfields */ public static function get_printable_event_name(object $log) { $logstringname = self::LOG_TO_STRING[$log->log] ?? 'event_unknown'; return get_string($logstringname, 'mod_bigbluebuttonbn'); } } PK\Կ`aaclasses/settings.phpnu[. namespace mod_bigbluebuttonbn; use admin_category; use admin_setting; use admin_setting_configcheckbox; use admin_setting_configmultiselect; use admin_setting_configpasswordunmask; use admin_setting_configselect; use admin_setting_configstoredfile; use admin_setting_configtext; use admin_setting_configtextarea; use admin_setting_heading; use admin_settingpage; use cache_helper; use core_plugin_manager; use lang_string; use mod_bigbluebuttonbn\local\config; use mod_bigbluebuttonbn\local\helpers\roles; use mod_bigbluebuttonbn\local\plugins\admin_page_manage_extensions; use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; /** * The mod_bigbluebuttonbn settings helper * * @package mod_bigbluebuttonbn * @copyright 2021 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent [at] call-learning [dt] fr) */ class settings { /** @var admin_category shared value */ private $admin; /** @var bool whether the current user has moodle/site:config capability */ private $hassiteconfig; /** @var bool Module is enabled */ private $moduleenabled; /** @var string The name of the section */ private $section; /** @var string The parent name */ private $parent = "modbigbluebuttonbnfolder"; /** @var string The section name prefix */ private $sectionnameprefix = "mod_bigbluebuttonbn"; /** * Constructor for the bigbluebuttonbn settings. * * @param admin_category $admin * @param \core\plugininfo\mod $module * @param string $categoryname for the plugin setting (main setting page) * @param bool $hassiteconfig whether the current user has moodle/site:config capability */ public function __construct(admin_category $admin, \core\plugininfo\mod $module, string $categoryname, bool $hassiteconfig) { $this->moduleenabled = $module->is_enabled() === true; $this->admin = $admin; $this->section = $categoryname; $this->hassiteconfig = $hassiteconfig; $modbigbluebuttobnfolder = new admin_category( $this->parent, new lang_string('pluginname', 'mod_bigbluebuttonbn'), $module->is_enabled() === false ); $admin->add('modsettings', $modbigbluebuttobnfolder); $mainsettings = $this->add_general_settings(); $admin->add($this->parent, $mainsettings); } /** * Add all settings. */ public function add_all_settings(): void { // Renders settings for welcome messages. $this->add_defaultmessages_settings(); // Evaluates if recordings are enabled for the Moodle site. // Renders settings for record feature. $this->add_record_settings(); // Renders settings for import recordings. $this->add_importrecordings_settings(); // Renders settings for showing recordings. $this->add_showrecordings_settings(); // Renders settings for meetings. $this->add_waitmoderator_settings(); $this->add_voicebridge_settings(); $this->add_preupload_settings(); $this->add_userlimit_settings(); $this->add_participants_settings(); $this->add_muteonstart_settings(); $this->add_lock_settings(); // Renders settings for extended capabilities. $this->add_extended_settings(); // Renders settings for experimental features. $this->add_experimental_settings(); // Add all subplugin settings if any. $this->admin->add($this->parent, new admin_category('bbbextplugins', new lang_string('subplugintype_bbbext', 'mod_bigbluebuttonbn'), !$this->moduleenabled)); $this->admin->add($this->parent, new admin_page_manage_extensions()); foreach (core_plugin_manager::instance()->get_plugins_of_type(extension::BBB_EXTENSION_PLUGIN_NAME) as $plugin) { $plugin->load_settings($this->admin, extension::BBB_EXTENSION_PLUGIN_NAME, $this->hassiteconfig); } } /** * Add the setting and lock it conditionally. * * @param string $name * @param admin_setting $item * @param admin_settingpage $settings */ protected function add_conditional_element(string $name, admin_setting $item, admin_settingpage $settings): void { global $CFG; if (isset($CFG->bigbluebuttonbn) && isset($CFG->bigbluebuttonbn[$name])) { if ($item->config_read($item->name)) { // A value has been set, we can safely omit the setting and it won't interfere with installation // process. // The idea behind it is to hide values from end-users in case we use multitenancy for example. return; } } $settings->add($item); } /** * Helper function renders general settings if the feature is enabled. * * @return admin_settingpage * @throws \coding_exception */ protected function add_general_settings(): admin_settingpage { global $CFG, $OUTPUT; $settingsgeneral = new admin_settingpage( $this->section, get_string('config_general', 'bigbluebuttonbn'), 'moodle/site:config', !((boolean) setting_validator::section_general_shown()) && ($this->moduleenabled) ); if ($this->admin->fulltree) { // Configuration for BigBlueButton. $item = new admin_setting_heading('bigbluebuttonbn_config_general', '', get_string('config_general_description', 'bigbluebuttonbn') ); $settingsgeneral->add($item); if (config::server_credentials_invalid()) { // A notification should appear when default credentials are used. $settingsgeneral->add(new admin_setting_heading( 'bigbluebuttonbn_notification', '', $OUTPUT->notification(get_string('credentials_warning', 'mod_bigbluebuttonbn'), 'error') )); } $item = new admin_setting_configtext( 'bigbluebuttonbn_server_url', get_string('config_server_url', 'bigbluebuttonbn'), get_string('config_server_url_description', 'bigbluebuttonbn'), '', PARAM_RAW ); $item->set_updatedcallback( function() { $this->reset_cache(); $task = new \mod_bigbluebuttonbn\task\reset_recordings(); \core\task\manager::queue_adhoc_task($task); } ); $this->add_conditional_element( 'server_url', $item, $settingsgeneral ); $item = new admin_setting_configpasswordunmask( 'bigbluebuttonbn_shared_secret', get_string('config_shared_secret', 'bigbluebuttonbn'), get_string('config_shared_secret_description', 'bigbluebuttonbn'), '' ); $this->add_conditional_element( 'shared_secret', $item, $settingsgeneral ); $item = new admin_setting_configselect( 'bigbluebuttonbn_checksum_algorithm', get_string('config_checksum_algorithm', 'bigbluebuttonbn'), get_string('config_checksum_algorithm_description', 'bigbluebuttonbn'), config::DEFAULT_CHECKSUM_ALGORITHM, array_combine(config::CHECKSUM_ALGORITHMS, config::CHECKSUM_ALGORITHMS) ); $this->add_conditional_element( 'checksum_algorithm', $item, $settingsgeneral ); $item = new admin_setting_configtext( 'bigbluebuttonbn_poll_interval', get_string('config_poll_interval', 'bigbluebuttonbn'), get_string('config_poll_interval_description', 'bigbluebuttonbn'), bigbluebutton_proxy::DEFAULT_POLL_INTERVAL, PARAM_INT ); $this->add_conditional_element( 'poll_interval', $item, $settingsgeneral ); } return $settingsgeneral; } /** * Helper function renders default messages settings if the feature is enabled. */ protected function add_defaultmessages_settings(): void { // Configuration for 'default messages' feature. $defaultmessagessetting = new admin_settingpage( "{$this->sectionnameprefix}_default_messages", get_string('config_default_messages', 'bigbluebuttonbn'), 'moodle/site:config', !((boolean) setting_validator::section_default_messages_shown()) && ($this->moduleenabled) ); if ($this->admin->fulltree) { $item = new admin_setting_heading( 'bigbluebuttonbn_config_default_messages', '', get_string('config_default_messages_description', 'bigbluebuttonbn') ); $defaultmessagessetting->add($item); $item = new admin_setting_configtextarea( 'bigbluebuttonbn_welcome_default', get_string('config_welcome_default', 'bigbluebuttonbn'), get_string('config_welcome_default_description', 'bigbluebuttonbn'), '', PARAM_TEXT ); $this->add_conditional_element( 'welcome_default', $item, $defaultmessagessetting ); $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_welcome_editable', get_string('config_welcome_editable', 'bigbluebuttonbn'), get_string('config_welcome_editable_description', 'bigbluebuttonbn'), 1, ); $this->add_conditional_element( 'welcome_editable', $item, $defaultmessagessetting ); } $this->admin->add($this->parent, $defaultmessagessetting); } /** * Helper function renders record settings if the feature is enabled. */ protected function add_record_settings(): void { // Configuration for 'recording' feature. $recordingsetting = new admin_settingpage( "{$this->sectionnameprefix}_recording", get_string('config_recording', 'bigbluebuttonbn'), 'moodle/site:config', !((boolean) setting_validator::section_record_meeting_shown()) && ($this->moduleenabled) ); if ($this->admin->fulltree) { $item = new admin_setting_heading( 'bigbluebuttonbn_config_recording', '', get_string('config_recording_description', 'bigbluebuttonbn') ); $recordingsetting->add($item); $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_recording_default', get_string('config_recording_default', 'bigbluebuttonbn'), get_string('config_recording_default_description', 'bigbluebuttonbn'), 1 ); $this->add_conditional_element( 'recording_default', $item, $recordingsetting ); $item = new admin_setting_configtext( 'bigbluebuttonbn_recording_refresh_period', get_string('config_recording_refresh_period', 'bigbluebuttonbn'), get_string('config_recording_refresh_period_description', 'bigbluebuttonbn'), recording::RECORDING_REFRESH_DEFAULT_PERIOD, PARAM_INT ); $this->add_conditional_element( 'recording_refresh_period', $item, $recordingsetting ); $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_recording_editable', get_string('config_recording_editable', 'bigbluebuttonbn'), get_string('config_recording_editable_description', 'bigbluebuttonbn'), 1 ); $this->add_conditional_element( 'recording_editable', $item, $recordingsetting ); // Add recording start to load and allow/hide stop/pause. $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_recording_all_from_start_default', get_string('config_recording_all_from_start_default', 'bigbluebuttonbn'), get_string('config_recording_all_from_start_default_description', 'bigbluebuttonbn'), 0 ); $this->add_conditional_element( 'recording_all_from_start_default', $item, $recordingsetting ); $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_recording_all_from_start_editable', get_string('config_recording_all_from_start_editable', 'bigbluebuttonbn'), get_string('config_recording_all_from_start_editable_description', 'bigbluebuttonbn'), 0 ); $this->add_conditional_element( 'recording_all_from_start_editable', $item, $recordingsetting ); $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_recording_hide_button_default', get_string('config_recording_hide_button_default', 'bigbluebuttonbn'), get_string('config_recording_hide_button_default_description', 'bigbluebuttonbn'), 0 ); $this->add_conditional_element( 'recording_hide_button_default', $item, $recordingsetting ); $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_recording_hide_button_editable', get_string('config_recording_hide_button_editable', 'bigbluebuttonbn'), get_string('config_recording_hide_button_editable_description', 'bigbluebuttonbn'), 0 ); $this->add_conditional_element( 'recording_hide_button_editable', $item, $recordingsetting ); $recordingsafeformat = [ 'notes' => get_string('view_recording_format_notes', 'mod_bigbluebuttonbn'), 'podcast' => get_string('view_recording_format_podcast', 'mod_bigbluebuttonbn'), 'presentation' => get_string('view_recording_format_presentation', 'mod_bigbluebuttonbn'), 'screenshare' => get_string('view_recording_format_screenshare', 'mod_bigbluebuttonbn'), 'statistics' => get_string('view_recording_format_statistics', 'mod_bigbluebuttonbn'), 'video' => get_string('view_recording_format_video', 'mod_bigbluebuttonbn'), ]; $item = new admin_setting_configmultiselect( 'bigbluebuttonbn_recording_safe_formats', get_string('config_recording_safe_formats', 'mod_bigbluebuttonbn'), get_string('config_recording_safe_formats_description', 'mod_bigbluebuttonbn'), ['video', 'presentation'], $recordingsafeformat ); $this->add_conditional_element( 'recording_hide_button_editable', $item, $recordingsetting ); } $this->admin->add($this->parent, $recordingsetting); } /** * Helper function renders import recording settings if the feature is enabled. */ protected function add_importrecordings_settings(): void { // Configuration for 'import recordings' feature. $importrecordingsettings = new admin_settingpage( "{$this->sectionnameprefix}_importrecording", get_string('config_importrecordings', 'bigbluebuttonbn'), 'moodle/site:config', !((boolean) setting_validator::section_import_recordings_shown()) && ($this->moduleenabled) ); if ($this->admin->fulltree) { $item = new admin_setting_heading( 'bigbluebuttonbn_config_importrecordings', '', get_string('config_importrecordings_description', 'bigbluebuttonbn') ); $importrecordingsettings->add($item); $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_importrecordings_enabled', get_string('config_importrecordings_enabled', 'bigbluebuttonbn'), get_string('config_importrecordings_enabled_description', 'bigbluebuttonbn'), 0 ); $this->add_conditional_element( 'importrecordings_enabled', $item, $importrecordingsettings ); $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_importrecordings_from_deleted_enabled', get_string('config_importrecordings_from_deleted_enabled', 'bigbluebuttonbn'), get_string('config_importrecordings_from_deleted_enabled_description', 'bigbluebuttonbn'), 0 ); $this->add_conditional_element( 'importrecordings_from_deleted_enabled', $item, $importrecordingsettings ); } $this->admin->add($this->parent, $importrecordingsettings); } /** * Helper function renders show recording settings if the feature is enabled. */ protected function add_showrecordings_settings(): void { // Configuration for 'show recordings' feature. $showrecordingsettings = new admin_settingpage( "{$this->sectionnameprefix}_showrecordings", get_string('config_recordings', 'bigbluebuttonbn'), 'moodle/site:config', !((boolean) setting_validator::section_show_recordings_shown()) && ($this->moduleenabled) ); if ($this->admin->fulltree) { $item = new admin_setting_heading( 'bigbluebuttonbn_config_recordings', '', get_string('config_recordings_description', 'bigbluebuttonbn') ); $showrecordingsettings->add($item); $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_recordings_deleted_default', get_string('config_recordings_deleted_default', 'bigbluebuttonbn'), get_string('config_recordings_deleted_default_description', 'bigbluebuttonbn'), 1 ); $this->add_conditional_element( 'recordings_deleted_default', $item, $showrecordingsettings ); $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_recordings_deleted_editable', get_string('config_recordings_deleted_editable', 'bigbluebuttonbn'), get_string('config_recordings_deleted_editable_description', 'bigbluebuttonbn'), 0 ); $this->add_conditional_element( 'recordings_deleted_editable', $item, $showrecordingsettings ); $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_recordings_imported_default', get_string('config_recordings_imported_default', 'bigbluebuttonbn'), get_string('config_recordings_imported_default_description', 'bigbluebuttonbn'), 0 ); $this->add_conditional_element( 'recordings_imported_default', $item, $showrecordingsettings ); $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_recordings_imported_editable', get_string('config_recordings_imported_editable', 'bigbluebuttonbn'), get_string('config_recordings_imported_editable_description', 'bigbluebuttonbn'), 1 ); $this->add_conditional_element( 'recordings_imported_editable', $item, $showrecordingsettings ); $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_recordings_preview_default', get_string('config_recordings_preview_default', 'bigbluebuttonbn'), get_string('config_recordings_preview_default_description', 'bigbluebuttonbn'), 1 ); $this->add_conditional_element( 'recordings_preview_default', $item, $showrecordingsettings ); $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_recordings_preview_editable', get_string('config_recordings_preview_editable', 'bigbluebuttonbn'), get_string('config_recordings_preview_editable_description', 'bigbluebuttonbn'), 0 ); $this->add_conditional_element( 'recordings_preview_editable', $item, $showrecordingsettings ); $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_recordings_asc_sort', get_string('config_recordings_asc_sort', 'bigbluebuttonbn'), get_string('config_recordings_asc_sort_description', 'bigbluebuttonbn'), 0 ); $this->add_conditional_element( 'recordings_asc_sort', $item, $showrecordingsettings ); $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_recording_protect_editable', get_string('config_recording_protect_editable', 'bigbluebuttonbn'), get_string('config_recording_protect_editable_description', 'bigbluebuttonbn'), 1 ); $this->add_conditional_element( 'recording_protect_editable', $item, $showrecordingsettings ); } $this->admin->add($this->parent, $showrecordingsettings); } /** * Helper function renders wait for moderator settings if the feature is enabled. */ protected function add_waitmoderator_settings(): void { // Configuration for wait for moderator feature. $waitmoderatorsettings = new admin_settingpage( "{$this->sectionnameprefix}_waitformoderator", get_string('config_waitformoderator', 'bigbluebuttonbn'), 'moodle/site:config', !((boolean) setting_validator::section_wait_moderator_shown()) && ($this->moduleenabled) ); if ($this->admin->fulltree) { $item = new admin_setting_heading( 'bigbluebuttonbn_config_waitformoderator', '', get_string('config_waitformoderator_description', 'bigbluebuttonbn') ); $waitmoderatorsettings->add($item); $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_waitformoderator_default', get_string('config_waitformoderator_default', 'bigbluebuttonbn'), get_string('config_waitformoderator_default_description', 'bigbluebuttonbn'), 0 ); $this->add_conditional_element( 'waitformoderator_default', $item, $waitmoderatorsettings ); $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_waitformoderator_editable', get_string('config_waitformoderator_editable', 'bigbluebuttonbn'), get_string('config_waitformoderator_editable_description', 'bigbluebuttonbn'), 1 ); $this->add_conditional_element( 'waitformoderator_editable', $item, $waitmoderatorsettings ); $item = new admin_setting_configtext( 'bigbluebuttonbn_waitformoderator_ping_interval', get_string('config_waitformoderator_ping_interval', 'bigbluebuttonbn'), get_string('config_waitformoderator_ping_interval_description', 'bigbluebuttonbn'), 10, PARAM_INT ); $this->add_conditional_element( 'waitformoderator_ping_interval', $item, $waitmoderatorsettings ); $item = new admin_setting_configtext( 'bigbluebuttonbn_waitformoderator_cache_ttl', get_string('config_waitformoderator_cache_ttl', 'bigbluebuttonbn'), get_string('config_waitformoderator_cache_ttl_description', 'bigbluebuttonbn'), 60, PARAM_INT ); $this->add_conditional_element( 'waitformoderator_cache_ttl', $item, $waitmoderatorsettings ); } $this->admin->add($this->parent, $waitmoderatorsettings); } /** * Helper function renders static voice bridge settings if the feature is enabled. */ protected function add_voicebridge_settings(): void { // Configuration for "static voice bridge" feature. $voicebridgesettings = new admin_settingpage( "{$this->sectionnameprefix}_voicebridge", get_string('config_voicebridge', 'bigbluebuttonbn'), 'moodle/site:config', !((boolean) setting_validator::section_static_voice_bridge_shown()) && ($this->moduleenabled) ); if ($this->admin->fulltree) { $item = new admin_setting_heading( 'bigbluebuttonbn_config_voicebridge', '', get_string('config_voicebridge_description', 'bigbluebuttonbn') ); $voicebridgesettings->add($item); $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_voicebridge_editable', get_string('config_voicebridge_editable', 'bigbluebuttonbn'), get_string('config_voicebridge_editable_description', 'bigbluebuttonbn'), 0 ); $this->add_conditional_element( 'voicebridge_editable', $item, $voicebridgesettings ); } $this->admin->add($this->parent, $voicebridgesettings); } /** * Helper function renders preuploaded presentation settings if the feature is enabled. */ protected function add_preupload_settings(): void { // Configuration for "preupload presentation" feature. $preuploadsettings = new admin_settingpage( "{$this->sectionnameprefix}_preupload", get_string('config_preuploadpresentation', 'bigbluebuttonbn'), 'moodle/site:config', !((boolean) setting_validator::section_preupload_presentation_shown()) && ($this->moduleenabled) ); if ($this->admin->fulltree) { // This feature only works if curl is installed (but it is as now required by Moodle). The checks have been removed. $item = new admin_setting_heading( 'bigbluebuttonbn_config_preuploadpresentation', '', get_string('config_preuploadpresentation_description', 'bigbluebuttonbn') ); $preuploadsettings->add($item); $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_preuploadpresentation_editable', get_string('config_preuploadpresentation_editable', 'bigbluebuttonbn'), get_string('config_preuploadpresentation_editable_description', 'bigbluebuttonbn'), 0 ); $this->add_conditional_element( 'preuploadpresentation_editable', $item, $preuploadsettings ); // Note: checks on curl library have been removed as it is a requirement from Moodle. $filemanageroptions = []; $filemanageroptions['accepted_types'] = '*'; $filemanageroptions['maxbytes'] = 0; $filemanageroptions['subdirs'] = 0; $filemanageroptions['maxfiles'] = 1; $filemanageroptions['mainfile'] = true; $filemanager = new admin_setting_configstoredfile( 'mod_bigbluebuttonbn/presentationdefault', get_string('config_presentation_default', 'bigbluebuttonbn'), get_string('config_presentation_default_description', 'bigbluebuttonbn'), 'presentationdefault', 0, $filemanageroptions ); $preuploadsettings->add($filemanager); $item = new admin_setting_configcheckbox( 'mod_bigbluebuttonbn/showpresentation_default', get_string('config_showpresentation_default', 'bigbluebuttonbn'), get_string('config_showpresentation_default_description', 'bigbluebuttonbn'), 1 ); $this->add_conditional_element( 'showpresentation_default', $item, $preuploadsettings ); $item = new admin_setting_configcheckbox( 'mod_bigbluebuttonbn/showpresentation_editable', get_string('config_showpresentation_editable', 'bigbluebuttonbn'), get_string('config_showpresentation_editable_description', 'bigbluebuttonbn'), 0 ); $this->add_conditional_element( 'showpresentation_editable', $item, $preuploadsettings ); } $this->admin->add($this->parent, $preuploadsettings); } /** * Helper function renders userlimit settings if the feature is enabled. */ protected function add_userlimit_settings(): void { $userlimitsettings = new admin_settingpage( "{$this->sectionnameprefix}_userlimit", get_string('config_userlimit', 'bigbluebuttonbn'), 'moodle/site:config', !((boolean) setting_validator::section_user_limit_shown()) && ($this->moduleenabled) ); if ($this->admin->fulltree) { // Configuration for "user limit" feature. $item = new admin_setting_heading( 'bigbluebuttonbn_config_userlimit', '', get_string('config_userlimit_description', 'bigbluebuttonbn') ); $userlimitsettings->add($item); $item = new admin_setting_configtext( 'bigbluebuttonbn_userlimit_default', get_string('config_userlimit_default', 'bigbluebuttonbn'), get_string('config_userlimit_default_description', 'bigbluebuttonbn'), 0, PARAM_INT ); $this->add_conditional_element( 'userlimit_default', $item, $userlimitsettings ); $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_userlimit_editable', get_string('config_userlimit_editable', 'bigbluebuttonbn'), get_string('config_userlimit_editable_description', 'bigbluebuttonbn'), 0 ); $this->add_conditional_element( 'userlimit_editable', $item, $userlimitsettings ); } $this->admin->add($this->parent, $userlimitsettings); } /** * Helper function renders participant settings if the feature is enabled. */ protected function add_participants_settings(): void { // Configuration for defining the default role/user that will be moderator on new activities. $participantsettings = new admin_settingpage( "{$this->sectionnameprefix}_participant", get_string('config_participant', 'bigbluebuttonbn'), 'moodle/site:config', !((boolean) setting_validator::section_moderator_default_shown()) && ($this->moduleenabled) ); if ($this->admin->fulltree) { $item = new admin_setting_heading( 'bigbluebuttonbn_config_participant', '', get_string('config_participant_description', 'bigbluebuttonbn') ); $participantsettings->add($item); // UI for 'participants' feature. $roles = roles::get_roles(null, false); $owner = [ '0' => get_string('mod_form_field_participant_list_type_owner', 'bigbluebuttonbn') ]; $item = new admin_setting_configmultiselect( 'bigbluebuttonbn_participant_moderator_default', get_string('config_participant_moderator_default', 'bigbluebuttonbn'), get_string('config_participant_moderator_default_description', 'bigbluebuttonbn'), array_keys($owner), $owner + $roles ); $this->add_conditional_element( 'participant_moderator_default', $item, $participantsettings ); } $this->admin->add($this->parent, $participantsettings); } /** * Helper function renders general settings if the feature is enabled. */ protected function add_muteonstart_settings(): void { // Configuration for BigBlueButton. $muteonstartsetting = new admin_settingpage( "{$this->sectionnameprefix}_muteonstart", get_string('config_muteonstart', 'bigbluebuttonbn'), 'moodle/site:config', !((boolean) setting_validator::section_muteonstart_shown()) && ($this->moduleenabled) ); if ($this->admin->fulltree) { $item = new admin_setting_heading( 'bigbluebuttonbn_config_muteonstart', '', get_string('config_muteonstart_description', 'bigbluebuttonbn') ); $muteonstartsetting->add($item); $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_muteonstart_default', get_string('config_muteonstart_default', 'bigbluebuttonbn'), get_string('config_muteonstart_default_description', 'bigbluebuttonbn'), 0 ); $this->add_conditional_element( 'muteonstart_default', $item, $muteonstartsetting ); $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_muteonstart_editable', get_string('config_muteonstart_editable', 'bigbluebuttonbn'), get_string('config_muteonstart_editable_description', 'bigbluebuttonbn'), 0 ); $this->add_conditional_element( 'muteonstart_editable', $item, $muteonstartsetting ); } $this->admin->add($this->parent, $muteonstartsetting); } /** * Helper function to render lock settings. */ protected function add_lock_settings(): void { $lockingsetting = new admin_settingpage( "{$this->sectionnameprefix}_locksettings", get_string('config_locksettings', 'bigbluebuttonbn'), 'moodle/site:config', !((boolean) setting_validator::section_lock_shown()) && ($this->moduleenabled) ); // Configuration for various lock settings for meetings. if ($this->admin->fulltree) { $this->add_lock_setting_from_name('disablecam', $lockingsetting); $this->add_lock_setting_from_name('disablemic', $lockingsetting); $this->add_lock_setting_from_name('disableprivatechat', $lockingsetting); $this->add_lock_setting_from_name('disablepublicchat', $lockingsetting); $this->add_lock_setting_from_name('disablenote', $lockingsetting); $this->add_lock_setting_from_name('hideuserlist', $lockingsetting); } $this->admin->add($this->parent, $lockingsetting); } /** * Helper function renders setting if the feature is enabled. * * @param string $settingname * @param admin_settingpage $lockingsetting The parent settingpage to add to */ protected function add_lock_setting_from_name(string $settingname, admin_settingpage $lockingsetting): void { $validatorname = "section_{$settingname}_shown"; if ((boolean) setting_validator::$validatorname()) { // Configuration for BigBlueButton. $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_' . $settingname . '_default', get_string('config_' . $settingname . '_default', 'bigbluebuttonbn'), get_string('config_' . $settingname . '_default_description', 'bigbluebuttonbn'), config::defaultvalue($settingname . '_default') ); $this->add_conditional_element( $settingname . '_default', $item, $lockingsetting ); $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_' . $settingname . '_editable', get_string('config_' . $settingname . '_editable', 'bigbluebuttonbn'), get_string('config_' . $settingname . '_editable_description', 'bigbluebuttonbn'), config::defaultvalue($settingname . '_editable') ); $this->add_conditional_element( $settingname . '_editable', $item, $lockingsetting ); } } /** * Helper function renders extended settings if any of the features there is enabled. */ protected function add_extended_settings(): void { // Configuration for extended capabilities. $extendedcapabilitiessetting = new admin_settingpage( "{$this->sectionnameprefix}_extendedcapabilities", get_string('config_extended_capabilities', 'bigbluebuttonbn'), 'moodle/site:config', !((boolean) setting_validator::section_settings_extended_shown()) && ($this->moduleenabled) ); if ($this->admin->fulltree) { $item = new admin_setting_heading( 'bigbluebuttonbn_config_extended_capabilities', '', get_string('config_extended_capabilities_description', 'bigbluebuttonbn') ); $extendedcapabilitiessetting->add($item); // UI for 'notify users when recording ready' feature. $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_recordingready_enabled', get_string('config_recordingready_enabled', 'bigbluebuttonbn'), get_string('config_recordingready_enabled_description', 'bigbluebuttonbn'), 0 ); $this->add_conditional_element( 'recordingready_enabled', $item, $extendedcapabilitiessetting ); $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_profile_picture_enabled', get_string('config_profile_picture_enabled', 'bigbluebuttonbn'), get_string('config_profile_picture_enabled_description', 'bigbluebuttonbn'), false ); $this->add_conditional_element( 'profile_picture_enabled', $item, $extendedcapabilitiessetting ); } $this->admin->add($this->parent, $extendedcapabilitiessetting); // Configuration for extended BN capabilities should go here. } /** * Helper function renders experimental settings if any of the features there is enabled. */ protected function add_experimental_settings(): void { // Configuration for experimental features should go here. $experimentalfeaturessetting = new admin_settingpage( "{$this->sectionnameprefix}_experimentalfeatures", get_string('config_experimental_features', 'bigbluebuttonbn'), 'moodle/site:config', !((boolean) setting_validator::section_settings_extended_shown()) && ($this->moduleenabled) ); if ($this->admin->fulltree) { $item = new admin_setting_heading( 'bigbluebuttonbn_config_experimental_features', '', get_string('config_experimental_features_description', 'bigbluebuttonbn') ); $experimentalfeaturessetting->add($item); // UI for 'register meeting events' feature. $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_meetingevents_enabled', get_string('config_meetingevents_enabled', 'bigbluebuttonbn'), get_string('config_meetingevents_enabled_description', 'bigbluebuttonbn'), 0 ); $this->add_conditional_element( 'meetingevents_enabled', $item, $experimentalfeaturessetting ); // UI for 'register meeting events' feature. $item = new admin_setting_configcheckbox( 'bigbluebuttonbn_guestaccess_enabled', get_string('config_guestaccess_enabled', 'bigbluebuttonbn'), get_string('config_guestaccess_enabled_description', 'bigbluebuttonbn'), 0 ); $this->add_conditional_element( 'guestaccess_enabled', $item, $experimentalfeaturessetting ); } $this->admin->add($this->parent, $experimentalfeaturessetting); } /** * Process reset cache. */ protected function reset_cache() { // Reset serverinfo cache. cache_helper::purge_by_event('mod_bigbluebuttonbn/serversettingschanged'); } } PK\L||%templates/recordings_session.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 mod_bigbluebuttonbn/recordings_session This template renders the mobile page. Example context (json): { "bbbid" : 3, "has_records": true, "search_input": {}, "import_button": { "method" : "get", "url" : "#", "primary" : 1, "tooltip" : "This is a tooltip", "label" : "This is a the button text", "id": "thisismybutton", "attributes": [{ "name": "data-attribute", "value": "yeah" }] } } }}

PK\3˴ƥ +templates/instance_updated_message.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 mod_bigbluebuttonbn/instance_updated_message.mustache This template renders a message used to inform users that an activity has been created or updated. Example context (json): { "link": "https://example.com/mod/bigbluebuttonbn/view.php?id=12345", "is_create": 1, "is_update": 0, "name": "Daily standup", "description": "A place to discuss daily progress, blockers, and the weather.", "openingtime": 1234556623, "closingtime": 1234559623 } }} {{#is_create}}

{{#str}} notification_instance_created_intro, mod_bigbluebuttonbn, { "name": {{#quote}}{{name}}{{/quote}}, "link": {{#quote}}{{{link}}}{{/quote}} } {{/str}} {{/is_create}} {{#is_update}}

{{#str}} notification_instance_updated_intro, mod_bigbluebuttonbn, { "name": {{#quote}}{{name}}{{/quote}}, "link": {{#quote}}{{link}}{{/quote}} } {{/str}} {{/is_update}} {{#openingtime}} {{/openingtime}} {{#closingtime}} {{/closingtime}}
{{#str}}notification_instance_name, mod_bigbluebuttonbn{{/str}} name
{{#str}}notification_instance_description, mod_bigbluebuttonbn{{/str}} {{{description}}}
{{#str}}notification_instance_start_date, mod_bigbluebuttonbn{{/str}} {{#userdate}} {{openingtime}} , {{#str}}strftimedatetime, langconfig{{/str}} {{/userdate}}
{{#str}}notification_instance_end_date, mod_bigbluebuttonbn{{/str}} {{#userdate}} {{closingtime}} , {{#str}}strftimedatetime, langconfig{{/str}} {{/userdate}}
PK\un,templates/participant_single_select.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 mod_bigbluebuttonbn/participant_single_select Moodle template for a single select without the form and autosubmit @see template core/single_select The original select form produced two embedded form tags and would produce random bugs (https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-owner) Context variables required for this template: * name - Element name. * method - get or post. * action - the action url to submit to. * classes - Element classes. * label - Element label. * disabled - true if this element is disabled. * title - Element title. * formid - optional id value for the form. * id - id for the element. * params - array of params with name and value attributes. * options - Array of options for the select with value, name , slected and optgroup properites. * labelattributes - Label attributes. * helpicon - Help icon. Example context (json): { "name": "lang", "method": "get", "action": "http://localhost/stable_master/mod/scorm/player.php", "classes": "langmenu", "label": "Zombies are coming...", "disabled": false, "title": null, "formid": "randomid", "id": "single_select5833dd4f4b08d108", "params": [ { "name": "scoid", "value": "12" }, { "name": "cm", "value": "15" }, { "name": "mode", "value": "review" }, { "name": "currentorg", "value": "eXeMapADrive4823c6301cf72b22b72" } ], "options": [ { "value": "en", "name": "English ‎(en)‎", "selected": true, "optgroup": false }, { "value": "ar", "name": "Muhaaaa..", "selected": false, "optgroup": false } ], "labelattributes": [], "helpicon": false } }}

{{#params}} {{/params}} {{#label}} {{/label}} {{#helpicon}} {{>core/help_icon}} {{/helpicon}}
PK\Ҧ#templates/participant_form.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 mod_bigbluebuttonbn/participant_form This template renders a row in the participant add form Example context (json): { "instanceTypeProfiles": [{}], "select": {}, "participanttype": "student", "participantvalue": "", "roles": [], "canRemove": true } }}
{{#selectionType}} {{> mod_bigbluebuttonbn/participant_single_select}} {{/selectionType}} {{#selectionParticipant}} {{> mod_bigbluebuttonbn/participant_single_select}} {{/selectionParticipant}} {{#action}} {{/action}}
PK\ߩ templates/import_view.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 mod_bigbluebuttonbn/import_view This template renders the import recordings page. Example context (json): { "bbbid": 3, "has_selected_course": 0, "back_button": { "method" : "get", "url" : "#", "primary" : 1, "tooltip" : "This is a tooltip", "label" : "This is a the button text", "id": "thisismybutton", "attributes": [{ "name": "data-attribute", "value": "yeah" }] } } }}
{{#str}}view_recording_list_course, mod_bigbluebuttonbn{{/str}} {{#course_select}} {{>core/single_select}} {{/course_select}}
{{#bbb_select}} {{#str}}view_recording_activity, mod_bigbluebuttonbn{{/str}} {{>core/single_select}} {{/bbb_select}}
{{#has_selected_course}} {{< mod_bigbluebuttonbn/recordings_table}} {{$title}}{{#str}} view_recording_button_import, mod_bigbluebuttonbn {{/str}}{{/title}} {{$tools}}import{{/tools}} {{$removeimportedid}}{{bbboriginid}}{{/removeimportedid}} {{$otherattributes}} {{! }} id="bigbluebuttonbn-recording-table-{{uniqid}}" {{! }} data-import-mode="1" {{! }} data-bbb-source-course-id="{{sourcecourseid}}" {{! }} data-bbb-source-instance-id="{{bbbsourceid}}" {{! }} {{/otherattributes}} {{/mod_bigbluebuttonbn/recordings_table}} {{/has_selected_course}} {{^has_selected_course}} {{#str}} view_recording_select_course, mod_bigbluebuttonbn {{/str}} {{/has_selected_course}}
{{#back_button}} {{>core/single_button}} {{/back_button}}
PK\H H #templates/recordings_table.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 mod_bigbluebuttonbn/recordings_table This template renders the mobile page. Example context (json): { "bbbid" : 3, "has_records": true, "search_input": {} } }}
{{#has_recordings}}

{{$title}}{{/title}}

{{#recordingwarnings}} {{>core/notification}} {{/recordingwarnings}}
{{#search}} {{>mod_bigbluebuttonbn/search_input_recordings}} {{/search}}
{{/has_recordings}} {{^has_recordings}}
{{#str}}view_message_norecordings, mod_bigbluebuttonbn{{/str}}
{{#recordingwarnings}} {{>core/notification}} {{/recordingwarnings}}
{{/has_recordings}}
{{#has_recordings}} {{#js}} require(['mod_bigbluebuttonbn/recordings'], function(recordings) { recordings.init('#bigbluebuttonbn-recording-table-{{uniqid}}', '#searchform-auto-{{uniqid}}'); }); require(['core/inplace_editable']); {{/js}} {{/has_recordings}} PK\7'(templates/recording_row_preview.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 mod_bigbluebuttonbn/recording_row_preview.mustache This template renders the mobile page. Example context (json): { "id":"1", "recordingid":"1", "tools":[ { "disabled":false, "url":"#", "id":"test-id", "classes":"btn btn-link", "attributes":[ { "name":"title", "value":"Action link's title", "class":"'btn btn-sm btn-default", "data-action":"play" } ], "text":"This is an action link" } ] } }}
{{#recordingpreviews}}
{{#thumbnails}} {{/thumbnails}}
{{#str}}view_recording_preview_help, mod_bigbluebuttonbn{{/str}}
{{/recordingpreviews}}
PK\I/Wtemplates/room_view.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 mod_bigbluebuttonbn/room_view This template renders the mobile page. Example context (json): { "bigbluebuttonbnid": 1, "groupid": 0, "statusclosed": true, "statusclosed": false, "openingtime": 123456789, "closingtime": 987654321 } }}
{{#ismoderator}} {{#guestaccessenabled}} {{> mod_bigbluebuttonbn/guest_links}} {{/guestaccessenabled}} {{> mod_bigbluebuttonbn/end_session_button}} {{/ismoderator}} {{#canjoin}} {{#str}}view_conference_action_join, mod_bigbluebuttonbn{{/str}} {{/canjoin}}
PK\88*templates/recording_row_actionbar.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 mod_bigbluebuttonbn/recording_row_actionbar.mustache This template renders the mobile page. Example context (json): { "id":"1", "recordingid":"1", "tools":[ { "disabled":false, "url":"#", "id":"test-id", "classes":"btn btn-link", "attributes":[ { "name":"title", "value":"Action link's title", "class":"'btn btn-sm btn-default", "data-action":"play" } ], "text":"This is an action link" } ] } }}
{{#tools}}
{{>core/action_link}}
{{/tools}}
PK\4::templates/guest_links.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 mod_bigbluebuttonbn/guest_links This template renders the mobile page. Example context (json): { "guestaccessenabled": 1, "guestjoinurl": "http://moodle.org/", "guestpassword": "password" } }} {{#js}} require(['mod_bigbluebuttonbn/guest_access_modal'], function(GuestLinkAccess) { GuestLinkAccess.init({ id: "{{ bigbluebuttonbnid }}", groupid: "{{ groupid }}" }); }); {{/js}} PK\[գ!templates/recentactivity.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 mod_bigbluebuttonbn/recentactivity This template will render the Bigbluebutton Activity inside a recent activity report. Variables requirer for this template: * userpicture - User fullname and picture * grade - User grade * userdate - Submission date Example context (json): { "userpicture": " ", "eventname": "Joined meeting", "userdate": "Wednesday, 27 January 2021, 9:54 AM", "userurl": "http://localhost/m/stable_master/user/view.php?id=3&course=6", "fullname": "Lukáš Svoboda" } }}
{{{userpicture}}}
{{#modinfo}} {{modicon}}{{modname}} {{/modinfo}}
{{#eventname}}
{{eventname}}
{{/eventname}}
{{fullname}} - {{#userdate}} {{submissiontimestamp}}, {{#str}} strftimedatetime {{/str}} {{/userdate}}
PK\* )templates/element_text_with_copy.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 mod_bigbluebuttonbn/element_text_with_copy This template renders a text input form element with a copy link next to it. Context variables required for this template: * label * required * advanced * helpbutton * error * element * id * name Example context (json): { "label": "Course full name", "required": true, "advanced": false, "error": null, "element": { "wrapperid": "fitem_id_fullname", "id": "id_fullname", "name": "fullname", "iderror": "id_error" } } }} {{< core_form/element-template }} {{$element}} {{/element}} {{/ core_form/element-template }} {{#js}} require(['core/copy_to_clipboard']); {{/js}}PK\}F0)templates/recording_row_playback.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 mod_bigbluebuttonbn/recording_row_playback.mustache This template renders the mobile page. Example context (json): { "id":"1", "recordingid":"1", "tools":[ { "disabled":false, "url":"#", "id":"test-id", "classes":"btn btn-link", "attributes":[ { "name":"title", "value":"Action link's title", "class":"'btn btn-sm btn-default", "data-action":"play" } ], "text":"This is an action link" } ] } }}
{{#playbacks}} {{>core/action_link}} {{/playbacks}}
PK\|templates/view_page.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 mod_bigbluebuttonbn/view_page This template renders the /view.php page. Example context (json): { "instanceid": 1234, "meetingname": "My first meeting", "meetingdescription": "This is my first meeting!", "serverwarning": true, "sitenotification": { "message": "Unlock premium feature", "actions": [ { "url": "https://example.com/", "name": "action", "value": "buy", "title": "Upgrade now" } ] }, "pollinterval": 5 } }} {{{groupselector}}} {{#serverwarning}} {{>core/notification}} {{/serverwarning}} {{#sitenotification}} {{>core/local/notification/cta}} {{/sitenotification}}

{{meetingname}}

{{{meetingdescription}}}
{{#room}} {{>mod_bigbluebuttonbn/room_view}} {{/room}} {{#recordings}} {{>mod_bigbluebuttonbn/recordings_session}} {{/recordings}} {{#js}} require(['mod_bigbluebuttonbn/rooms'], function(rooms) { // Register action on all buttons. rooms.init( {{instanceid}}, {{pollinterval}} ); }); {{/js}} PK\%<'templates/participant_form_add.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 mod_bigbluebuttonbn/participant_form_add This template renders a row in the participant add form Example context (json): { "selectiontypevalue": "example", "selectionvalue": "example", "participanttype": "student", "participantvalue": "", "roles": [], "canRemove": true } }} {{participanttype}} {{participantvalue}} {{#str}}mod_form_field_participant_list_text_as, mod_bigbluebuttonbn{{/str}} {{#canRemove}} {{#pix}}t/delete, core, {{#str}}mod_form_field_participant_list_action_remove, mod_bigbluebuttonbn{{/str}}{{/pix}} {{/canRemove}} PK\N++%templates/end_session_button.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 mod_bigbluebuttonbn/end_session_button This template renders the End session button. Example context (json): { "statusrunning": true, "bigbluebuttonbnid": 12345, "groupid": 98765 } }} {{#statusrunning}} {{#str}}view_conference_action_end, mod_bigbluebuttonbn{{/str}} {{/statusrunning}} PK\s  *templates/search_input_recordings.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 mod_bigbluebuttonbn/search_input_recordings Search input that auto searches. Inspired from the core/search_input_auto but does not use a unique id for the search input. Add a submit button and clear button. Example context (json): { "action": "https://moodle.local/admin/search.php", "extraclasses": "my-2", "inputname": "search", "searchstring": "Search settings", "sesskey": "sesskey", "value": "policy", "btnclass": "primary", "hiddenfields": [ { "name": "course", "value": "11" } ] } }}
PK\ZU= upgrade.txtnu[=== 4.5 Onwards === This file has been replaced by UPGRADING.md. See MDL-81125 for further information. === This files describes API changes in the bigbluebuttonbn code. === 4.3 === * Make instance class constructor private and use the factory method to create instances. === 4.2 === * Simplify bigbluebutton_proxy get_join_url so to pass only parameters that cannot be deduced from the instance. * Add a new parameter for mod_bigbluebuttonbn\recording::sync_pending_recordings_from_server so sync only 'dismissed' recordings (This was fixed in 4.2, 4.1.3 and 4.0.8). === 4.1 === * External function mod_bigbluebuttonbn\external\meeting_info now return the list of the instance features and whether they are enabled or not. * External function mod_bigbluebuttonbn\external\meeting_info may now return guestaccessenabled, guestjoinurl, and guestpassword if the callee can have access to them. PK\;| bbb_broker.phpnu[. /** * Intermediator for handling requests from the BigBlueButton server. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) * @author Darko Miletic (darko.miletic [at] gmail [dt] com) */ // We should not have any require login or MOODLE_INTERNAL Check in this file. // phpcs:disable moodle.Files.MoodleInternal.MoodleInternalGlobalState,moodle.Files.RequireLogin.Missing require(__DIR__ . '/../../config.php'); use Firebase\JWT\Key; use mod_bigbluebuttonbn\broker; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\local\config; use mod_bigbluebuttonbn\meeting; global $PAGE, $USER, $CFG, $SESSION, $DB; $params = $_REQUEST; $error = broker::validate_parameters($params); if (!empty($error)) { header('HTTP/1.0 400 Bad Request. ' . $error); return; } $action = $params['action']; $instance = instance::get_from_instanceid($params['bigbluebuttonbn']); if (empty($instance)) { header('HTTP/1.0 410 Gone. The activity may have been deleted'); return; } $PAGE->set_context($instance->get_context()); try { switch (strtolower($action)) { case 'recording_ready': broker::process_recording_ready($instance, $params); return; case 'meeting_events': // When meeting_events callback is implemented by BigBlueButton, Moodle receives a POST request // which is processed in the function using super globals. broker::process_meeting_events($instance); return; } header("HTTP/1.0 400 Bad request. The action '{$action}' does not exist"); } catch (Exception $e) { header('HTTP/1.0 500 Internal Server Error. ' . $e->getMessage()); } PK\gRR index.phpnu[. /** * View all BigBlueButton instances in this course. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) */ use core\notification; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\output\index; use mod_bigbluebuttonbn\plugin; require(__DIR__.'/../../config.php'); global $PAGE, $OUTPUT; $id = required_param('id', PARAM_INT); $course = get_course($id); require_login($course, true); $PAGE->set_url('/mod/bigbluebuttonbn/index.php', ['id' => $id]); $PAGE->set_title(get_string('modulename', plugin::COMPONENT)); $PAGE->set_heading($course->fullname); $PAGE->set_cacheable(false); $PAGE->set_pagelayout('incourse'); $PAGE->navbar->add($PAGE->title, $PAGE->url); $instances = instance::get_all_instances_in_course($course->id); if (empty($instances)) { notification::add( get_string('index_error_noinstances', plugin::COMPONENT), notification::ERROR ); redirect(new moodle_url('/course/view.php', ['id' => $course->id])); } echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('index_heading', plugin::COMPONENT)); $renderer = $PAGE->get_renderer(plugin::COMPONENT); echo $renderer->render(new index($course, $instances)); echo $OUTPUT->footer(); PK\C]k7k7amd/src/recordings.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 . /** * JS for the recordings page on mod_bigbluebuttonbn plugin. * * @module mod_bigbluebuttonbn/recordings * @copyright 2021 Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import * as repository from './repository'; import {exception as displayException, saveCancelPromise} from 'core/notification'; import {prefetchStrings} from 'core/prefetch'; import {getString, getStrings} from 'core/str'; import {addIconToContainerWithPromise} from 'core/loadingicon'; import Pending from 'core/pending'; const stringsWithKeys = { first: 'view_recording_yui_first', prev: 'view_recording_yui_prev', next: 'view_recording_yui_next', last: 'view_recording_yui_last', goToLabel: 'view_recording_yui_page', goToAction: 'view_recording_yui_go', perPage: 'view_recording_yui_rows', showAll: 'view_recording_yui_show_all', }; // Load global strings. prefetchStrings('bigbluebuttonbn', Object.entries(stringsWithKeys).map((entry) => entry[1])); const getStringsForYui = () => { const stringMap = Object.keys(stringsWithKeys).map(key => { return { key: stringsWithKeys[key], component: 'mod_bigbluebuttonbn', }; }); // Return an object with the matching string keys (we want an object with {: ...}). return getStrings(stringMap) .then((stringArray) => Object.assign( {}, ...Object.keys(stringsWithKeys).map( (key, index) => ({[key]: stringArray[index]}) ) )); }; const getYuiInstance = lang => new Promise(resolve => { // eslint-disable-next-line YUI({ lang, }).use('intl', 'datatable', 'datatable-sort', 'datatable-paginator', 'datatype-number', Y => { resolve(Y); }); }); /** * Format the supplied date per the specified locale. * * @param {string} locale * @param {number} date * @returns {array} */ const formatDate = (locale, date) => { const realDate = new Date(date); return realDate.toLocaleDateString(locale, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', }); }; /** * Format response data for the table. * * @param {string} response JSON-encoded table data * @returns {array} */ const getFormattedData = response => { const recordingData = response.tabledata; return JSON.parse(recordingData.data); }; const getTableNode = tableSelector => document.querySelector(tableSelector); const fetchRecordingData = tableSelector => { const tableNode = getTableNode(tableSelector); if (tableNode === null) { return Promise.resolve(false); } if (tableNode.dataset.importMode) { return repository.fetchRecordingsToImport( tableNode.dataset.bbbid, tableNode.dataset.bbbSourceInstanceId, tableNode.dataset.bbbSourceCourseId, tableNode.dataset.tools, tableNode.dataset.groupId ); } else { return repository.fetchRecordings( tableNode.dataset.bbbid, tableNode.dataset.tools, tableNode.dataset.groupId ); } }; /** * Fetch the data table functinos for the specified table. * * @param {String} tableId in which we will display the table * @param {String} searchFormId The Id of the relate. * @param {Object} dataTable * @returns {Object} * @private */ const getDataTableFunctions = (tableId, searchFormId, dataTable) => { const tableNode = getTableNode(tableId); const bbbid = tableNode.dataset.bbbid; const updateTableFromResponse = response => { if (!response || !response.status) { // There was no output at all. return; } dataTable.get('data').reset(getFormattedData(response)); dataTable.set( 'currentData', dataTable.get('data') ); const currentFilter = dataTable.get('currentFilter'); if (currentFilter) { filterByText(currentFilter); } }; const refreshTableData = () => fetchRecordingData(tableId).then(updateTableFromResponse); const filterByText = value => { const dataModel = dataTable.get('currentData'); dataTable.set('currentFilter', value); const escapedRegex = value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); const rsearch = new RegExp(`.*?${escapedRegex}.*?`, 'i'); dataTable.set('data', dataModel.filter({asList: true}, item => { const name = item.get('recording'); if (name && rsearch.test(name)) { return true; } const description = item.get('description'); return description && rsearch.test(description); })); }; const requestAction = async(element) => { const getDataFromAction = (element, dataType) => { const dataElement = element.closest(`[data-${dataType}]`); if (dataElement) { return dataElement.dataset[dataType]; } return null; }; const elementData = element.dataset; const payload = { bigbluebuttonbnid: bbbid, recordingid: getDataFromAction(element, 'recordingid'), additionaloptions: getDataFromAction(element, 'additionaloptions'), action: elementData.action, }; // Slight change for import, for additional options. if (!payload.additionaloptions) { payload.additionaloptions = {}; } if (elementData.action === 'import') { const bbbsourceid = getDataFromAction(element, 'source-instance-id'); const bbbcourseid = getDataFromAction(element, 'source-course-id'); if (!payload.additionaloptions) { payload.additionaloptions = {}; } payload.additionaloptions.sourceid = bbbsourceid ? bbbsourceid : 0; payload.additionaloptions.bbbcourseid = bbbcourseid ? bbbcourseid : 0; } // Now additional options should be a json string. payload.additionaloptions = JSON.stringify(payload.additionaloptions); if (element.dataset.requireConfirmation === "1") { // Create the confirmation dialogue. try { await saveCancelPromise( getString('confirm'), recordingConfirmationMessage(payload), getString('ok', 'moodle'), ); } catch { // User cancelled the dialogue. return; } } return repository.updateRecording(payload); }; const recordingConfirmationMessage = async(data) => { const playbackElement = document.querySelector(`#playbacks-${data.recordingid}`); const recordingType = await getString( playbackElement.dataset.imported === 'true' ? 'view_recording_link' : 'view_recording', 'bigbluebuttonbn' ); const confirmation = await getString(`view_recording_${data.action}_confirmation`, 'bigbluebuttonbn', recordingType); if (data.action === 'import') { return confirmation; } // If it has associated links imported in a different course/activity, show that in confirmation dialog. const associatedLinkCount = document.querySelector(`a#recording-${data.action}-${data.recordingid}`)?.dataset?.links; if (!associatedLinkCount || associatedLinkCount === 0) { return confirmation; } const confirmationWarning = await getString( associatedLinkCount === 1 ? `view_recording_${data.action}_confirmation_warning_p` : `view_recording_${data.action}_confirmation_warning_s`, 'bigbluebuttonbn', associatedLinkCount ); return confirmationWarning + '\n\n' + confirmation; }; /** * Process an action event. * * @param {Event} e */ const processAction = e => { const popoutLink = e.target.closest('[data-action="play"]'); if (popoutLink) { e.preventDefault(); const videoPlayer = window.open('', '_blank'); videoPlayer.opener = null; videoPlayer.location.href = popoutLink.href; // TODO send a recording viewed event when this event will be implemented. return; } // Fetch any clicked anchor. const clickedLink = e.target.closest('a[data-action]'); if (clickedLink && !clickedLink.classList.contains('disabled')) { e.preventDefault(); // Create a spinning icon on the table. const iconPromise = addIconToContainerWithPromise(dataTable.get('boundingBox').getDOMNode()); requestAction(clickedLink) .then(refreshTableData) .then(iconPromise.resolve) .catch(displayException); } }; const processSearchSubmission = e => { // Prevent the default action. e.preventDefault(); const parentNode = e.target.closest('div[role=search]'); const searchInput = parentNode.querySelector('input[name=search]'); filterByText(searchInput.value); }; const registerEventListeners = () => { // Add event listeners to the table boundingBox. const boundingBox = dataTable.get('boundingBox').getDOMNode(); boundingBox.addEventListener('click', processAction); // Setup the search from handlers. const searchForm = document.querySelector(searchFormId); if (searchForm) { const searchButton = document.querySelector(searchFormId + ' button'); searchButton.addEventListener('click', processSearchSubmission); } }; return { filterByText, refreshTableData, registerEventListeners, }; }; /** * Setup the data table for the specified BBB instance. * * @param {String} tableId in which we will display the table * @param {String} searchFormId The Id of the relate. * @param {object} response The response from the data request * @returns {Promise} */ const setupDatatable = (tableId, searchFormId, response) => { if (!response) { return Promise.resolve(); } if (!response.status) { // Something failed. Continue to show the plain output. return Promise.resolve(); } const recordingData = response.tabledata; const pendingPromise = new Pending('mod_bigbluebuttonbn/recordings/setupDatatable'); return Promise.all([getYuiInstance(recordingData.locale), getStringsForYui()]) .then(([yuiInstance, strings]) => { // Here we use a custom formatter for date. // See https://clarle.github.io/yui3/yui/docs/api/classes/DataTable.BodyView.Formatters.html // Inspired from examples here: https://clarle.github.io/yui3/yui/docs/datatable/ // Normally formatter have the prototype: (col) => (cell) => , see: // https://clarle.github.io/yui3/yui/docs/api/files/datatable_js_formatters.js.html#l100 . const dateCustomFormatter = () => (cell) => formatDate(recordingData.locale, cell.value); // Add the fetched strings to the YUI Instance. yuiInstance.Intl.add('datatable-paginator', yuiInstance.config.lang, {...strings}); yuiInstance.DataTable.BodyView.Formatters.customDate = dateCustomFormatter; return yuiInstance; }) .then(yuiInstance => { const tableData = getFormattedData(response); yuiInstance.RecordsPaginatorView = Y.Base.create('my-paginator-view', yuiInstance.DataTable.Paginator.View, [], { _modelChange: function(e) { var changed = e.changed, totalItems = (changed && changed.totalItems); if (totalItems) { this._updateControlsUI(e.target.get('page')); } } }); return new yuiInstance.DataTable({ paginatorView: "RecordsPaginatorView", width: "1195px", columns: recordingData.columns, data: tableData, rowsPerPage: 10, paginatorLocation: ['header', 'footer'], autoSync: true }); }) .then(dataTable => { dataTable.render(tableId); const {registerEventListeners} = getDataTableFunctions( tableId, searchFormId, dataTable); registerEventListeners(); return dataTable; }) .then(dataTable => { pendingPromise.resolve(); return dataTable; }); }; /** * Initialise recordings code. * * @method init * @param {String} tableId in which we will display the table * @param {String} searchFormId The Id of the relate. */ export const init = (tableId, searchFormId) => { const pendingPromise = new Pending('mod_bigbluebuttonbn/recordings:init'); fetchRecordingData(tableId) .then(response => setupDatatable(tableId, searchFormId, response)) .then(() => pendingPromise.resolve()) .catch(displayException); }; PK\```amd/src/events.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 . /** * Events for the mod_bigbluebuttonbn plugin. * * @module mod_bigbluebuttonbn/events * @copyright 2021 Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import {dispatchEvent} from 'core/event_dispatcher'; export const eventTypes = { /** * Fired when a session has been ended. * * @event mod_bigbluebuttonbn/sessionEnded * @type CustomEvent * @property {object} detail * @property {number} detail.bbbId * @property {number} detail.groupId */ sessionEnded: 'mod_bigbluebuttonbn/sessionEnded', /** * Fired when the current session has been ended. * * @event mod_bigbluebuttonbn/currentSessionEnded * @type CustomEvent * @property {object} detail */ currentSessionEnded: 'mod_bigbluebuttonbn/currentSessionEnded', }; /** * Trigger the sessionEnded event. * * @param {number} bbbId * @param {number} groupId * @returns {CustomEvent} * @fires event:mod_bigbluebuttonbn/sessionEnded */ export const notifySessionEnded = (bbbId, groupId) => dispatchEvent(eventTypes.sessionEnded, { bbbId, groupId, }); /** * Trigger the currentSessionEnded event. * * @param {Element} container * @returns {CustomEvent} * @fires event:mod_bigbluebuttonbn/currentSessionEnded */ export const notifyCurrentSessionEnded = container => dispatchEvent( eventTypes.currentSessionEnded, {}, container ); PK\+=KKamd/src/index.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 . /** * Index page for the mod_bigbluebuttonbn plugin. * * @module mod_bigbluebuttonbn/index * @copyright 2021 Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import './actions'; import {eventTypes} from './events'; export const init = () => { document.addEventListener(eventTypes.sessionEnded, () => { window.location.reload(); }); }; PK\m  amd/src/rooms.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 . /** * JS actions for the rooms page for mod_bigbluebuttonbn. * * @module mod_bigbluebuttonbn/rooms * @copyright 2021 Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import './actions'; import * as repository from './repository'; import * as roomUpdater from './roomupdater'; import { exception as displayException, fetchNotifications, } from 'core/notification'; import Pending from 'core/pending'; import {getString} from 'core/str'; import {add as addToast} from 'core/toast'; import {eventTypes, notifyCurrentSessionEnded} from './events'; /** * Init the room * * @param {Number} bigbluebuttonbnid bigblubeutton identifier * @param {Number} pollInterval poll interval in miliseconds */ export const init = (bigbluebuttonbnid, pollInterval) => { const completionElement = document.querySelector('a[href*=completion_validate]'); if (completionElement) { completionElement.addEventListener("click", event => { event.preventDefault(); const pendingPromise = new Pending('mod_bigbluebuttonbn/completion:validate'); repository.completionValidate(bigbluebuttonbnid) .then(() => getString('completionvalidatestatetriggered', 'mod_bigbluebuttonbn')) .then(str => addToast(str)) .then(() => pendingPromise.resolve()) .catch(displayException); }); } document.addEventListener('click', e => { const joinButton = e.target.closest('[data-action="join"]'); if (joinButton) { window.open(joinButton.href, 'bigbluebutton_conference'); e.preventDefault(); // Gives the user a bit of time to go into the meeting before polling the room. setTimeout(() => { roomUpdater.updateRoom(true); }, pollInterval); } }); document.addEventListener(eventTypes.sessionEnded, () => { roomUpdater.stop(); roomUpdater.updateRoom(); fetchNotifications(); }); window.addEventListener(eventTypes.currentSessionEnded, () => { roomUpdater.stop(); roomUpdater.updateRoom(); fetchNotifications(); }); // Room update. roomUpdater.start(pollInterval); }; /** * Auto close child windows when clicking the End meeting button. * @param {Number} closeDelay time to wait in miliseconds before closing the window */ export const setupWindowAutoClose = (closeDelay = 2000) => { notifyCurrentSessionEnded(window.opener); window.addEventListener('onbeforeunload', () => { window.opener.setTimeout(() => { roomUpdater.updateRoom(true); }, closeDelay); }, { once: true }); window.close(); // This does not work as scripts can only close windows that are opened by themselves. }; PK\e^@u u amd/src/roomupdater.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 . /** * JS room updater. * * @module mod_bigbluebuttonbn/roomupdater * @copyright 2021 Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import Pending from 'core/pending'; import Templates from "core/templates"; import {exception as displayException} from 'core/notification'; import {getMeetingInfo} from './repository'; let timerReference = null; let timerRunning = false; let pollInterval = 0; let pollIntervalFactor = 1; const MAX_POLL_INTERVAL_FACTOR = 10; const resetValues = () => { timerRunning = false; timerReference = null; pollInterval = 0; pollIntervalFactor = 1; }; /** * Start the information poller. * @param {Number} interval interval in miliseconds between each poll action. */ export const start = (interval) => { resetValues(); timerRunning = true; pollInterval = interval; poll(); }; /** * Stop the room updater. */ export const stop = () => { if (timerReference) { clearTimeout(timerReference); } resetValues(); }; /** * Start the information poller. */ const poll = () => { if (!timerRunning || !pollInterval) { // The poller has been stopped. return; } updateRoom() .then((updateOk) => { if (!updateOk) { pollIntervalFactor = (pollIntervalFactor < MAX_POLL_INTERVAL_FACTOR) ? pollIntervalFactor + 1 : MAX_POLL_INTERVAL_FACTOR; // We make sure if there is an error that we do not try too often. } timerReference = setTimeout(() => poll(), pollInterval * pollIntervalFactor); return true; }) .catch(); }; /** * Update the room information. * * @param {boolean} [updatecache=false] should we update cache * @returns {Promise} */ export const updateRoom = (updatecache = false) => { const bbbRoomViewElement = document.getElementById('bigbluebuttonbn-room-view'); if (bbbRoomViewElement === null) { return Promise.resolve(false); } const bbbId = bbbRoomViewElement.dataset.bbbId; const groupId = bbbRoomViewElement.dataset.groupId; const pendingPromise = new Pending('mod_bigbluebuttonbn/roomupdater:updateRoom'); return getMeetingInfo(bbbId, groupId, updatecache) .then(data => { // Just make sure we have the right information for the template. data.haspresentations = !!(data.presentations && data.presentations.length); return Templates.renderForPromise('mod_bigbluebuttonbn/room_view', data); }) .then(({html, js}) => Templates.replaceNode(bbbRoomViewElement, html, js)) .then(() => pendingPromise.resolve()) .catch(displayException); }; PK\yZooamd/src/actions.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 . /** * JS actions. * * @module mod_bigbluebuttonbn/actions * @copyright 2021 Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import {endMeeting as requestEndMeeting} from './repository'; import { exception as displayException, saveCancel, } from 'core/notification'; import {notifySessionEnded} from './events'; import {getString} from 'core/str'; const confirmedPromise = (title, question, saveLabel) => new Promise(resolve => { saveCancel(title, question, saveLabel, resolve); }); const registerEventListeners = () => { document.addEventListener('click', e => { const actionButton = e.target.closest('.bbb-btn-action[data-action="end"]'); if (!actionButton) { return; } e.preventDefault(); const bbbId = actionButton.dataset.bbbId; const groupId = actionButton.dataset.groupId ? actionButton.dataset.groupId : 0; confirmedPromise( getString('end_session_confirm_title', 'mod_bigbluebuttonbn'), getString('end_session_confirm', 'mod_bigbluebuttonbn'), getString('yes', 'moodle') ) .then(() => requestEndMeeting(bbbId, groupId)) .then(() => { notifySessionEnded(bbbId, groupId); return; }) .catch(displayException); }); }; let listening = false; if (!listening) { registerEventListeners(); listening = true; } PK\sGx7x7amd/src/modform.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 . /** * JS for the mod_form page on mod_bigbluebuttonbn plugin. * * @module mod_bigbluebuttonbn/modform * @copyright 2021 Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import {getString} from 'core/str'; import Notification from 'core/notification'; import Templates from "core/templates"; /** * Get all selectors in one place. * */ const ELEMENT_SELECTOR = { instanceTypeSelection: () => document.querySelector('select#id_type'), instanceTypeProfiles: () => document.querySelector('[data-profile-types]'), participantData: () => document.querySelector('[data-participant-data]'), participantList: () => document.getElementsByName('participants')[0], participantTable: () => document.getElementById('participant_list_table'), participantSelectionType: () => document.getElementsByName('bigbluebuttonbn_participant_selection_type')[0], participantSelection: () => document.getElementsByName('bigbluebuttonbn_participant_selection')[0], participantAddButton: () => document.getElementsByName('bigbluebuttonbn_participant_selection_add')[0], }; /** * Initialise the moodle form code. * * This will help hide or show items depending on the selection of the instance type. * * @method init * @param {object} info */ export const init = (info) => { const selectedType = ELEMENT_SELECTOR.instanceTypeSelection(); const instanceTypeProfiles = JSON.parse(ELEMENT_SELECTOR.instanceTypeProfiles().dataset.profileTypes); let profileType = info.instanceTypeDefault; if (selectedType !== null && selectedType.selectedIndex !== -1) { profileType = selectedType.options[selectedType.selectedIndex].value; } const isFeatureEnabled = (profileType, feature) => { const features = instanceTypeProfiles[profileType].features; return (features.indexOf(feature) !== -1); }; applyInstanceTypeProfile(profileType, isFeatureEnabled); // Change form visible fields depending on the selection. selectedType.addEventListener('change', (e) => { applyInstanceTypeProfile(e.target.value, isFeatureEnabled); }); ELEMENT_SELECTOR.participantSelectionType().addEventListener('change', (e) => { const currentTypeSelect = e.target; updateSelectionFromType(currentTypeSelect); }); ELEMENT_SELECTOR.participantAddButton().addEventListener('click', (e) => { e.stopPropagation(); e.preventDefault(); participantAddFromCurrentSelection(); }); participantListInit(); }; /** * Show or hide form element depending on the selected profile * * @param {string} profileType * @param {function} isFeatureEnabled */ const applyInstanceTypeProfile = (profileType, isFeatureEnabled) => { let showAll = isFeatureEnabled(profileType, 'all'); const showFieldset = (id, show) => { // Show room settings validation. const node = document.querySelector('#' + id); if (!node) { return; } if (show) { node.style.display = 'block'; return; } node.style.display = 'none'; }; const showInput = (id, show) => { // Show room settings validation. const node = document.querySelector('#' + id); if (!node) { return; } var ancestor = node.closest('div').closest('div'); if (show) { ancestor.style.display = 'block'; return; } ancestor.style.display = 'none'; }; const showFormGroup = (id, show) => { // Show room settings validation. const node = document.querySelector('#fgroup_id_' + id); if (!node) { return; } if (show) { node.classList.remove('hidden'); return; } node.classList.add('hidden'); }; // Show room settings validation. showFieldset('id_room', showAll || isFeatureEnabled(profileType, 'showroom')); showInput('id_record', showAll || isFeatureEnabled(profileType, 'showroom')); // Show recordings settings validation. showFieldset('id_recordings', showAll || isFeatureEnabled(profileType, 'showrecordings')); // Show recordings imported settings validation. showInput('id_recordings_imported', showAll || isFeatureEnabled(profileType, 'showrecordings')); // Show lock settings validation. showFieldset('id_lock', showAll || isFeatureEnabled(profileType, 'lock')); // Show guest settings validation. showFieldset('id_guestaccess', showAll || isFeatureEnabled(profileType, 'showroom')); // Preuploadpresentation feature validation. showFieldset('id_preuploadpresentation', showAll || isFeatureEnabled(profileType, 'preuploadpresentation')); // Participants feature validation. showFieldset('id_permissions', showAll || isFeatureEnabled(profileType, 'permissions')); // Schedule feature validation. showFieldset('id_schedule', showAll || isFeatureEnabled(profileType, 'schedule')); // Common module settings validation. showFieldset('id_modstandardelshdr', showAll || isFeatureEnabled(profileType, 'modstandardelshdr')); // Restrict access validation. showFieldset('id_availabilityconditionsheader', showAll || isFeatureEnabled(profileType, 'availabilityconditionsheader')); // Tags validation. showFieldset('id_tagshdr', showAll || isFeatureEnabled(profileType, 'tagshdr')); // Competencies validation. showFieldset('id_competenciessection', showAll || isFeatureEnabled(profileType, 'competenciessection')); // Standards grading feature validation. showFieldset('id_modstandardgrade', showAll || isFeatureEnabled(profileType, 'modstandardgrade')); // Completion validation. showFieldset('id_activitycompletionheader', showAll || isFeatureEnabled(profileType, 'activitycompletionheader')); // Completion validation. showFormGroup('completionattendancegroup', showAll || isFeatureEnabled(profileType, 'completionattendance')); // Completion validation. showFormGroup('completionengagementgroup', showAll || isFeatureEnabled(profileType, 'completionengagement')); }; /** * Init the participant list */ const participantListInit = () => { const participantData = JSON.parse(ELEMENT_SELECTOR.participantData().dataset.participantData); const participantList = getParticipantList(); participantList.forEach(participant => { const selectionTypeValue = participant.selectiontype; const selectionValue = participant.selectionid; const selectionRole = participant.role; if (participant.selectiontype === 'all' || typeof participantData[participant.selectiontype].children[participant.selectionid] !== 'undefined') { // Add it to the form, but don't add the delete button if it is the first item. participantAddToForm(selectionTypeValue, selectionValue, selectionRole, true).then(); } }); }; /** * Add rows to the participant list depending on the current selection. * * @param {string} selectionTypeValue * @param {string} selectionValue * @param {string} selectedRole * @param {boolean} canRemove * @returns {Promise} */ const participantAddToForm = async(selectionTypeValue, selectionValue, selectedRole, canRemove) => { const participantData = JSON.parse(ELEMENT_SELECTOR.participantData().dataset.participantData); const sviewer = await getString('mod_form_field_participant_bbb_role_viewer', 'mod_bigbluebuttonbn'); const smoderator = await getString('mod_form_field_participant_bbb_role_moderator', 'mod_bigbluebuttonbn'); let roles = { viewer: {'id': 'viewer', label: sviewer}, moderator: {'id': 'moderator', label: smoderator} }; roles[selectedRole].isselected = true; try { const listTable = document.querySelector('#participant_list_table tbody'); const templateContext = { 'selectiontypevalue': selectionTypeValue, 'selectionvalue': selectionValue, 'participanttype': participantData[selectionTypeValue].name, 'participantvalue': (selectionTypeValue !== 'all') ? participantData[selectionTypeValue].children[selectionValue].name : null, 'roles': Object.values(roles), 'canRemove': canRemove }; const {html, js} = await Templates.renderForPromise('mod_bigbluebuttonbn/participant_form_add', templateContext); const newNode = Templates.appendNodeContents(listTable, html, js)[0]; newNode.querySelector('.participant-select').addEventListener('change', () => { participantListRoleUpdate(selectionTypeValue, selectionValue); }); // Now add the callbacks: participantListRoleUpdate() and participantRemove(). const removeNode = newNode.querySelector('.remove-button'); if (removeNode) { removeNode .addEventListener('click', () => { participantRemove(selectionTypeValue, selectionValue); }); } } catch (e) { Notification.exception(e); } }; /* */ /** * Update the related form element with the list value. * * @param {object} list */ const participantListUpdate = (list) => { const participantList = ELEMENT_SELECTOR.participantList(); participantList.value = JSON.stringify(list); }; /** * * @returns {any} */ const getParticipantList = () => { const participantListValue = ELEMENT_SELECTOR.participantList().value; if (participantListValue) { return JSON.parse(participantListValue); } return []; }; /** * Remove participant both in the table/form and in the form element. * * @param {string} selectionTypeValue * @param {string} selectionValue */ const participantRemove = (selectionTypeValue, selectionValue) => { const pList = getParticipantList(); const id = 'participant_list_tr_' + selectionTypeValue + '-' + selectionValue; const participantListTable = ELEMENT_SELECTOR.participantTable(); const selectionid = (selectionValue === '' ? null : selectionValue); for (let i = 0; i < pList.length; i++) { if (pList[i].selectiontype === selectionTypeValue && pList[i].selectionid === selectionid) { pList.splice(i, 1); } } // Remove from the form. for (let i = 0; i < participantListTable.rows.length; i++) { if (participantListTable.rows[i].id === id) { participantListTable.deleteRow(i); } } // Update value in the form. participantListUpdate(pList); }; /** * Role update * * @param {string} type * @param {string} id */ const participantListRoleUpdate = (type, id) => { // Update in memory. const participantListRoleSelection = document.querySelector(`#participant_list_tr_${type}-${id} .participant-select`); const pList = getParticipantList(); for (var i = 0; i < pList.length; i++) { if (pList[i].selectiontype === type && pList[i].selectionid === id) { pList[i].role = participantListRoleSelection.value; } } // Update in the form. participantListUpdate(pList); }; /** * Add participant from the currently selected options */ const participantAddFromCurrentSelection = () => { let selectionType = ELEMENT_SELECTOR.participantSelectionType(); let selection = ELEMENT_SELECTOR.participantSelection(); const pList = getParticipantList(); // Lookup to see if it has been added already. for (var i = 0; i < pList.length; i++) { if (pList[i].selectiontype === selectionType.value && pList[i].selectionid === selection.value) { return; } } pList.push({ "selectiontype": selectionType.value, "selectionid": selection.value, "role": "viewer" }); // Add it to the form. participantAddToForm(selectionType.value, selection.value, 'viewer', true).then(); // Update in the form. participantListUpdate(pList); }; /** * Update selectable options when changing types * * @param {HTMLNode} currentTypeSelect */ const updateSelectionFromType = (currentTypeSelect) => { const createNewOption = (selectItem, label, value) => { const option = document.createElement('option'); option.text = label; option.value = value; selectItem.add(option); }; const participantData = JSON.parse(ELEMENT_SELECTOR.participantData().dataset.participantData); // Clear all selection items. const participantSelect = ELEMENT_SELECTOR.participantSelection(); while (participantSelect.firstChild) { participantSelect.removeChild(participantSelect.firstChild); } // Add options depending on the selection. if (currentTypeSelect.selectedIndex !== -1) { const options = Object.values(participantData[currentTypeSelect.value].children); options.forEach(option => { createNewOption(participantSelect, option.name, option.id); }); if (currentTypeSelect.value === 'all') { createNewOption(participantSelect, '---------------', 'all'); participantSelect.disabled = true; } else { participantSelect.disabled = false; } } }; PK\DSSamd/src/repository.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 . /** * Repository to perform WS calls for mod_bigbluebuttonbn. * * @module mod_bigbluebuttonbn/repository * @copyright 2021 Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import {call as fetchMany} from 'core/ajax'; /** * Fetch the list of recordings from the server. * * @param {Number} bigbluebuttonbnid The instance ID * @param {String} tools the set of tools to display * @param {number} groupid * @returns {Promise} */ export const fetchRecordings = (bigbluebuttonbnid, tools, groupid) => { const args = { bigbluebuttonbnid, tools, }; if (groupid) { args.groupid = groupid; } return fetchMany([{methodname: 'mod_bigbluebuttonbn_get_recordings', args}])[0]; }; /** * Fetch the list of recordings from the server that can be imported. * * @param {Number} destinationinstanceid The destination instance ID * @param {Number} sourcebigbluebuttonbnid The original instance ID * @param {Number} sourcecourseid The destination instance ID * @param {String} tools the set of tools to display * @param {number} groupid * @returns {Promise} */ export const fetchRecordingsToImport = ( destinationinstanceid, sourcebigbluebuttonbnid, sourcecourseid, tools, groupid ) => { const args = { destinationinstanceid, sourcebigbluebuttonbnid, sourcecourseid, tools, }; if (groupid) { args.groupid = groupid; } return fetchMany([{methodname: 'mod_bigbluebuttonbn_get_recordings_to_import', args}])[0]; }; /** * Perform an update on a single recording. * * @param {object} args The instance ID * @returns {Promise} */ export const updateRecording = args => fetchMany([ { methodname: 'mod_bigbluebuttonbn_update_recording', args, } ])[0]; /** * End the Meeting * * @param {number} bigbluebuttonbnid * @param {number} groupid * @returns {Promise} */ export const endMeeting = (bigbluebuttonbnid, groupid) => fetchMany([ { methodname: 'mod_bigbluebuttonbn_end_meeting', args: { bigbluebuttonbnid, groupid }, } ])[0]; /** * Validate completion. * * @param {number} bigbluebuttonbnid * @returns {Promise} */ export const completionValidate = (bigbluebuttonbnid) => fetchMany([ { methodname: 'mod_bigbluebuttonbn_completion_validate', args: { bigbluebuttonbnid }, } ])[0]; /** * Fetch meeting info for the specified meeting. * * @param {number} bigbluebuttonbnid * @param {number} groupid * @param {boolean} [updatecache=false] * @returns {Promise} */ export const getMeetingInfo = (bigbluebuttonbnid, groupid, updatecache = false) => fetchMany([ { methodname: 'mod_bigbluebuttonbn_meeting_info', args: { bigbluebuttonbnid, groupid, updatecache, }, } ])[0]; PK\pt t amd/src/guest_access_modal.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 . /** * Javascript module for importing presets. * * @module mod_bigbluebuttonbn/guest_access_modal * @copyright 2022 Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import {getString} from 'core/str'; import ModalForm from 'core_form/modalform'; import {add as toastAdd, addToastRegion} from 'core/toast'; import { exception as displayException, } from 'core/notification'; const selectors = { showGuestAccessButton: '[data-action="show-guest-access"]', }; /** * Intialise the object and click event to show the popup form * * @param {object} guestInfo * @param {string} guestInfo.id * @param {string} guestInfo.groupid * @param {string} guestInfo.guestjoinurl * @param {string} guestInfo.guestpassword */ export const init = (guestInfo) => { const showGuestAccessButton = document.querySelector(selectors.showGuestAccessButton); if (showGuestAccessButton === null) { return; } const modalForm = new ModalForm({ modalConfig: { title: getString('guestaccess_title', 'mod_bigbluebuttonbn'), large: true, }, args: guestInfo, saveButtonText: getString('ok', 'core_moodle'), formClass: 'mod_bigbluebuttonbn\\form\\guest_add', }); showGuestAccessButton.addEventListener('click', event => { modalForm.show().then(() => { addToastRegion(modalForm.modal.getRoot()[0]); return true; }).catch(displayException); modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, (e) => { // Remove toast region as if not it will be displayed on the closed modal. const modalElement = modalForm.modal.getRoot()[0]; const regions = modalElement.querySelectorAll('.toast-wrapper'); regions.forEach((reg) => reg.remove()); if (e.detail.result) { if (e.detail.emailcount > 0) { toastAdd(getString('guestaccess_invite_success', 'mod_bigbluebuttonbn', e.detail), { type: 'success', } ); } } else { toastAdd(getString('guestaccess_invite_failure', 'mod_bigbluebuttonbn', e.detail), { type: 'warning', } ); } }, {once: true}); event.stopPropagation(); }); }; PK\^^KQKQamd/build/recordings.min.js.mapnu[{"version":3,"file":"recordings.min.js","sources":["../src/recordings.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 * JS for the recordings page on mod_bigbluebuttonbn plugin.\n *\n * @module mod_bigbluebuttonbn/recordings\n * @copyright 2021 Blindside Networks Inc\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as repository from './repository';\nimport {exception as displayException, saveCancelPromise} from 'core/notification';\nimport {prefetchStrings} from 'core/prefetch';\nimport {getString, getStrings} from 'core/str';\nimport {addIconToContainerWithPromise} from 'core/loadingicon';\nimport Pending from 'core/pending';\n\nconst stringsWithKeys = {\n first: 'view_recording_yui_first',\n prev: 'view_recording_yui_prev',\n next: 'view_recording_yui_next',\n last: 'view_recording_yui_last',\n goToLabel: 'view_recording_yui_page',\n goToAction: 'view_recording_yui_go',\n perPage: 'view_recording_yui_rows',\n showAll: 'view_recording_yui_show_all',\n};\n// Load global strings.\nprefetchStrings('bigbluebuttonbn', Object.entries(stringsWithKeys).map((entry) => entry[1]));\n\nconst getStringsForYui = () => {\n const stringMap = Object.keys(stringsWithKeys).map(key => {\n return {\n key: stringsWithKeys[key],\n component: 'mod_bigbluebuttonbn',\n };\n });\n\n // Return an object with the matching string keys (we want an object with {: ...}).\n return getStrings(stringMap)\n .then((stringArray) => Object.assign(\n {},\n ...Object.keys(stringsWithKeys).map(\n (key, index) => ({[key]: stringArray[index]})\n )\n ));\n};\n\nconst getYuiInstance = lang => new Promise(resolve => {\n // eslint-disable-next-line\n YUI({\n lang,\n }).use('intl', 'datatable', 'datatable-sort', 'datatable-paginator', 'datatype-number', Y => {\n resolve(Y);\n });\n});\n\n/**\n * Format the supplied date per the specified locale.\n *\n * @param {string} locale\n * @param {number} date\n * @returns {array}\n */\nconst formatDate = (locale, date) => {\n const realDate = new Date(date);\n return realDate.toLocaleDateString(locale, {\n weekday: 'long',\n year: 'numeric',\n month: 'long',\n day: 'numeric',\n });\n};\n\n/**\n * Format response data for the table.\n *\n * @param {string} response JSON-encoded table data\n * @returns {array}\n */\nconst getFormattedData = response => {\n const recordingData = response.tabledata;\n return JSON.parse(recordingData.data);\n};\n\nconst getTableNode = tableSelector => document.querySelector(tableSelector);\n\nconst fetchRecordingData = tableSelector => {\n const tableNode = getTableNode(tableSelector);\n if (tableNode === null) {\n return Promise.resolve(false);\n }\n\n if (tableNode.dataset.importMode) {\n return repository.fetchRecordingsToImport(\n tableNode.dataset.bbbid,\n tableNode.dataset.bbbSourceInstanceId,\n tableNode.dataset.bbbSourceCourseId,\n tableNode.dataset.tools,\n tableNode.dataset.groupId\n );\n } else {\n return repository.fetchRecordings(\n tableNode.dataset.bbbid,\n tableNode.dataset.tools,\n tableNode.dataset.groupId\n );\n }\n};\n\n/**\n * Fetch the data table functinos for the specified table.\n *\n * @param {String} tableId in which we will display the table\n * @param {String} searchFormId The Id of the relate.\n * @param {Object} dataTable\n * @returns {Object}\n * @private\n */\nconst getDataTableFunctions = (tableId, searchFormId, dataTable) => {\n const tableNode = getTableNode(tableId);\n const bbbid = tableNode.dataset.bbbid;\n\n const updateTableFromResponse = response => {\n if (!response || !response.status) {\n // There was no output at all.\n return;\n }\n\n dataTable.get('data').reset(getFormattedData(response));\n dataTable.set(\n 'currentData',\n dataTable.get('data')\n );\n\n const currentFilter = dataTable.get('currentFilter');\n if (currentFilter) {\n filterByText(currentFilter);\n }\n };\n\n const refreshTableData = () => fetchRecordingData(tableId).then(updateTableFromResponse);\n\n const filterByText = value => {\n const dataModel = dataTable.get('currentData');\n dataTable.set('currentFilter', value);\n\n const escapedRegex = value.replace(/[-[\\]{}()*+?.,\\\\^$|#\\s]/g, \"\\\\$&\");\n const rsearch = new RegExp(`.*?${escapedRegex}.*?`, 'i');\n\n dataTable.set('data', dataModel.filter({asList: true}, item => {\n const name = item.get('recording');\n if (name && rsearch.test(name)) {\n return true;\n }\n\n const description = item.get('description');\n return description && rsearch.test(description);\n }));\n };\n\n const requestAction = async(element) => {\n const getDataFromAction = (element, dataType) => {\n const dataElement = element.closest(`[data-${dataType}]`);\n if (dataElement) {\n return dataElement.dataset[dataType];\n }\n\n return null;\n };\n\n const elementData = element.dataset;\n const payload = {\n bigbluebuttonbnid: bbbid,\n recordingid: getDataFromAction(element, 'recordingid'),\n additionaloptions: getDataFromAction(element, 'additionaloptions'),\n action: elementData.action,\n };\n // Slight change for import, for additional options.\n if (!payload.additionaloptions) {\n payload.additionaloptions = {};\n }\n if (elementData.action === 'import') {\n const bbbsourceid = getDataFromAction(element, 'source-instance-id');\n const bbbcourseid = getDataFromAction(element, 'source-course-id');\n if (!payload.additionaloptions) {\n payload.additionaloptions = {};\n }\n payload.additionaloptions.sourceid = bbbsourceid ? bbbsourceid : 0;\n payload.additionaloptions.bbbcourseid = bbbcourseid ? bbbcourseid : 0;\n }\n // Now additional options should be a json string.\n payload.additionaloptions = JSON.stringify(payload.additionaloptions);\n if (element.dataset.requireConfirmation === \"1\") {\n // Create the confirmation dialogue.\n try {\n await saveCancelPromise(\n getString('confirm'),\n recordingConfirmationMessage(payload),\n getString('ok', 'moodle'),\n );\n } catch {\n // User cancelled the dialogue.\n return;\n }\n }\n\n return repository.updateRecording(payload);\n };\n\n const recordingConfirmationMessage = async(data) => {\n\n const playbackElement = document.querySelector(`#playbacks-${data.recordingid}`);\n const recordingType = await getString(\n playbackElement.dataset.imported === 'true' ? 'view_recording_link' : 'view_recording',\n 'bigbluebuttonbn'\n );\n\n const confirmation = await getString(`view_recording_${data.action}_confirmation`, 'bigbluebuttonbn', recordingType);\n\n if (data.action === 'import') {\n return confirmation;\n }\n\n // If it has associated links imported in a different course/activity, show that in confirmation dialog.\n const associatedLinkCount = document.querySelector(`a#recording-${data.action}-${data.recordingid}`)?.dataset?.links;\n if (!associatedLinkCount || associatedLinkCount === 0) {\n return confirmation;\n }\n\n const confirmationWarning = await getString(\n associatedLinkCount === 1\n ? `view_recording_${data.action}_confirmation_warning_p`\n : `view_recording_${data.action}_confirmation_warning_s`,\n 'bigbluebuttonbn',\n associatedLinkCount\n );\n\n return confirmationWarning + '\\n\\n' + confirmation;\n };\n\n /**\n * Process an action event.\n *\n * @param {Event} e\n */\n const processAction = e => {\n const popoutLink = e.target.closest('[data-action=\"play\"]');\n if (popoutLink) {\n e.preventDefault();\n\n const videoPlayer = window.open('', '_blank');\n videoPlayer.opener = null;\n videoPlayer.location.href = popoutLink.href;\n // TODO send a recording viewed event when this event will be implemented.\n return;\n }\n\n // Fetch any clicked anchor.\n const clickedLink = e.target.closest('a[data-action]');\n if (clickedLink && !clickedLink.classList.contains('disabled')) {\n e.preventDefault();\n\n // Create a spinning icon on the table.\n const iconPromise = addIconToContainerWithPromise(dataTable.get('boundingBox').getDOMNode());\n\n requestAction(clickedLink)\n .then(refreshTableData)\n .then(iconPromise.resolve)\n .catch(displayException);\n }\n };\n\n const processSearchSubmission = e => {\n // Prevent the default action.\n e.preventDefault();\n const parentNode = e.target.closest('div[role=search]');\n const searchInput = parentNode.querySelector('input[name=search]');\n filterByText(searchInput.value);\n };\n\n const registerEventListeners = () => {\n // Add event listeners to the table boundingBox.\n const boundingBox = dataTable.get('boundingBox').getDOMNode();\n boundingBox.addEventListener('click', processAction);\n\n // Setup the search from handlers.\n const searchForm = document.querySelector(searchFormId);\n if (searchForm) {\n const searchButton = document.querySelector(searchFormId + ' button');\n searchButton.addEventListener('click', processSearchSubmission);\n }\n };\n\n return {\n filterByText,\n refreshTableData,\n registerEventListeners,\n };\n};\n\n/**\n * Setup the data table for the specified BBB instance.\n *\n * @param {String} tableId in which we will display the table\n * @param {String} searchFormId The Id of the relate.\n * @param {object} response The response from the data request\n * @returns {Promise}\n */\nconst setupDatatable = (tableId, searchFormId, response) => {\n if (!response) {\n return Promise.resolve();\n }\n\n if (!response.status) {\n // Something failed. Continue to show the plain output.\n return Promise.resolve();\n }\n\n const recordingData = response.tabledata;\n\n const pendingPromise = new Pending('mod_bigbluebuttonbn/recordings/setupDatatable');\n return Promise.all([getYuiInstance(recordingData.locale), getStringsForYui()])\n .then(([yuiInstance, strings]) => {\n // Here we use a custom formatter for date.\n // See https://clarle.github.io/yui3/yui/docs/api/classes/DataTable.BodyView.Formatters.html\n // Inspired from examples here: https://clarle.github.io/yui3/yui/docs/datatable/\n // Normally formatter have the prototype: (col) => (cell) => , see:\n // https://clarle.github.io/yui3/yui/docs/api/files/datatable_js_formatters.js.html#l100 .\n const dateCustomFormatter = () => (cell) => formatDate(recordingData.locale, cell.value);\n // Add the fetched strings to the YUI Instance.\n yuiInstance.Intl.add('datatable-paginator', yuiInstance.config.lang, {...strings});\n yuiInstance.DataTable.BodyView.Formatters.customDate = dateCustomFormatter;\n return yuiInstance;\n })\n .then(yuiInstance => {\n\n const tableData = getFormattedData(response);\n yuiInstance.RecordsPaginatorView = Y.Base.create('my-paginator-view', yuiInstance.DataTable.Paginator.View, [], {\n _modelChange: function(e) {\n var changed = e.changed,\n totalItems = (changed && changed.totalItems);\n if (totalItems) {\n this._updateControlsUI(e.target.get('page'));\n }\n }\n });\n return new yuiInstance.DataTable({\n paginatorView: \"RecordsPaginatorView\",\n width: \"1195px\",\n columns: recordingData.columns,\n data: tableData,\n rowsPerPage: 10,\n paginatorLocation: ['header', 'footer'],\n autoSync: true\n });\n })\n .then(dataTable => {\n dataTable.render(tableId);\n const {registerEventListeners} = getDataTableFunctions(\n tableId,\n searchFormId,\n dataTable);\n registerEventListeners();\n return dataTable;\n })\n .then(dataTable => {\n pendingPromise.resolve();\n return dataTable;\n });\n};\n\n/**\n * Initialise recordings code.\n *\n * @method init\n * @param {String} tableId in which we will display the table\n * @param {String} searchFormId The Id of the relate.\n */\nexport const init = (tableId, searchFormId) => {\n const pendingPromise = new Pending('mod_bigbluebuttonbn/recordings:init');\n\n fetchRecordingData(tableId)\n .then(response => setupDatatable(tableId, searchFormId, response))\n .then(() => pendingPromise.resolve())\n .catch(displayException);\n};\n"],"names":["stringsWithKeys","first","prev","next","last","goToLabel","goToAction","perPage","showAll","Object","entries","map","entry","getStringsForYui","stringMap","keys","key","component","then","stringArray","assign","index","getFormattedData","response","recordingData","tabledata","JSON","parse","data","getTableNode","tableSelector","document","querySelector","fetchRecordingData","tableNode","Promise","resolve","dataset","importMode","repository","fetchRecordingsToImport","bbbid","bbbSourceInstanceId","bbbSourceCourseId","tools","groupId","fetchRecordings","getDataTableFunctions","tableId","searchFormId","dataTable","updateTableFromResponse","status","get","reset","set","currentFilter","filterByText","refreshTableData","value","dataModel","escapedRegex","replace","rsearch","RegExp","filter","asList","item","name","test","description","recordingConfirmationMessage","async","playbackElement","recordingid","recordingType","imported","confirmation","action","associatedLinkCount","_document$querySelect","_document$querySelect2","links","processAction","e","popoutLink","target","closest","preventDefault","videoPlayer","window","open","opener","location","href","clickedLink","classList","contains","iconPromise","getDOMNode","getDataFromAction","element","dataType","dataElement","elementData","payload","bigbluebuttonbnid","additionaloptions","bbbsourceid","bbbcourseid","sourceid","stringify","requireConfirmation","updateRecording","requestAction","catch","displayException","processSearchSubmission","searchInput","registerEventListeners","addEventListener","setupDatatable","pendingPromise","Pending","all","lang","locale","YUI","use","Y","_ref","yuiInstance","strings","Intl","add","config","DataTable","BodyView","Formatters","customDate","cell","formatDate","date","Date","toLocaleDateString","weekday","year","month","day","tableData","RecordsPaginatorView","Base","create","Paginator","View","_modelChange","changed","totalItems","_updateControlsUI","paginatorView","width","columns","rowsPerPage","paginatorLocation","autoSync","render"],"mappings":";;;;;;;kFA8BMA,gBAAkB,CACpBC,MAAO,2BACPC,KAAM,0BACNC,KAAM,0BACNC,KAAM,0BACNC,UAAW,0BACXC,WAAY,wBACZC,QAAS,0BACTC,QAAS,6DAGG,kBAAmBC,OAAOC,QAAQV,iBAAiBW,KAAKC,OAAUA,MAAM,YAElFC,iBAAmB,WACfC,UAAYL,OAAOM,KAAKf,iBAAiBW,KAAIK,MACxC,CACHA,IAAKhB,gBAAgBgB,KACrBC,UAAW,iCAKZ,mBAAWH,WACbI,MAAMC,aAAgBV,OAAOW,OAC1B,MACGX,OAAOM,KAAKf,iBAAiBW,KAC5B,CAACK,IAAKK,WAAaL,KAAMG,YAAYE,eAqC/CC,iBAAmBC,iBACfC,cAAgBD,SAASE,iBACxBC,KAAKC,MAAMH,cAAcI,OAG9BC,aAAeC,eAAiBC,SAASC,cAAcF,eAEvDG,mBAAqBH,sBACjBI,UAAYL,aAAaC,sBACb,OAAdI,UACOC,QAAQC,SAAQ,GAGvBF,UAAUG,QAAQC,WACXC,WAAWC,wBACdN,UAAUG,QAAQI,MAClBP,UAAUG,QAAQK,oBAClBR,UAAUG,QAAQM,kBAClBT,UAAUG,QAAQO,MAClBV,UAAUG,QAAQQ,SAGfN,WAAWO,gBACdZ,UAAUG,QAAQI,MAClBP,UAAUG,QAAQO,MAClBV,UAAUG,QAAQQ,UAcxBE,sBAAwB,CAACC,QAASC,aAAcC,mBAE5CT,MADYZ,aAAamB,SACPX,QAAQI,MAE1BU,wBAA0B5B,eACvBA,WAAaA,SAAS6B,cAK3BF,UAAUG,IAAI,QAAQC,MAAMhC,iBAAiBC,WAC7C2B,UAAUK,IACN,cACAL,UAAUG,IAAI,eAGZG,cAAgBN,UAAUG,IAAI,iBAChCG,eACAC,aAAaD,gBAIfE,iBAAmB,IAAMzB,mBAAmBe,SAAS9B,KAAKiC,yBAE1DM,aAAeE,cACXC,UAAYV,UAAUG,IAAI,eAChCH,UAAUK,IAAI,gBAAiBI,aAEzBE,aAAeF,MAAMG,QAAQ,2BAA4B,QACzDC,QAAU,IAAIC,0BAAmBH,2BAA0B,KAEjEX,UAAUK,IAAI,OAAQK,UAAUK,OAAO,CAACC,QAAQ,IAAOC,aAC7CC,KAAOD,KAAKd,IAAI,gBAClBe,MAAQL,QAAQM,KAAKD,aACd,QAGLE,YAAcH,KAAKd,IAAI,sBACtBiB,aAAeP,QAAQM,KAAKC,kBAqDrCC,6BAA+BC,MAAAA,8DAE3BC,gBAAkB1C,SAASC,mCAA4BJ,KAAK8C,cAC5DC,oBAAsB,kBACa,SAArCF,gBAAgBpC,QAAQuC,SAAsB,sBAAwB,iBACtE,mBAGEC,mBAAqB,2CAA4BjD,KAAKkD,wBAAuB,kBAAmBH,kBAElF,WAAhB/C,KAAKkD,cACED,mBAILE,kDAAsBhD,SAASC,oCAA6BJ,KAAKkD,mBAAUlD,KAAK8C,+EAA1DM,sBAA0E3C,iDAA1E4C,uBAAmFC,UAC1GH,qBAA+C,IAAxBA,2BACjBF,0BAGuB,2CAENjD,KAAKkD,OADL,IAAxBC,yEAGA,kBACAA,qBAGyB,OAASF,cAQpCM,cAAgBC,UACZC,WAAaD,EAAEE,OAAOC,QAAQ,2BAChCF,WAAY,CACZD,EAAEI,uBAEIC,YAAcC,OAAOC,KAAK,GAAI,iBACpCF,YAAYG,OAAS,UACrBH,YAAYI,SAASC,KAAOT,WAAWS,YAMrCC,YAAcX,EAAEE,OAAOC,QAAQ,qBACjCQ,cAAgBA,YAAYC,UAAUC,SAAS,YAAa,CAC5Db,EAAEI,uBAGIU,aAAc,8CAA8BhD,UAAUG,IAAI,eAAe8C,cAvGjE3B,OAAAA,gBACZ4B,kBAAoB,CAACC,QAASC,kBAC1BC,YAAcF,QAAQd,wBAAiBe,sBACzCC,YACOA,YAAYlE,QAAQiE,UAGxB,MAGLE,YAAcH,QAAQhE,QACtBoE,QAAU,CACZC,kBAAmBjE,MACnBiC,YAAa0B,kBAAkBC,QAAS,eACxCM,kBAAmBP,kBAAkBC,QAAS,qBAC9CvB,OAAQ0B,YAAY1B,WAGnB2B,QAAQE,oBACTF,QAAQE,kBAAoB,IAEL,WAAvBH,YAAY1B,OAAqB,OAC3B8B,YAAcR,kBAAkBC,QAAS,sBACzCQ,YAAcT,kBAAkBC,QAAS,oBAC1CI,QAAQE,oBACTF,QAAQE,kBAAoB,IAEhCF,QAAQE,kBAAkBG,SAAWF,aAA4B,EACjEH,QAAQE,kBAAkBE,YAAcA,aAA4B,KAGxEJ,QAAQE,kBAAoBjF,KAAKqF,UAAUN,QAAQE,mBACP,MAAxCN,QAAQhE,QAAQ2E,8BAGN,oCACF,kBAAU,WACVzC,6BAA6BkC,UAC7B,kBAAU,KAAM,WAEtB,oBAMClE,WAAW0E,gBAAgBR,UA2D9BS,CAAcnB,aACT7E,KAAKwC,kBACLxC,KAAKgF,YAAY9D,SACjB+E,MAAMC,2BAIbC,wBAA0BjC,IAE5BA,EAAEI,uBAEI8B,YADalC,EAAEE,OAAOC,QAAQ,oBACLvD,cAAc,sBAC7CyB,aAAa6D,YAAY3D,cAgBtB,CACHF,aAAAA,aACAC,iBAAAA,iBACA6D,uBAhB2B,KAEPrE,UAAUG,IAAI,eAAe8C,aACrCqB,iBAAiB,QAASrC,kBAGnBpD,SAASC,cAAciB,cAC1B,CACSlB,SAASC,cAAciB,aAAe,WAC9CuE,iBAAiB,QAASH,6BAmB7CI,eAAiB,CAACzE,QAASC,aAAc1B,gBACtCA,gBACMY,QAAQC,cAGdb,SAAS6B,cAEHjB,QAAQC,gBAGbZ,cAAgBD,SAASE,UAEzBiG,eAAiB,IAAIC,iBAAQ,wDAC5BxF,QAAQyF,IAAI,EAlRAC,KAkRgBrG,cAAcsG,OAlRtB,IAAI3F,SAAQC,UAEvC2F,IAAI,CACAF,KAAAA,OACDG,IAAI,OAAQ,YAAa,iBAAkB,sBAAuB,mBAAmBC,IACpF7F,QAAQ6F,UA6Q8CpH,qBACrDK,MAAKgH,WAAEC,YAAaC,qBAQjBD,YAAYE,KAAKC,IAAI,sBAAuBH,YAAYI,OAAOV,KAAM,IAAIO,UACzED,YAAYK,UAAUC,SAASC,WAAWC,WAHd,IAAOC,OAASC,OAzQpCf,OAyQ+CtG,cAAcsG,OAzQrDgB,KAyQ6DF,KAAKjF,MAxQzE,IAAIoF,KAAKD,MACVE,mBAAmBlB,OAAQ,CACvCmB,QAAS,OACTC,KAAM,UACNC,MAAO,OACPC,IAAK,YANM,IAACtB,OAAQgB,MA6QTX,eAEVjH,MAAKiH,oBAEIkB,UAAY/H,iBAAiBC,iBACnC4G,YAAYmB,qBAAuBrB,EAAEsB,KAAKC,OAAO,oBAAqBrB,YAAYK,UAAUiB,UAAUC,KAAM,GAAI,CAC5GC,aAAc,SAASvE,OACfwE,QAAUxE,EAAEwE,QACEA,SAAWA,QAAQC,iBAE5BC,kBAAkB1E,EAAEE,OAAOjC,IAAI,YAIzC,IAAI8E,YAAYK,UAAU,CAC7BuB,cAAe,uBACfC,MAAO,SACPC,QAASzI,cAAcyI,QACvBrI,KAAMyH,UACNa,YAAa,GACbC,kBAAmB,CAAC,SAAU,UAC9BC,UAAU,OAGjBlJ,MAAKgC,YACFA,UAAUmH,OAAOrH,eACXuE,uBAACA,wBAA0BxE,sBAC7BC,QACAC,aACAC,kBACJqE,yBACOrE,aAEVhC,MAAKgC,YACFwE,eAAetF,UACRc,aAhUI2E,IAAAA,oBA2UH,CAAC7E,QAASC,sBACpByE,eAAiB,IAAIC,iBAAQ,uCAEnC1F,mBAAmBe,SACd9B,MAAKK,UAAYkG,eAAezE,QAASC,aAAc1B,YACvDL,MAAK,IAAMwG,eAAetF,YAC1B+E,MAAMC"}PK\/Bamd/build/actions.min.jsnu[define("mod_bigbluebuttonbn/actions",["./repository","core/notification","./events","core/str"],(function(_repository,_notification,_events,_str){let listening=!1;listening||((()=>{document.addEventListener("click",(e=>{const actionButton=e.target.closest('.bbb-btn-action[data-action="end"]');if(!actionButton)return;e.preventDefault();const bbbId=actionButton.dataset.bbbId,groupId=actionButton.dataset.groupId?actionButton.dataset.groupId:0;var title,question,saveLabel;(title=(0,_str.getString)("end_session_confirm_title","mod_bigbluebuttonbn"),question=(0,_str.getString)("end_session_confirm","mod_bigbluebuttonbn"),saveLabel=(0,_str.getString)("yes","moodle"),new Promise((resolve=>{(0,_notification.saveCancel)(title,question,saveLabel,resolve)}))).then((()=>(0,_repository.endMeeting)(bbbId,groupId))).then((()=>{(0,_events.notifySessionEnded)(bbbId,groupId)})).catch(_notification.exception)}))})(),listening=!0)})); //# sourceMappingURL=actions.min.js.mapPK\""amd/build/modform.min.jsnu[define("mod_bigbluebuttonbn/modform",["exports","core/str","core/notification","core/templates"],(function(_exports,_str,_notification,_templates){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * JS for the mod_form page on mod_bigbluebuttonbn plugin. * * @module mod_bigbluebuttonbn/modform * @copyright 2021 Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_notification=_interopRequireDefault(_notification),_templates=_interopRequireDefault(_templates);const ELEMENT_SELECTOR_instanceTypeSelection=()=>document.querySelector("select#id_type"),ELEMENT_SELECTOR_instanceTypeProfiles=()=>document.querySelector("[data-profile-types]"),ELEMENT_SELECTOR_participantData=()=>document.querySelector("[data-participant-data]"),ELEMENT_SELECTOR_participantList=()=>document.getElementsByName("participants")[0],ELEMENT_SELECTOR_participantTable=()=>document.getElementById("participant_list_table"),ELEMENT_SELECTOR_participantSelectionType=()=>document.getElementsByName("bigbluebuttonbn_participant_selection_type")[0],ELEMENT_SELECTOR_participantSelection=()=>document.getElementsByName("bigbluebuttonbn_participant_selection")[0],ELEMENT_SELECTOR_participantAddButton=()=>document.getElementsByName("bigbluebuttonbn_participant_selection_add")[0];_exports.init=info=>{const selectedType=ELEMENT_SELECTOR_instanceTypeSelection(),instanceTypeProfiles=JSON.parse(ELEMENT_SELECTOR_instanceTypeProfiles().dataset.profileTypes);let profileType=info.instanceTypeDefault;null!==selectedType&&-1!==selectedType.selectedIndex&&(profileType=selectedType.options[selectedType.selectedIndex].value);const isFeatureEnabled=(profileType,feature)=>-1!==instanceTypeProfiles[profileType].features.indexOf(feature);applyInstanceTypeProfile(profileType,isFeatureEnabled),selectedType.addEventListener("change",(e=>{applyInstanceTypeProfile(e.target.value,isFeatureEnabled)})),ELEMENT_SELECTOR_participantSelectionType().addEventListener("change",(e=>{const currentTypeSelect=e.target;updateSelectionFromType(currentTypeSelect)})),ELEMENT_SELECTOR_participantAddButton().addEventListener("click",(e=>{e.stopPropagation(),e.preventDefault(),participantAddFromCurrentSelection()})),participantListInit()};const applyInstanceTypeProfile=(profileType,isFeatureEnabled)=>{let showAll=isFeatureEnabled(profileType,"all");const showFieldset=(id,show)=>{const node=document.querySelector("#"+id);node&&(node.style.display=show?"block":"none")},showInput=(id,show)=>{const node=document.querySelector("#"+id);if(node){var ancestor=node.closest("div").closest("div");ancestor.style.display=show?"block":"none"}},showFormGroup=(id,show)=>{const node=document.querySelector("#fgroup_id_"+id);node&&(show?node.classList.remove("hidden"):node.classList.add("hidden"))};showFieldset("id_room",showAll||isFeatureEnabled(profileType,"showroom")),showInput("id_record",showAll||isFeatureEnabled(profileType,"showroom")),showFieldset("id_recordings",showAll||isFeatureEnabled(profileType,"showrecordings")),showInput("id_recordings_imported",showAll||isFeatureEnabled(profileType,"showrecordings")),showFieldset("id_lock",showAll||isFeatureEnabled(profileType,"lock")),showFieldset("id_guestaccess",showAll||isFeatureEnabled(profileType,"showroom")),showFieldset("id_preuploadpresentation",showAll||isFeatureEnabled(profileType,"preuploadpresentation")),showFieldset("id_permissions",showAll||isFeatureEnabled(profileType,"permissions")),showFieldset("id_schedule",showAll||isFeatureEnabled(profileType,"schedule")),showFieldset("id_modstandardelshdr",showAll||isFeatureEnabled(profileType,"modstandardelshdr")),showFieldset("id_availabilityconditionsheader",showAll||isFeatureEnabled(profileType,"availabilityconditionsheader")),showFieldset("id_tagshdr",showAll||isFeatureEnabled(profileType,"tagshdr")),showFieldset("id_competenciessection",showAll||isFeatureEnabled(profileType,"competenciessection")),showFieldset("id_modstandardgrade",showAll||isFeatureEnabled(profileType,"modstandardgrade")),showFieldset("id_activitycompletionheader",showAll||isFeatureEnabled(profileType,"activitycompletionheader")),showFormGroup("completionattendancegroup",showAll||isFeatureEnabled(profileType,"completionattendance")),showFormGroup("completionengagementgroup",showAll||isFeatureEnabled(profileType,"completionengagement"))},participantListInit=()=>{const participantData=JSON.parse(ELEMENT_SELECTOR_participantData().dataset.participantData);getParticipantList().forEach((participant=>{const selectionTypeValue=participant.selectiontype,selectionValue=participant.selectionid,selectionRole=participant.role;"all"!==participant.selectiontype&&void 0===participantData[participant.selectiontype].children[participant.selectionid]||participantAddToForm(selectionTypeValue,selectionValue,selectionRole,!0).then()}))},participantAddToForm=async(selectionTypeValue,selectionValue,selectedRole,canRemove)=>{const participantData=JSON.parse(ELEMENT_SELECTOR_participantData().dataset.participantData);let roles={viewer:{id:"viewer",label:await(0,_str.getString)("mod_form_field_participant_bbb_role_viewer","mod_bigbluebuttonbn")},moderator:{id:"moderator",label:await(0,_str.getString)("mod_form_field_participant_bbb_role_moderator","mod_bigbluebuttonbn")}};roles[selectedRole].isselected=!0;try{const listTable=document.querySelector("#participant_list_table tbody"),templateContext={selectiontypevalue:selectionTypeValue,selectionvalue:selectionValue,participanttype:participantData[selectionTypeValue].name,participantvalue:"all"!==selectionTypeValue?participantData[selectionTypeValue].children[selectionValue].name:null,roles:Object.values(roles),canRemove:canRemove},{html:html,js:js}=await _templates.default.renderForPromise("mod_bigbluebuttonbn/participant_form_add",templateContext),newNode=_templates.default.appendNodeContents(listTable,html,js)[0];newNode.querySelector(".participant-select").addEventListener("change",(()=>{participantListRoleUpdate(selectionTypeValue,selectionValue)}));const removeNode=newNode.querySelector(".remove-button");removeNode&&removeNode.addEventListener("click",(()=>{participantRemove(selectionTypeValue,selectionValue)}))}catch(e){_notification.default.exception(e)}},participantListUpdate=list=>{ELEMENT_SELECTOR_participantList().value=JSON.stringify(list)},getParticipantList=()=>{const participantListValue=ELEMENT_SELECTOR_participantList().value;return participantListValue?JSON.parse(participantListValue):[]},participantRemove=(selectionTypeValue,selectionValue)=>{const pList=getParticipantList(),id="participant_list_tr_"+selectionTypeValue+"-"+selectionValue,participantListTable=ELEMENT_SELECTOR_participantTable(),selectionid=""===selectionValue?null:selectionValue;for(let i=0;i{const participantListRoleSelection=document.querySelector("#participant_list_tr_".concat(type,"-").concat(id," .participant-select")),pList=getParticipantList();for(var i=0;i{let selectionType=ELEMENT_SELECTOR_participantSelectionType(),selection=ELEMENT_SELECTOR_participantSelection();const pList=getParticipantList();for(var i=0;i{const createNewOption=(selectItem,label,value)=>{const option=document.createElement("option");option.text=label,option.value=value,selectItem.add(option)},participantData=JSON.parse(ELEMENT_SELECTOR_participantData().dataset.participantData),participantSelect=ELEMENT_SELECTOR_participantSelection();for(;participantSelect.firstChild;)participantSelect.removeChild(participantSelect.firstChild);if(-1!==currentTypeSelect.selectedIndex){Object.values(participantData[currentTypeSelect.value].children).forEach((option=>{createNewOption(participantSelect,option.name,option.id)})),"all"===currentTypeSelect.value?(createNewOption(participantSelect,"---------------","all"),participantSelect.disabled=!0):participantSelect.disabled=!1}}})); //# sourceMappingURL=modform.min.js.mapPK\1""amd/build/recordings.min.jsnu[define("mod_bigbluebuttonbn/recordings",["exports","./repository","core/notification","core/prefetch","core/str","core/loadingicon","core/pending"],(function(_exports,repository,_notification,_prefetch,_str,_loadingicon,_pending){var obj;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)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,repository=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} /** * JS for the recordings page on mod_bigbluebuttonbn plugin. * * @module mod_bigbluebuttonbn/recordings * @copyright 2021 Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */(repository),_pending=(obj=_pending)&&obj.__esModule?obj:{default:obj};const stringsWithKeys={first:"view_recording_yui_first",prev:"view_recording_yui_prev",next:"view_recording_yui_next",last:"view_recording_yui_last",goToLabel:"view_recording_yui_page",goToAction:"view_recording_yui_go",perPage:"view_recording_yui_rows",showAll:"view_recording_yui_show_all"};(0,_prefetch.prefetchStrings)("bigbluebuttonbn",Object.entries(stringsWithKeys).map((entry=>entry[1])));const getStringsForYui=()=>{const stringMap=Object.keys(stringsWithKeys).map((key=>({key:stringsWithKeys[key],component:"mod_bigbluebuttonbn"})));return(0,_str.getStrings)(stringMap).then((stringArray=>Object.assign({},...Object.keys(stringsWithKeys).map(((key,index)=>({[key]:stringArray[index]}))))))},getFormattedData=response=>{const recordingData=response.tabledata;return JSON.parse(recordingData.data)},getTableNode=tableSelector=>document.querySelector(tableSelector),fetchRecordingData=tableSelector=>{const tableNode=getTableNode(tableSelector);return null===tableNode?Promise.resolve(!1):tableNode.dataset.importMode?repository.fetchRecordingsToImport(tableNode.dataset.bbbid,tableNode.dataset.bbbSourceInstanceId,tableNode.dataset.bbbSourceCourseId,tableNode.dataset.tools,tableNode.dataset.groupId):repository.fetchRecordings(tableNode.dataset.bbbid,tableNode.dataset.tools,tableNode.dataset.groupId)},getDataTableFunctions=(tableId,searchFormId,dataTable)=>{const bbbid=getTableNode(tableId).dataset.bbbid,updateTableFromResponse=response=>{if(!response||!response.status)return;dataTable.get("data").reset(getFormattedData(response)),dataTable.set("currentData",dataTable.get("data"));const currentFilter=dataTable.get("currentFilter");currentFilter&&filterByText(currentFilter)},refreshTableData=()=>fetchRecordingData(tableId).then(updateTableFromResponse),filterByText=value=>{const dataModel=dataTable.get("currentData");dataTable.set("currentFilter",value);const escapedRegex=value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&"),rsearch=new RegExp(".*?".concat(escapedRegex,".*?"),"i");dataTable.set("data",dataModel.filter({asList:!0},(item=>{const name=item.get("recording");if(name&&rsearch.test(name))return!0;const description=item.get("description");return description&&rsearch.test(description)})))},recordingConfirmationMessage=async data=>{var _document$querySelect,_document$querySelect2;const playbackElement=document.querySelector("#playbacks-".concat(data.recordingid)),recordingType=await(0,_str.getString)("true"===playbackElement.dataset.imported?"view_recording_link":"view_recording","bigbluebuttonbn"),confirmation=await(0,_str.getString)("view_recording_".concat(data.action,"_confirmation"),"bigbluebuttonbn",recordingType);if("import"===data.action)return confirmation;const associatedLinkCount=null===(_document$querySelect=document.querySelector("a#recording-".concat(data.action,"-").concat(data.recordingid)))||void 0===_document$querySelect||null===(_document$querySelect2=_document$querySelect.dataset)||void 0===_document$querySelect2?void 0:_document$querySelect2.links;if(!associatedLinkCount||0===associatedLinkCount)return confirmation;return await(0,_str.getString)("view_recording_".concat(data.action,1===associatedLinkCount?"_confirmation_warning_p":"_confirmation_warning_s"),"bigbluebuttonbn",associatedLinkCount)+"\n\n"+confirmation},processAction=e=>{const popoutLink=e.target.closest('[data-action="play"]');if(popoutLink){e.preventDefault();const videoPlayer=window.open("","_blank");return videoPlayer.opener=null,void(videoPlayer.location.href=popoutLink.href)}const clickedLink=e.target.closest("a[data-action]");if(clickedLink&&!clickedLink.classList.contains("disabled")){e.preventDefault();const iconPromise=(0,_loadingicon.addIconToContainerWithPromise)(dataTable.get("boundingBox").getDOMNode());(async element=>{const getDataFromAction=(element,dataType)=>{const dataElement=element.closest("[data-".concat(dataType,"]"));return dataElement?dataElement.dataset[dataType]:null},elementData=element.dataset,payload={bigbluebuttonbnid:bbbid,recordingid:getDataFromAction(element,"recordingid"),additionaloptions:getDataFromAction(element,"additionaloptions"),action:elementData.action};if(payload.additionaloptions||(payload.additionaloptions={}),"import"===elementData.action){const bbbsourceid=getDataFromAction(element,"source-instance-id"),bbbcourseid=getDataFromAction(element,"source-course-id");payload.additionaloptions||(payload.additionaloptions={}),payload.additionaloptions.sourceid=bbbsourceid||0,payload.additionaloptions.bbbcourseid=bbbcourseid||0}if(payload.additionaloptions=JSON.stringify(payload.additionaloptions),"1"===element.dataset.requireConfirmation)try{await(0,_notification.saveCancelPromise)((0,_str.getString)("confirm"),recordingConfirmationMessage(payload),(0,_str.getString)("ok","moodle"))}catch{return}return repository.updateRecording(payload)})(clickedLink).then(refreshTableData).then(iconPromise.resolve).catch(_notification.exception)}},processSearchSubmission=e=>{e.preventDefault();const searchInput=e.target.closest("div[role=search]").querySelector("input[name=search]");filterByText(searchInput.value)};return{filterByText:filterByText,refreshTableData:refreshTableData,registerEventListeners:()=>{dataTable.get("boundingBox").getDOMNode().addEventListener("click",processAction);if(document.querySelector(searchFormId)){document.querySelector(searchFormId+" button").addEventListener("click",processSearchSubmission)}}}},setupDatatable=(tableId,searchFormId,response)=>{if(!response)return Promise.resolve();if(!response.status)return Promise.resolve();const recordingData=response.tabledata,pendingPromise=new _pending.default("mod_bigbluebuttonbn/recordings/setupDatatable");return Promise.all([(lang=recordingData.locale,new Promise((resolve=>{YUI({lang:lang}).use("intl","datatable","datatable-sort","datatable-paginator","datatype-number",(Y=>{resolve(Y)}))}))),getStringsForYui()]).then((_ref=>{let[yuiInstance,strings]=_ref;return yuiInstance.Intl.add("datatable-paginator",yuiInstance.config.lang,{...strings}),yuiInstance.DataTable.BodyView.Formatters.customDate=()=>cell=>{return locale=recordingData.locale,date=cell.value,new Date(date).toLocaleDateString(locale,{weekday:"long",year:"numeric",month:"long",day:"numeric"});var locale,date},yuiInstance})).then((yuiInstance=>{const tableData=getFormattedData(response);return yuiInstance.RecordsPaginatorView=Y.Base.create("my-paginator-view",yuiInstance.DataTable.Paginator.View,[],{_modelChange:function(e){var changed=e.changed;changed&&changed.totalItems&&this._updateControlsUI(e.target.get("page"))}}),new yuiInstance.DataTable({paginatorView:"RecordsPaginatorView",width:"1195px",columns:recordingData.columns,data:tableData,rowsPerPage:10,paginatorLocation:["header","footer"],autoSync:!0})})).then((dataTable=>{dataTable.render(tableId);const{registerEventListeners:registerEventListeners}=getDataTableFunctions(tableId,searchFormId,dataTable);return registerEventListeners(),dataTable})).then((dataTable=>(pendingPromise.resolve(),dataTable)));var lang};_exports.init=(tableId,searchFormId)=>{const pendingPromise=new _pending.default("mod_bigbluebuttonbn/recordings:init");fetchRecordingData(tableId).then((response=>setupDatatable(tableId,searchFormId,response))).then((()=>pendingPromise.resolve())).catch(_notification.exception)}})); //# sourceMappingURL=recordings.min.js.mapPK\kjamd/build/repository.min.jsnu[define("mod_bigbluebuttonbn/repository",["exports","core/ajax"],(function(_exports,_ajax){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.updateRecording=_exports.getMeetingInfo=_exports.fetchRecordingsToImport=_exports.fetchRecordings=_exports.endMeeting=_exports.completionValidate=void 0;_exports.fetchRecordings=(bigbluebuttonbnid,tools,groupid)=>{const args={bigbluebuttonbnid:bigbluebuttonbnid,tools:tools};return groupid&&(args.groupid=groupid),(0,_ajax.call)([{methodname:"mod_bigbluebuttonbn_get_recordings",args:args}])[0]};_exports.fetchRecordingsToImport=(destinationinstanceid,sourcebigbluebuttonbnid,sourcecourseid,tools,groupid)=>{const args={destinationinstanceid:destinationinstanceid,sourcebigbluebuttonbnid:sourcebigbluebuttonbnid,sourcecourseid:sourcecourseid,tools:tools};return groupid&&(args.groupid=groupid),(0,_ajax.call)([{methodname:"mod_bigbluebuttonbn_get_recordings_to_import",args:args}])[0]};_exports.updateRecording=args=>(0,_ajax.call)([{methodname:"mod_bigbluebuttonbn_update_recording",args:args}])[0];_exports.endMeeting=(bigbluebuttonbnid,groupid)=>(0,_ajax.call)([{methodname:"mod_bigbluebuttonbn_end_meeting",args:{bigbluebuttonbnid:bigbluebuttonbnid,groupid:groupid}}])[0];_exports.completionValidate=bigbluebuttonbnid=>(0,_ajax.call)([{methodname:"mod_bigbluebuttonbn_completion_validate",args:{bigbluebuttonbnid:bigbluebuttonbnid}}])[0];_exports.getMeetingInfo=function(bigbluebuttonbnid,groupid){let updatecache=arguments.length>2&&void 0!==arguments[2]&&arguments[2];return(0,_ajax.call)([{methodname:"mod_bigbluebuttonbn_meeting_info",args:{bigbluebuttonbnid:bigbluebuttonbnid,groupid:groupid,updatecache:updatecache}}])[0]}})); //# sourceMappingURL=repository.min.js.mapPK\-|{8 amd/build/actions.min.js.mapnu[{"version":3,"file":"actions.min.js","sources":["../src/actions.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 * JS actions.\n *\n * @module mod_bigbluebuttonbn/actions\n * @copyright 2021 Blindside Networks Inc\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {endMeeting as requestEndMeeting} from './repository';\nimport {\n exception as displayException,\n saveCancel,\n} from 'core/notification';\nimport {notifySessionEnded} from './events';\nimport {getString} from 'core/str';\n\nconst confirmedPromise = (title, question, saveLabel) => new Promise(resolve => {\n saveCancel(title, question, saveLabel, resolve);\n});\n\nconst registerEventListeners = () => {\n document.addEventListener('click', e => {\n const actionButton = e.target.closest('.bbb-btn-action[data-action=\"end\"]');\n if (!actionButton) {\n return;\n }\n\n e.preventDefault();\n\n const bbbId = actionButton.dataset.bbbId;\n const groupId = actionButton.dataset.groupId ? actionButton.dataset.groupId : 0;\n\n confirmedPromise(\n getString('end_session_confirm_title', 'mod_bigbluebuttonbn'),\n getString('end_session_confirm', 'mod_bigbluebuttonbn'),\n getString('yes', 'moodle')\n )\n .then(() => requestEndMeeting(bbbId, groupId))\n .then(() => {\n notifySessionEnded(bbbId, groupId);\n\n return;\n })\n .catch(displayException);\n });\n};\n\nlet listening = false;\nif (!listening) {\n registerEventListeners();\n listening = true;\n}\n"],"names":["listening","document","addEventListener","e","actionButton","target","closest","preventDefault","bbbId","dataset","groupId","title","question","saveLabel","Promise","resolve","then","catch","displayException","registerEventListeners"],"mappings":"sJA6DIA,WAAY,EACXA,YA5B0B,MAC3BC,SAASC,iBAAiB,SAASC,UACzBC,aAAeD,EAAEE,OAAOC,QAAQ,0CACjCF,oBAILD,EAAEI,uBAEIC,MAAQJ,aAAaK,QAAQD,MAC7BE,QAAUN,aAAaK,QAAQC,QAAUN,aAAaK,QAAQC,QAAU,EAd7D,IAACC,MAAOC,SAAUC,WAAjBF,OAiBd,kBAAU,4BAA6B,uBAjBlBC,UAkBrB,kBAAU,sBAAuB,uBAlBFC,WAmB/B,kBAAU,MAAO,UAnB4B,IAAIC,SAAQC,uCACtDJ,MAAOC,SAAUC,UAAWE,aAoBlCC,MAAK,KAAM,0BAAkBR,MAAOE,WACpCM,MAAK,oCACiBR,MAAOE,YAI7BO,MAAMC,6BAMXC,GACAnB,WAAY"}PK\; amd/build/roomupdater.min.js.mapnu[{"version":3,"file":"roomupdater.min.js","sources":["../src/roomupdater.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 * JS room updater.\n *\n * @module mod_bigbluebuttonbn/roomupdater\n * @copyright 2021 Blindside Networks Inc\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Pending from 'core/pending';\nimport Templates from \"core/templates\";\nimport {exception as displayException} from 'core/notification';\nimport {getMeetingInfo} from './repository';\n\nlet timerReference = null;\nlet timerRunning = false;\nlet pollInterval = 0;\nlet pollIntervalFactor = 1;\nconst MAX_POLL_INTERVAL_FACTOR = 10;\n\nconst resetValues = () => {\n timerRunning = false;\n timerReference = null;\n pollInterval = 0;\n pollIntervalFactor = 1;\n};\n\n/**\n * Start the information poller.\n * @param {Number} interval interval in miliseconds between each poll action.\n */\nexport const start = (interval) => {\n resetValues();\n timerRunning = true;\n pollInterval = interval;\n poll();\n};\n\n/**\n * Stop the room updater.\n */\nexport const stop = () => {\n if (timerReference) {\n clearTimeout(timerReference);\n }\n resetValues();\n};\n\n/**\n * Start the information poller.\n */\nconst poll = () => {\n if (!timerRunning || !pollInterval) {\n // The poller has been stopped.\n return;\n }\n updateRoom()\n .then((updateOk) => {\n if (!updateOk) {\n pollIntervalFactor = (pollIntervalFactor < MAX_POLL_INTERVAL_FACTOR) ?\n pollIntervalFactor + 1 : MAX_POLL_INTERVAL_FACTOR;\n // We make sure if there is an error that we do not try too often.\n }\n timerReference = setTimeout(() => poll(), pollInterval * pollIntervalFactor);\n return true;\n })\n .catch();\n};\n\n/**\n * Update the room information.\n *\n * @param {boolean} [updatecache=false] should we update cache\n * @returns {Promise}\n */\nexport const updateRoom = (updatecache = false) => {\n const bbbRoomViewElement = document.getElementById('bigbluebuttonbn-room-view');\n if (bbbRoomViewElement === null) {\n return Promise.resolve(false);\n }\n\n const bbbId = bbbRoomViewElement.dataset.bbbId;\n const groupId = bbbRoomViewElement.dataset.groupId;\n\n const pendingPromise = new Pending('mod_bigbluebuttonbn/roomupdater:updateRoom');\n\n return getMeetingInfo(bbbId, groupId, updatecache)\n .then(data => {\n // Just make sure we have the right information for the template.\n data.haspresentations = !!(data.presentations && data.presentations.length);\n return Templates.renderForPromise('mod_bigbluebuttonbn/room_view', data);\n })\n .then(({html, js}) => Templates.replaceNode(bbbRoomViewElement, html, js))\n .then(() => pendingPromise.resolve())\n .catch(displayException);\n};\n"],"names":["timerReference","timerRunning","pollInterval","pollIntervalFactor","resetValues","interval","poll","clearTimeout","updateRoom","then","updateOk","setTimeout","catch","updatecache","bbbRoomViewElement","document","getElementById","Promise","resolve","bbbId","dataset","groupId","pendingPromise","Pending","data","haspresentations","presentations","length","Templates","renderForPromise","_ref","html","js","replaceNode","displayException"],"mappings":";;;;;;;iNA4BIA,eAAiB,KACjBC,cAAe,EACfC,aAAe,EACfC,mBAAqB,QAGnBC,YAAc,KAChBH,cAAe,EACfD,eAAiB,KACjBE,aAAe,EACfC,mBAAqB,kBAOHE,WAClBD,cACAH,cAAe,EACfC,aAAeG,SACfC,sBAMgB,KACZN,gBACAO,aAAaP,gBAEjBI,qBAMEE,KAAO,KACJL,cAAiBC,cAItBM,aACKC,MAAMC,WACEA,WACDP,mBAAsBA,mBAzCL,GA0CbA,mBAAqB,EA1CR,IA6CrBH,eAAiBW,YAAW,IAAML,QAAQJ,aAAeC,qBAClD,KAEVS,SASIJ,WAAa,eAACK,0EACjBC,mBAAqBC,SAASC,eAAe,gCACxB,OAAvBF,0BACOG,QAAQC,SAAQ,SAGrBC,MAAQL,mBAAmBM,QAAQD,MACnCE,QAAUP,mBAAmBM,QAAQC,QAErCC,eAAiB,IAAIC,iBAAQ,qDAE5B,8BAAeJ,MAAOE,QAASR,aACjCJ,MAAKe,OAEFA,KAAKC,oBAAsBD,KAAKE,gBAAiBF,KAAKE,cAAcC,QAC7DC,mBAAUC,iBAAiB,gCAAiCL,SAEtEf,MAAKqB,WAACC,KAACA,KAADC,GAAOA,gBAAQJ,mBAAUK,YAAYnB,mBAAoBiB,KAAMC,OACrEvB,MAAK,IAAMa,eAAeJ,YAC1BN,MAAMsB"}PK\nm amd/build/rooms.min.jsnu[define("mod_bigbluebuttonbn/rooms",["exports","./actions","./repository","./roomupdater","core/notification","core/pending","core/str","core/toast","./events"],(function(_exports,_actions,repository,roomUpdater,_notification,_pending,_str,_toast,_events){var obj;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} /** * JS actions for the rooms page for mod_bigbluebuttonbn. * * @module mod_bigbluebuttonbn/rooms * @copyright 2021 Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setupWindowAutoClose=_exports.init=void 0,repository=_interopRequireWildcard(repository),roomUpdater=_interopRequireWildcard(roomUpdater),_pending=(obj=_pending)&&obj.__esModule?obj:{default:obj};_exports.init=(bigbluebuttonbnid,pollInterval)=>{const completionElement=document.querySelector("a[href*=completion_validate]");completionElement&&completionElement.addEventListener("click",(event=>{event.preventDefault();const pendingPromise=new _pending.default("mod_bigbluebuttonbn/completion:validate");repository.completionValidate(bigbluebuttonbnid).then((()=>(0,_str.getString)("completionvalidatestatetriggered","mod_bigbluebuttonbn"))).then((str=>(0,_toast.add)(str))).then((()=>pendingPromise.resolve())).catch(_notification.exception)})),document.addEventListener("click",(e=>{const joinButton=e.target.closest('[data-action="join"]');joinButton&&(window.open(joinButton.href,"bigbluebutton_conference"),e.preventDefault(),setTimeout((()=>{roomUpdater.updateRoom(!0)}),pollInterval))})),document.addEventListener(_events.eventTypes.sessionEnded,(()=>{roomUpdater.stop(),roomUpdater.updateRoom(),(0,_notification.fetchNotifications)()})),window.addEventListener(_events.eventTypes.currentSessionEnded,(()=>{roomUpdater.stop(),roomUpdater.updateRoom(),(0,_notification.fetchNotifications)()})),roomUpdater.start(pollInterval)};_exports.setupWindowAutoClose=function(){let closeDelay=arguments.length>0&&void 0!==arguments[0]?arguments[0]:2e3;(0,_events.notifyCurrentSessionEnded)(window.opener),window.addEventListener("onbeforeunload",(()=>{window.opener.setTimeout((()=>{roomUpdater.updateRoom(!0)}),closeDelay)}),{once:!0}),window.close()}})); //# sourceMappingURL=rooms.min.js.mapPK\c<^O'amd/build/guest_access_modal.min.js.mapnu[{"version":3,"file":"guest_access_modal.min.js","sources":["../src/guest_access_modal.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 * Javascript module for importing presets.\n *\n * @module mod_bigbluebuttonbn/guest_access_modal\n * @copyright 2022 Blindside Networks Inc\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport {getString} from 'core/str';\nimport ModalForm from 'core_form/modalform';\nimport {add as toastAdd, addToastRegion} from 'core/toast';\nimport {\n exception as displayException,\n} from 'core/notification';\nconst selectors = {\n showGuestAccessButton: '[data-action=\"show-guest-access\"]',\n};\n\n/**\n * Intialise the object and click event to show the popup form\n *\n * @param {object} guestInfo\n * @param {string} guestInfo.id\n * @param {string} guestInfo.groupid\n * @param {string} guestInfo.guestjoinurl\n * @param {string} guestInfo.guestpassword\n */\nexport const init = (guestInfo) => {\n const showGuestAccessButton = document.querySelector(selectors.showGuestAccessButton);\n if (showGuestAccessButton === null) {\n return;\n }\n\n const modalForm = new ModalForm({\n modalConfig: {\n title: getString('guestaccess_title', 'mod_bigbluebuttonbn'),\n large: true,\n },\n args: guestInfo,\n saveButtonText: getString('ok', 'core_moodle'),\n formClass: 'mod_bigbluebuttonbn\\\\form\\\\guest_add',\n });\n showGuestAccessButton.addEventListener('click', event => {\n modalForm.show().then(() => {\n addToastRegion(modalForm.modal.getRoot()[0]);\n return true;\n }).catch(displayException);\n modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, (e) => {\n // Remove toast region as if not it will be displayed on the closed modal.\n const modalElement = modalForm.modal.getRoot()[0];\n const regions = modalElement.querySelectorAll('.toast-wrapper');\n regions.forEach((reg) => reg.remove());\n if (e.detail.result) {\n if (e.detail.emailcount > 0) {\n toastAdd(getString('guestaccess_invite_success', 'mod_bigbluebuttonbn', e.detail),\n {\n type: 'success',\n }\n );\n }\n } else {\n toastAdd(getString('guestaccess_invite_failure', 'mod_bigbluebuttonbn', e.detail),\n {\n type: 'warning',\n }\n );\n }\n }, {once: true});\n event.stopPropagation();\n });\n};\n"],"names":["selectors","guestInfo","showGuestAccessButton","document","querySelector","modalForm","ModalForm","modalConfig","title","large","args","saveButtonText","formClass","addEventListener","event","show","then","modal","getRoot","catch","displayException","events","FORM_SUBMITTED","e","querySelectorAll","forEach","reg","remove","detail","result","emailcount","type","once","stopPropagation"],"mappings":";;;;;;;sJA4BMA,gCACqB,kDAYNC,kBACXC,sBAAwBC,SAASC,cAAcJ,oCACvB,OAA1BE,mCAIEG,UAAY,IAAIC,mBAAU,CAC5BC,YAAa,CACTC,OAAO,kBAAU,oBAAqB,uBACtCC,OAAO,GAEXC,KAAMT,UACNU,gBAAgB,kBAAU,KAAM,eAChCC,UAAW,yCAEfV,sBAAsBW,iBAAiB,SAASC,QAC5CT,UAAUU,OAAOC,MAAK,+BACHX,UAAUY,MAAMC,UAAU,KAClC,KACRC,MAAMC,yBACTf,UAAUQ,iBAAiBR,UAAUgB,OAAOC,gBAAiBC,IAEpClB,UAAUY,MAAMC,UAAU,GAClBM,iBAAiB,kBACtCC,SAASC,KAAQA,IAAIC,WACzBJ,EAAEK,OAAOC,OACLN,EAAEK,OAAOE,WAAa,mBACb,kBAAU,6BAA8B,sBAAuBP,EAAEK,QACtE,CACIG,KAAM,4BAKT,kBAAU,6BAA8B,sBAAuBR,EAAEK,QACtE,CACIG,KAAM,cAInB,CAACC,MAAM,IACVlB,MAAMmB"}PK\ m\\amd/build/repository.min.js.mapnu[{"version":3,"file":"repository.min.js","sources":["../src/repository.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 * Repository to perform WS calls for mod_bigbluebuttonbn.\n *\n * @module mod_bigbluebuttonbn/repository\n * @copyright 2021 Blindside Networks Inc\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {call as fetchMany} from 'core/ajax';\n\n/**\n * Fetch the list of recordings from the server.\n *\n * @param {Number} bigbluebuttonbnid The instance ID\n * @param {String} tools the set of tools to display\n * @param {number} groupid\n * @returns {Promise}\n */\nexport const fetchRecordings = (bigbluebuttonbnid, tools, groupid) => {\n const args = {\n bigbluebuttonbnid,\n tools,\n };\n\n if (groupid) {\n args.groupid = groupid;\n }\n\n return fetchMany([{methodname: 'mod_bigbluebuttonbn_get_recordings', args}])[0];\n};\n\n/**\n * Fetch the list of recordings from the server that can be imported.\n *\n * @param {Number} destinationinstanceid The destination instance ID\n * @param {Number} sourcebigbluebuttonbnid The original instance ID\n * @param {Number} sourcecourseid The destination instance ID\n * @param {String} tools the set of tools to display\n * @param {number} groupid\n * @returns {Promise}\n */\nexport const fetchRecordingsToImport = (\n destinationinstanceid,\n sourcebigbluebuttonbnid,\n sourcecourseid,\n tools,\n groupid\n) => {\n const args = {\n destinationinstanceid,\n sourcebigbluebuttonbnid,\n sourcecourseid,\n tools,\n };\n\n if (groupid) {\n args.groupid = groupid;\n }\n\n return fetchMany([{methodname: 'mod_bigbluebuttonbn_get_recordings_to_import', args}])[0];\n};\n\n/**\n * Perform an update on a single recording.\n *\n * @param {object} args The instance ID\n * @returns {Promise}\n */\nexport const updateRecording = args => fetchMany([\n {\n methodname: 'mod_bigbluebuttonbn_update_recording',\n args,\n }\n])[0];\n\n/**\n * End the Meeting\n *\n * @param {number} bigbluebuttonbnid\n * @param {number} groupid\n * @returns {Promise}\n */\nexport const endMeeting = (bigbluebuttonbnid, groupid) => fetchMany([\n {\n methodname: 'mod_bigbluebuttonbn_end_meeting',\n args: {\n bigbluebuttonbnid,\n groupid\n },\n }\n])[0];\n\n/**\n * Validate completion.\n *\n * @param {number} bigbluebuttonbnid\n * @returns {Promise}\n */\nexport const completionValidate = (bigbluebuttonbnid) => fetchMany([\n {\n methodname: 'mod_bigbluebuttonbn_completion_validate',\n args: {\n bigbluebuttonbnid\n },\n }\n])[0];\n\n\n/**\n * Fetch meeting info for the specified meeting.\n *\n * @param {number} bigbluebuttonbnid\n * @param {number} groupid\n * @param {boolean} [updatecache=false]\n * @returns {Promise}\n */\nexport const getMeetingInfo = (bigbluebuttonbnid, groupid, updatecache = false) => fetchMany([\n {\n methodname: 'mod_bigbluebuttonbn_meeting_info',\n args: {\n bigbluebuttonbnid,\n groupid,\n updatecache,\n },\n }\n])[0];\n"],"names":["bigbluebuttonbnid","tools","groupid","args","methodname","destinationinstanceid","sourcebigbluebuttonbnid","sourcecourseid","updatecache"],"mappings":"6UAiC+B,CAACA,kBAAmBC,MAAOC,iBAChDC,KAAO,CACTH,kBAAAA,kBACAC,MAAAA,cAGAC,UACAC,KAAKD,QAAUA,UAGZ,cAAU,CAAC,CAACE,WAAY,qCAAsCD,KAAAA,QAAQ,qCAa1C,CACnCE,sBACAC,wBACAC,eACAN,MACAC,iBAEMC,KAAO,CACTE,sBAAAA,sBACAC,wBAAAA,wBACAC,eAAAA,eACAN,MAAAA,cAGAC,UACAC,KAAKD,QAAUA,UAGZ,cAAU,CAAC,CAACE,WAAY,+CAAgDD,KAAAA,QAAQ,6BAS5DA,OAAQ,cAAU,CAC7C,CACIC,WAAY,uCACZD,KAAAA,QAEL,uBASuB,CAACH,kBAAmBE,WAAY,cAAU,CAChE,CACIE,WAAY,kCACZD,KAAM,CACFH,kBAAAA,kBACAE,QAAAA,YAGT,+BAQgCF,oBAAsB,cAAU,CAC/D,CACII,WAAY,0CACZD,KAAM,CACFH,kBAAAA,sBAGT,2BAW2B,SAACA,kBAAmBE,aAASM,2EAAwB,cAAU,CACzF,CACIJ,WAAY,mCACZD,KAAM,CACFH,kBAAAA,kBACAE,QAAAA,QACAM,YAAAA,gBAGT"}PK\B amd/build/events.min.js.mapnu[{"version":3,"file":"events.min.js","sources":["../src/events.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 * Events for the mod_bigbluebuttonbn plugin.\n *\n * @module mod_bigbluebuttonbn/events\n * @copyright 2021 Blindside Networks Inc\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {dispatchEvent} from 'core/event_dispatcher';\n\nexport const eventTypes = {\n /**\n * Fired when a session has been ended.\n *\n * @event mod_bigbluebuttonbn/sessionEnded\n * @type CustomEvent\n * @property {object} detail\n * @property {number} detail.bbbId\n * @property {number} detail.groupId\n */\n sessionEnded: 'mod_bigbluebuttonbn/sessionEnded',\n\n /**\n * Fired when the current session has been ended.\n *\n * @event mod_bigbluebuttonbn/currentSessionEnded\n * @type CustomEvent\n * @property {object} detail\n */\n currentSessionEnded: 'mod_bigbluebuttonbn/currentSessionEnded',\n};\n\n/**\n * Trigger the sessionEnded event.\n *\n * @param {number} bbbId\n * @param {number} groupId\n * @returns {CustomEvent}\n * @fires event:mod_bigbluebuttonbn/sessionEnded\n */\nexport const notifySessionEnded = (bbbId, groupId) => dispatchEvent(eventTypes.sessionEnded, {\n bbbId,\n groupId,\n});\n\n/**\n * Trigger the currentSessionEnded event.\n *\n * @param {Element} container\n * @returns {CustomEvent}\n * @fires event:mod_bigbluebuttonbn/currentSessionEnded\n */\nexport const notifyCurrentSessionEnded = container => dispatchEvent(\n eventTypes.currentSessionEnded,\n {},\n container\n);\n"],"names":["eventTypes","sessionEnded","currentSessionEnded","bbbId","groupId","container"],"mappings":";;;;;;;;MAyBaA,WAAa,CAUtBC,aAAc,mCASdC,oBAAqB,sGAWS,CAACC,MAAOC,WAAY,mCAAcJ,WAAWC,aAAc,CACzFE,MAAAA,MACAC,QAAAA,6CAUqCC,YAAa,mCAClDL,WAAWE,oBACX,GACAG"}PK\Zkkamd/build/index.min.js.mapnu[{"version":3,"file":"index.min.js","sources":["../src/index.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 * Index page for the mod_bigbluebuttonbn plugin.\n *\n * @module mod_bigbluebuttonbn/index\n * @copyright 2021 Blindside Networks Inc\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport './actions';\nimport {eventTypes} from './events';\n\nexport const init = () => {\n document.addEventListener(eventTypes.sessionEnded, () => {\n window.location.reload();\n });\n};\n"],"names":["document","addEventListener","eventTypes","sessionEnded","window","location","reload"],"mappings":"sMA0BoB,KAChBA,SAASC,iBAAiBC,mBAAWC,cAAc,KAC/CC,OAAOC,SAASC"}PK\}ظamd/build/events.min.jsnu[define("mod_bigbluebuttonbn/events",["exports","core/event_dispatcher"],(function(_exports,_event_dispatcher){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.notifySessionEnded=_exports.notifyCurrentSessionEnded=_exports.eventTypes=void 0; /** * Events for the mod_bigbluebuttonbn plugin. * * @module mod_bigbluebuttonbn/events * @copyright 2021 Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ const eventTypes={sessionEnded:"mod_bigbluebuttonbn/sessionEnded",currentSessionEnded:"mod_bigbluebuttonbn/currentSessionEnded"};_exports.eventTypes=eventTypes;_exports.notifySessionEnded=(bbbId,groupId)=>(0,_event_dispatcher.dispatchEvent)(eventTypes.sessionEnded,{bbbId:bbbId,groupId:groupId});_exports.notifyCurrentSessionEnded=container=>(0,_event_dispatcher.dispatchEvent)(eventTypes.currentSessionEnded,{},container)})); //# sourceMappingURL=events.min.js.mapPK\ԥRRamd/build/index.min.jsnu[define("mod_bigbluebuttonbn/index",["exports","./actions","./events"],(function(_exports,_actions,_events){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0;_exports.init=()=>{document.addEventListener(_events.eventTypes.sessionEnded,(()=>{window.location.reload()}))}})); //# sourceMappingURL=index.min.js.mapPK\Ê #amd/build/guest_access_modal.min.jsnu[define("mod_bigbluebuttonbn/guest_access_modal",["exports","core/str","core_form/modalform","core/toast","core/notification"],(function(_exports,_str,_modalform,_toast,_notification){var obj; /** * Javascript module for importing presets. * * @module mod_bigbluebuttonbn/guest_access_modal * @copyright 2022 Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_modalform=(obj=_modalform)&&obj.__esModule?obj:{default:obj};const selectors_showGuestAccessButton='[data-action="show-guest-access"]';_exports.init=guestInfo=>{const showGuestAccessButton=document.querySelector(selectors_showGuestAccessButton);if(null===showGuestAccessButton)return;const modalForm=new _modalform.default({modalConfig:{title:(0,_str.getString)("guestaccess_title","mod_bigbluebuttonbn"),large:!0},args:guestInfo,saveButtonText:(0,_str.getString)("ok","core_moodle"),formClass:"mod_bigbluebuttonbn\\form\\guest_add"});showGuestAccessButton.addEventListener("click",(event=>{modalForm.show().then((()=>((0,_toast.addToastRegion)(modalForm.modal.getRoot()[0]),!0))).catch(_notification.exception),modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,(e=>{modalForm.modal.getRoot()[0].querySelectorAll(".toast-wrapper").forEach((reg=>reg.remove())),e.detail.result?e.detail.emailcount>0&&(0,_toast.add)((0,_str.getString)("guestaccess_invite_success","mod_bigbluebuttonbn",e.detail),{type:"success"}):(0,_toast.add)((0,_str.getString)("guestaccess_invite_failure","mod_bigbluebuttonbn",e.detail),{type:"warning"})}),{once:!0}),event.stopPropagation()}))}})); //# sourceMappingURL=guest_access_modal.min.js.mapPK\G;TTamd/build/roomupdater.min.jsnu[define("mod_bigbluebuttonbn/roomupdater",["exports","core/pending","core/templates","core/notification","./repository"],(function(_exports,_pending,_templates,_notification,_repository){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * JS room updater. * * @module mod_bigbluebuttonbn/roomupdater * @copyright 2021 Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.updateRoom=_exports.stop=_exports.start=void 0,_pending=_interopRequireDefault(_pending),_templates=_interopRequireDefault(_templates);let timerReference=null,timerRunning=!1,pollInterval=0,pollIntervalFactor=1;const resetValues=()=>{timerRunning=!1,timerReference=null,pollInterval=0,pollIntervalFactor=1};_exports.start=interval=>{resetValues(),timerRunning=!0,pollInterval=interval,poll()};_exports.stop=()=>{timerReference&&clearTimeout(timerReference),resetValues()};const poll=()=>{timerRunning&&pollInterval&&updateRoom().then((updateOk=>(updateOk||(pollIntervalFactor=pollIntervalFactor<10?pollIntervalFactor+1:10),timerReference=setTimeout((()=>poll()),pollInterval*pollIntervalFactor),!0))).catch()},updateRoom=function(){let updatecache=arguments.length>0&&void 0!==arguments[0]&&arguments[0];const bbbRoomViewElement=document.getElementById("bigbluebuttonbn-room-view");if(null===bbbRoomViewElement)return Promise.resolve(!1);const bbbId=bbbRoomViewElement.dataset.bbbId,groupId=bbbRoomViewElement.dataset.groupId,pendingPromise=new _pending.default("mod_bigbluebuttonbn/roomupdater:updateRoom");return(0,_repository.getMeetingInfo)(bbbId,groupId,updatecache).then((data=>(data.haspresentations=!(!data.presentations||!data.presentations.length),_templates.default.renderForPromise("mod_bigbluebuttonbn/room_view",data)))).then((_ref=>{let{html:html,js:js}=_ref;return _templates.default.replaceNode(bbbRoomViewElement,html,js)})).then((()=>pendingPromise.resolve())).catch(_notification.exception)};_exports.updateRoom=updateRoom})); //# sourceMappingURL=roomupdater.min.js.mapPK\OOamd/build/modform.min.js.mapnu[{"version":3,"file":"modform.min.js","sources":["../src/modform.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 * JS for the mod_form page on mod_bigbluebuttonbn plugin.\n *\n * @module mod_bigbluebuttonbn/modform\n * @copyright 2021 Blindside Networks Inc\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getString} from 'core/str';\nimport Notification from 'core/notification';\nimport Templates from \"core/templates\";\n\n/**\n * Get all selectors in one place.\n *\n */\nconst ELEMENT_SELECTOR = {\n instanceTypeSelection: () => document.querySelector('select#id_type'),\n instanceTypeProfiles: () => document.querySelector('[data-profile-types]'),\n participantData: () => document.querySelector('[data-participant-data]'),\n participantList: () => document.getElementsByName('participants')[0],\n participantTable: () => document.getElementById('participant_list_table'),\n participantSelectionType: () => document.getElementsByName('bigbluebuttonbn_participant_selection_type')[0],\n participantSelection: () => document.getElementsByName('bigbluebuttonbn_participant_selection')[0],\n participantAddButton: () => document.getElementsByName('bigbluebuttonbn_participant_selection_add')[0],\n};\n/**\n * Initialise the moodle form code.\n *\n * This will help hide or show items depending on the selection of the instance type.\n *\n * @method init\n * @param {object} info\n */\nexport const init = (info) => {\n const selectedType = ELEMENT_SELECTOR.instanceTypeSelection();\n const instanceTypeProfiles = JSON.parse(ELEMENT_SELECTOR.instanceTypeProfiles().dataset.profileTypes);\n\n let profileType = info.instanceTypeDefault;\n if (selectedType !== null && selectedType.selectedIndex !== -1) {\n profileType = selectedType.options[selectedType.selectedIndex].value;\n }\n\n const isFeatureEnabled = (profileType, feature) => {\n const features = instanceTypeProfiles[profileType].features;\n return (features.indexOf(feature) !== -1);\n };\n applyInstanceTypeProfile(profileType, isFeatureEnabled);\n\n // Change form visible fields depending on the selection.\n selectedType.addEventListener('change', (e) => {\n applyInstanceTypeProfile(e.target.value, isFeatureEnabled);\n });\n\n ELEMENT_SELECTOR.participantSelectionType().addEventListener('change', (e) => {\n const currentTypeSelect = e.target;\n updateSelectionFromType(currentTypeSelect);\n });\n\n ELEMENT_SELECTOR.participantAddButton().addEventListener('click', (e) => {\n e.stopPropagation();\n e.preventDefault();\n participantAddFromCurrentSelection();\n });\n\n participantListInit();\n};\n\n/**\n * Show or hide form element depending on the selected profile\n *\n * @param {string} profileType\n * @param {function} isFeatureEnabled\n */\nconst applyInstanceTypeProfile = (profileType, isFeatureEnabled) => {\n let showAll = isFeatureEnabled(profileType, 'all');\n const showFieldset = (id, show) => {\n // Show room settings validation.\n const node = document.querySelector('#' + id);\n if (!node) {\n return;\n }\n if (show) {\n node.style.display = 'block';\n return;\n }\n node.style.display = 'none';\n };\n const showInput = (id, show) => {\n // Show room settings validation.\n const node = document.querySelector('#' + id);\n if (!node) {\n return;\n }\n var ancestor = node.closest('div').closest('div');\n if (show) {\n ancestor.style.display = 'block';\n return;\n }\n ancestor.style.display = 'none';\n };\n const showFormGroup = (id, show) => {\n // Show room settings validation.\n const node = document.querySelector('#fgroup_id_' + id);\n if (!node) {\n return;\n }\n if (show) {\n node.classList.remove('hidden');\n return;\n }\n node.classList.add('hidden');\n };\n // Show room settings validation.\n showFieldset('id_room', showAll ||\n isFeatureEnabled(profileType, 'showroom'));\n showInput('id_record', showAll ||\n isFeatureEnabled(profileType, 'showroom'));\n // Show recordings settings validation.\n showFieldset('id_recordings', showAll ||\n isFeatureEnabled(profileType, 'showrecordings'));\n // Show recordings imported settings validation.\n showInput('id_recordings_imported', showAll ||\n isFeatureEnabled(profileType, 'showrecordings'));\n // Show lock settings validation.\n showFieldset('id_lock', showAll ||\n isFeatureEnabled(profileType, 'lock'));\n // Show guest settings validation.\n showFieldset('id_guestaccess', showAll ||\n isFeatureEnabled(profileType, 'showroom'));\n // Preuploadpresentation feature validation.\n showFieldset('id_preuploadpresentation', showAll ||\n isFeatureEnabled(profileType, 'preuploadpresentation'));\n // Participants feature validation.\n showFieldset('id_permissions', showAll ||\n isFeatureEnabled(profileType, 'permissions'));\n // Schedule feature validation.\n showFieldset('id_schedule', showAll ||\n isFeatureEnabled(profileType, 'schedule'));\n // Common module settings validation.\n showFieldset('id_modstandardelshdr', showAll ||\n isFeatureEnabled(profileType, 'modstandardelshdr'));\n // Restrict access validation.\n showFieldset('id_availabilityconditionsheader', showAll ||\n isFeatureEnabled(profileType, 'availabilityconditionsheader'));\n // Tags validation.\n showFieldset('id_tagshdr', showAll || isFeatureEnabled(profileType, 'tagshdr'));\n // Competencies validation.\n showFieldset('id_competenciessection', showAll ||\n isFeatureEnabled(profileType, 'competenciessection'));\n // Standards grading feature validation.\n showFieldset('id_modstandardgrade', showAll ||\n isFeatureEnabled(profileType, 'modstandardgrade'));\n // Completion validation.\n showFieldset('id_activitycompletionheader', showAll ||\n isFeatureEnabled(profileType, 'activitycompletionheader'));\n // Completion validation.\n showFormGroup('completionattendancegroup', showAll ||\n isFeatureEnabled(profileType, 'completionattendance'));\n // Completion validation.\n showFormGroup('completionengagementgroup', showAll ||\n isFeatureEnabled(profileType, 'completionengagement'));\n};\n\n/**\n * Init the participant list\n */\nconst participantListInit = () => {\n const participantData = JSON.parse(ELEMENT_SELECTOR.participantData().dataset.participantData);\n const participantList = getParticipantList();\n participantList.forEach(participant => {\n const selectionTypeValue = participant.selectiontype;\n const selectionValue = participant.selectionid;\n const selectionRole = participant.role;\n if (participant.selectiontype === 'all' ||\n typeof participantData[participant.selectiontype].children[participant.selectionid] !== 'undefined') {\n // Add it to the form, but don't add the delete button if it is the first item.\n participantAddToForm(selectionTypeValue, selectionValue, selectionRole, true).then();\n }\n });\n};\n\n/**\n * Add rows to the participant list depending on the current selection.\n *\n * @param {string} selectionTypeValue\n * @param {string} selectionValue\n * @param {string} selectedRole\n * @param {boolean} canRemove\n * @returns {Promise}\n */\nconst participantAddToForm = async(selectionTypeValue, selectionValue, selectedRole, canRemove) => {\n const participantData = JSON.parse(ELEMENT_SELECTOR.participantData().dataset.participantData);\n const sviewer = await getString('mod_form_field_participant_bbb_role_viewer', 'mod_bigbluebuttonbn');\n const smoderator = await getString('mod_form_field_participant_bbb_role_moderator', 'mod_bigbluebuttonbn');\n let roles = {\n viewer: {'id': 'viewer', label: sviewer},\n moderator: {'id': 'moderator', label: smoderator}\n };\n roles[selectedRole].isselected = true;\n try {\n const listTable = document.querySelector('#participant_list_table tbody');\n const templateContext = {\n 'selectiontypevalue': selectionTypeValue,\n 'selectionvalue': selectionValue,\n 'participanttype': participantData[selectionTypeValue].name,\n 'participantvalue':\n (selectionTypeValue !== 'all') ?\n participantData[selectionTypeValue].children[selectionValue].name : null,\n 'roles': Object.values(roles),\n 'canRemove': canRemove\n };\n const {html, js} = await Templates.renderForPromise('mod_bigbluebuttonbn/participant_form_add', templateContext);\n const newNode = Templates.appendNodeContents(listTable, html, js)[0];\n newNode.querySelector('.participant-select').addEventListener('change', () => {\n participantListRoleUpdate(selectionTypeValue, selectionValue);\n });\n // Now add the callbacks: participantListRoleUpdate() and participantRemove().\n const removeNode = newNode.querySelector('.remove-button');\n if (removeNode) {\n removeNode\n .addEventListener('click', () => {\n participantRemove(selectionTypeValue, selectionValue);\n });\n }\n\n } catch (e) {\n Notification.exception(e);\n }\n};\n/*\n\n */\n\n/**\n * Update the related form element with the list value.\n *\n * @param {object} list\n */\nconst participantListUpdate = (list) => {\n const participantList = ELEMENT_SELECTOR.participantList();\n participantList.value = JSON.stringify(list);\n};\n\n/**\n *\n * @returns {any}\n */\nconst getParticipantList = () => {\n const participantListValue = ELEMENT_SELECTOR.participantList().value;\n if (participantListValue) {\n return JSON.parse(participantListValue);\n }\n return [];\n};\n\n/**\n * Remove participant both in the table/form and in the form element.\n *\n * @param {string} selectionTypeValue\n * @param {string} selectionValue\n */\nconst participantRemove = (selectionTypeValue, selectionValue) => {\n const pList = getParticipantList();\n const id = 'participant_list_tr_' + selectionTypeValue + '-' + selectionValue;\n const participantListTable = ELEMENT_SELECTOR.participantTable();\n const selectionid = (selectionValue === '' ? null : selectionValue);\n for (let i = 0; i < pList.length; i++) {\n if (pList[i].selectiontype === selectionTypeValue &&\n pList[i].selectionid === selectionid) {\n pList.splice(i, 1);\n }\n }\n // Remove from the form.\n for (let i = 0; i < participantListTable.rows.length; i++) {\n if (participantListTable.rows[i].id === id) {\n participantListTable.deleteRow(i);\n }\n }\n // Update value in the form.\n participantListUpdate(pList);\n};\n\n/**\n * Role update\n *\n * @param {string} type\n * @param {string} id\n */\nconst participantListRoleUpdate = (type, id) => {\n // Update in memory.\n const participantListRoleSelection = document.querySelector(`#participant_list_tr_${type}-${id} .participant-select`);\n const pList = getParticipantList();\n\n for (var i = 0; i < pList.length; i++) {\n if (pList[i].selectiontype === type && pList[i].selectionid === id) {\n pList[i].role = participantListRoleSelection.value;\n }\n }\n // Update in the form.\n participantListUpdate(pList);\n};\n\n/**\n * Add participant from the currently selected options\n */\nconst participantAddFromCurrentSelection = () => {\n let selectionType = ELEMENT_SELECTOR.participantSelectionType();\n let selection = ELEMENT_SELECTOR.participantSelection();\n const pList = getParticipantList();\n // Lookup to see if it has been added already.\n for (var i = 0; i < pList.length; i++) {\n if (pList[i].selectiontype === selectionType.value &&\n pList[i].selectionid === selection.value) {\n return;\n }\n }\n pList.push({\n \"selectiontype\": selectionType.value,\n \"selectionid\": selection.value,\n \"role\": \"viewer\"\n });\n // Add it to the form.\n participantAddToForm(selectionType.value, selection.value, 'viewer', true).then();\n // Update in the form.\n participantListUpdate(pList);\n};\n\n/**\n * Update selectable options when changing types\n *\n * @param {HTMLNode} currentTypeSelect\n */\nconst updateSelectionFromType = (currentTypeSelect) => {\n const createNewOption = (selectItem, label, value) => {\n const option = document.createElement('option');\n option.text = label;\n option.value = value;\n\n selectItem.add(option);\n };\n\n const participantData = JSON.parse(ELEMENT_SELECTOR.participantData().dataset.participantData);\n // Clear all selection items.\n const participantSelect = ELEMENT_SELECTOR.participantSelection();\n while (participantSelect.firstChild) {\n participantSelect.removeChild(participantSelect.firstChild);\n }\n // Add options depending on the selection.\n if (currentTypeSelect.selectedIndex !== -1) {\n const options = Object.values(participantData[currentTypeSelect.value].children);\n options.forEach(option => {\n createNewOption(participantSelect, option.name, option.id);\n });\n\n if (currentTypeSelect.value === 'all') {\n createNewOption(participantSelect, '---------------', 'all');\n participantSelect.disabled = true;\n } else {\n participantSelect.disabled = false;\n }\n }\n};\n"],"names":["ELEMENT_SELECTOR","document","querySelector","getElementsByName","getElementById","info","selectedType","instanceTypeProfiles","JSON","parse","dataset","profileTypes","profileType","instanceTypeDefault","selectedIndex","options","value","isFeatureEnabled","feature","features","indexOf","applyInstanceTypeProfile","addEventListener","e","target","currentTypeSelect","updateSelectionFromType","stopPropagation","preventDefault","participantAddFromCurrentSelection","participantListInit","showAll","showFieldset","id","show","node","style","display","showInput","ancestor","closest","showFormGroup","classList","remove","add","participantData","getParticipantList","forEach","participant","selectionTypeValue","selectiontype","selectionValue","selectionid","selectionRole","role","children","participantAddToForm","then","async","selectedRole","canRemove","roles","viewer","label","moderator","isselected","listTable","templateContext","name","Object","values","html","js","Templates","renderForPromise","newNode","appendNodeContents","participantListRoleUpdate","removeNode","participantRemove","exception","participantListUpdate","list","stringify","participantListValue","pList","participantListTable","i","length","splice","rows","deleteRow","type","participantListRoleSelection","selectionType","selection","push","createNewOption","selectItem","option","createElement","text","participantSelect","firstChild","removeChild","disabled"],"mappings":";;;;;;;0LA+BMA,uCACqB,IAAMC,SAASC,cAAc,kBADlDF,sCAEoB,IAAMC,SAASC,cAAc,wBAFjDF,iCAGe,IAAMC,SAASC,cAAc,2BAH5CF,iCAIe,IAAMC,SAASE,kBAAkB,gBAAgB,GAJhEH,kCAKgB,IAAMC,SAASG,eAAe,0BAL9CJ,0CAMwB,IAAMC,SAASE,kBAAkB,8CAA8C,GANvGH,sCAOoB,IAAMC,SAASE,kBAAkB,yCAAyC,GAP9FH,sCAQoB,IAAMC,SAASE,kBAAkB,6CAA6C,iBAUnFE,aACXC,aAAeN,yCACfO,qBAAuBC,KAAKC,MAAMT,wCAAwCU,QAAQC,kBAEpFC,YAAcP,KAAKQ,oBACF,OAAjBP,eAAyD,IAAhCA,aAAaQ,gBACtCF,YAAcN,aAAaS,QAAQT,aAAaQ,eAAeE,aAG7DC,iBAAmB,CAACL,YAAaM,WAEI,IADtBX,qBAAqBK,aAAaO,SAClCC,QAAQF,SAE7BG,yBAAyBT,YAAaK,kBAGtCX,aAAagB,iBAAiB,UAAWC,IACrCF,yBAAyBE,EAAEC,OAAOR,MAAOC,qBAG7CjB,4CAA4CsB,iBAAiB,UAAWC,UAC9DE,kBAAoBF,EAAEC,OAC5BE,wBAAwBD,sBAG5BzB,wCAAwCsB,iBAAiB,SAAUC,IAC/DA,EAAEI,kBACFJ,EAAEK,iBACFC,wCAGJC,6BASET,yBAA2B,CAACT,YAAaK,wBACvCc,QAAUd,iBAAiBL,YAAa,aACtCoB,aAAe,CAACC,GAAIC,cAEhBC,KAAOlC,SAASC,cAAc,IAAM+B,IACrCE,OAIDA,KAAKC,MAAMC,QADXH,KACqB,QAGJ,SAEnBI,UAAY,CAACL,GAAIC,cAEbC,KAAOlC,SAASC,cAAc,IAAM+B,OACrCE,UAGDI,SAAWJ,KAAKK,QAAQ,OAAOA,QAAQ,OAEvCD,SAASH,MAAMC,QADfH,KACyB,QAGJ,SAEvBO,cAAgB,CAACR,GAAIC,cAEjBC,KAAOlC,SAASC,cAAc,cAAgB+B,IAC/CE,OAGDD,KACAC,KAAKO,UAAUC,OAAO,UAG1BR,KAAKO,UAAUE,IAAI,YAGvBZ,aAAa,UAAWD,SACpBd,iBAAiBL,YAAa,aAClC0B,UAAU,YAAaP,SACnBd,iBAAiBL,YAAa,aAElCoB,aAAa,gBAAiBD,SAC1Bd,iBAAiBL,YAAa,mBAElC0B,UAAU,yBAA0BP,SAChCd,iBAAiBL,YAAa,mBAElCoB,aAAa,UAAWD,SACpBd,iBAAiBL,YAAa,SAElCoB,aAAa,iBAAkBD,SAC3Bd,iBAAiBL,YAAa,aAElCoB,aAAa,2BAA4BD,SACrCd,iBAAiBL,YAAa,0BAElCoB,aAAa,iBAAkBD,SAC3Bd,iBAAiBL,YAAa,gBAElCoB,aAAa,cAAeD,SACxBd,iBAAiBL,YAAa,aAElCoB,aAAa,uBAAwBD,SACjCd,iBAAiBL,YAAa,sBAElCoB,aAAa,kCAAmCD,SAC5Cd,iBAAiBL,YAAa,iCAElCoB,aAAa,aAAcD,SAAWd,iBAAiBL,YAAa,YAEpEoB,aAAa,yBAA0BD,SACnCd,iBAAiBL,YAAa,wBAElCoB,aAAa,sBAAuBD,SAChCd,iBAAiBL,YAAa,qBAElCoB,aAAa,8BAA+BD,SACxCd,iBAAiBL,YAAa,6BAElC6B,cAAc,4BAA6BV,SACvCd,iBAAiBL,YAAa,yBAElC6B,cAAc,4BAA6BV,SACvCd,iBAAiBL,YAAa,0BAMhCkB,oBAAsB,WAClBe,gBAAkBrC,KAAKC,MAAMT,mCAAmCU,QAAQmC,iBACtDC,qBACRC,SAAQC,oBACdC,mBAAqBD,YAAYE,cACjCC,eAAiBH,YAAYI,YAC7BC,cAAgBL,YAAYM,KACA,QAA9BN,YAAYE,oBAC4E,IAAjFL,gBAAgBG,YAAYE,eAAeK,SAASP,YAAYI,cAEvEI,qBAAqBP,mBAAoBE,eAAgBE,eAAe,GAAMI,WAcpFD,qBAAuBE,MAAMT,mBAAoBE,eAAgBQ,aAAcC,mBAC3Ef,gBAAkBrC,KAAKC,MAAMT,mCAAmCU,QAAQmC,qBAG1EgB,MAAQ,CACRC,OAAQ,IAAO,SAAUC,YAHP,kBAAU,6CAA8C,wBAI1EC,UAAW,IAAO,YAAaD,YAHV,kBAAU,gDAAiD,yBAKpFF,MAAMF,cAAcM,YAAa,YAEvBC,UAAYjE,SAASC,cAAc,iCACnCiE,gBAAkB,oBACElB,kCACJE,+BACCN,gBAAgBI,oBAAoBmB,sBAE3B,QAAvBnB,mBACGJ,gBAAgBI,oBAAoBM,SAASJ,gBAAgBiB,KAAO,WACnEC,OAAOC,OAAOT,iBACVD,YAEXW,KAACA,KAADC,GAAOA,UAAYC,mBAAUC,iBAAiB,2CAA4CP,iBAC1FQ,QAAUF,mBAAUG,mBAAmBV,UAAWK,KAAMC,IAAI,GAClEG,QAAQzE,cAAc,uBAAuBoB,iBAAiB,UAAU,KACpEuD,0BAA0B5B,mBAAoBE,yBAG5C2B,WAAaH,QAAQzE,cAAc,kBACrC4E,YACAA,WACKxD,iBAAiB,SAAS,KACvByD,kBAAkB9B,mBAAoBE,mBAIpD,MAAO5B,yBACQyD,UAAUzD,KAYzB0D,sBAAyBC,OACHlF,mCACRgB,MAAQR,KAAK2E,UAAUD,OAOrCpC,mBAAqB,WACjBsC,qBAAuBpF,mCAAmCgB,aAC5DoE,qBACO5E,KAAKC,MAAM2E,sBAEf,IASLL,kBAAoB,CAAC9B,mBAAoBE,wBACrCkC,MAAQvC,qBACRb,GAAK,uBAAyBgB,mBAAqB,IAAME,eACzDmC,qBAAuBtF,oCACvBoD,YAAkC,KAAnBD,eAAwB,KAAOA,mBAC/C,IAAIoC,EAAI,EAAGA,EAAIF,MAAMG,OAAQD,IAC1BF,MAAME,GAAGrC,gBAAkBD,oBAC3BoC,MAAME,GAAGnC,cAAgBA,aACzBiC,MAAMI,OAAOF,EAAG,OAInB,IAAIA,EAAI,EAAGA,EAAID,qBAAqBI,KAAKF,OAAQD,IAC9CD,qBAAqBI,KAAKH,GAAGtD,KAAOA,IACpCqD,qBAAqBK,UAAUJ,GAIvCN,sBAAsBI,QASpBR,0BAA4B,CAACe,KAAM3D,YAE/B4D,6BAA+B5F,SAASC,6CAAsC0F,iBAAQ3D,4BACtFoD,MAAQvC,yBAET,IAAIyC,EAAI,EAAGA,EAAIF,MAAMG,OAAQD,IAC1BF,MAAME,GAAGrC,gBAAkB0C,MAAQP,MAAME,GAAGnC,cAAgBnB,KAC5DoD,MAAME,GAAGjC,KAAOuC,6BAA6B7E,OAIrDiE,sBAAsBI,QAMpBxD,mCAAqC,SACnCiE,cAAgB9F,4CAChB+F,UAAY/F,8CACVqF,MAAQvC,yBAET,IAAIyC,EAAI,EAAGA,EAAIF,MAAMG,OAAQD,OAC1BF,MAAME,GAAGrC,gBAAkB4C,cAAc9E,OACzCqE,MAAME,GAAGnC,cAAgB2C,UAAU/E,aAI3CqE,MAAMW,KAAK,eACUF,cAAc9E,kBAChB+E,UAAU/E,WACjB,WAGZwC,qBAAqBsC,cAAc9E,MAAO+E,UAAU/E,MAAO,UAAU,GAAMyC,OAE3EwB,sBAAsBI,QAQpB3D,wBAA2BD,0BACvBwE,gBAAkB,CAACC,WAAYnC,MAAO/C,eAClCmF,OAASlG,SAASmG,cAAc,UACtCD,OAAOE,KAAOtC,MACdoC,OAAOnF,MAAQA,MAEfkF,WAAWtD,IAAIuD,SAGbtD,gBAAkBrC,KAAKC,MAAMT,mCAAmCU,QAAQmC,iBAExEyD,kBAAoBtG,6CACnBsG,kBAAkBC,YACrBD,kBAAkBE,YAAYF,kBAAkBC,gBAGX,IAArC9E,kBAAkBX,cAAsB,CACxBuD,OAAOC,OAAOzB,gBAAgBpB,kBAAkBT,OAAOuC,UAC/DR,SAAQoD,SACZF,gBAAgBK,kBAAmBH,OAAO/B,KAAM+B,OAAOlE,OAG3B,QAA5BR,kBAAkBT,OAClBiF,gBAAgBK,kBAAmB,kBAAmB,OACtDA,kBAAkBG,UAAW,GAE7BH,kBAAkBG,UAAW"}PK\0G66amd/build/rooms.min.js.mapnu[{"version":3,"file":"rooms.min.js","sources":["../src/rooms.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 * JS actions for the rooms page for mod_bigbluebuttonbn.\n *\n * @module mod_bigbluebuttonbn/rooms\n * @copyright 2021 Blindside Networks Inc\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport './actions';\nimport * as repository from './repository';\nimport * as roomUpdater from './roomupdater';\nimport {\n exception as displayException,\n fetchNotifications,\n} from 'core/notification';\nimport Pending from 'core/pending';\nimport {getString} from 'core/str';\nimport {add as addToast} from 'core/toast';\nimport {eventTypes, notifyCurrentSessionEnded} from './events';\n\n/**\n * Init the room\n *\n * @param {Number} bigbluebuttonbnid bigblubeutton identifier\n * @param {Number} pollInterval poll interval in miliseconds\n */\nexport const init = (bigbluebuttonbnid, pollInterval) => {\n const completionElement = document.querySelector('a[href*=completion_validate]');\n if (completionElement) {\n completionElement.addEventListener(\"click\", event => {\n event.preventDefault();\n\n const pendingPromise = new Pending('mod_bigbluebuttonbn/completion:validate');\n\n repository.completionValidate(bigbluebuttonbnid)\n .then(() => getString('completionvalidatestatetriggered', 'mod_bigbluebuttonbn'))\n .then(str => addToast(str))\n .then(() => pendingPromise.resolve())\n .catch(displayException);\n });\n }\n\n document.addEventListener('click', e => {\n const joinButton = e.target.closest('[data-action=\"join\"]');\n if (joinButton) {\n window.open(joinButton.href, 'bigbluebutton_conference');\n e.preventDefault();\n // Gives the user a bit of time to go into the meeting before polling the room.\n setTimeout(() => {\n roomUpdater.updateRoom(true);\n }, pollInterval);\n }\n });\n\n document.addEventListener(eventTypes.sessionEnded, () => {\n roomUpdater.stop();\n roomUpdater.updateRoom();\n fetchNotifications();\n });\n\n window.addEventListener(eventTypes.currentSessionEnded, () => {\n roomUpdater.stop();\n roomUpdater.updateRoom();\n fetchNotifications();\n });\n // Room update.\n roomUpdater.start(pollInterval);\n};\n\n/**\n * Auto close child windows when clicking the End meeting button.\n * @param {Number} closeDelay time to wait in miliseconds before closing the window\n */\nexport const setupWindowAutoClose = (closeDelay = 2000) => {\n notifyCurrentSessionEnded(window.opener);\n window.addEventListener('onbeforeunload', () => {\n window.opener.setTimeout(() => {\n roomUpdater.updateRoom(true);\n }, closeDelay);\n },\n {\n once: true\n });\n window.close(); // This does not work as scripts can only close windows that are opened by themselves.\n};\n"],"names":["bigbluebuttonbnid","pollInterval","completionElement","document","querySelector","addEventListener","event","preventDefault","pendingPromise","Pending","repository","completionValidate","then","str","resolve","catch","displayException","e","joinButton","target","closest","window","open","href","setTimeout","roomUpdater","updateRoom","eventTypes","sessionEnded","stop","currentSessionEnded","start","closeDelay","opener","once","close"],"mappings":";;;;;;;wRAyCoB,CAACA,kBAAmBC,sBAC9BC,kBAAoBC,SAASC,cAAc,gCAC7CF,mBACAA,kBAAkBG,iBAAiB,SAASC,QACxCA,MAAMC,uBAEAC,eAAiB,IAAIC,iBAAQ,2CAEnCC,WAAWC,mBAAmBX,mBACzBY,MAAK,KAAM,kBAAU,mCAAoC,yBACzDA,MAAKC,MAAO,cAASA,OACrBD,MAAK,IAAMJ,eAAeM,YAC1BC,MAAMC,4BAInBb,SAASE,iBAAiB,SAASY,UACzBC,WAAaD,EAAEE,OAAOC,QAAQ,wBAChCF,aACAG,OAAOC,KAAKJ,WAAWK,KAAM,4BAC7BN,EAAEV,iBAEFiB,YAAW,KACPC,YAAYC,YAAW,KACxBzB,kBAIXE,SAASE,iBAAiBsB,mBAAWC,cAAc,KAC/CH,YAAYI,OACZJ,YAAYC,uDAIhBL,OAAOhB,iBAAiBsB,mBAAWG,qBAAqB,KACpDL,YAAYI,OACZJ,YAAYC,uDAIhBD,YAAYM,MAAM9B,6CAOc,eAAC+B,kEAAa,0CACpBX,OAAOY,QACjCZ,OAAOhB,iBAAiB,kBAAkB,KAClCgB,OAAOY,OAAOT,YAAW,KACrBC,YAAYC,YAAW,KACxBM,cAEP,CACIE,MAAM,IAEdb,OAAOc"}PK\fww UPGRADING.mdnu[# mod_bigbluebuttonbn Upgrade notes ## 4.5 ### Added - Added new `meeting_info` value to show presentation file on BBB activity page For more information see [MDL-82520](https://tracker.moodle.org/browse/MDL-82520) - The `broker::process_meeting_events()` method has been extended to call the `::process_action()` method implemented by plugins. For more information see [MDL-82872](https://tracker.moodle.org/browse/MDL-82872) ### Removed - Mobile support via plugin has been removed as it is now natively available in the Moodle App. For more information see [MDL-82447](https://tracker.moodle.org/browse/MDL-82447) PK\&cli/migrate_recordings.phpnu[. /** * Recordings CLI Migration script. * * @package mod_bigbluebuttonbn * @copyright 2022 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent [at] call-learning [dt] fr) */ use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\logger; use mod_bigbluebuttonbn\task\upgrade_recordings_task; define('CLI_SCRIPT', true); require(__DIR__ . '/../../../config.php'); global $CFG; require_once($CFG->libdir . '/clilib.php'); // Now get cli options. list($options, $unrecognized) = cli_get_params( [ 'help' => false, 'courseid' => 0, 'bigbluebuttonid' => 0, 'forcemigrate' => false ], [ 'h' => 'help', 'c' => 'courseid', 'b' => 'bigbluebuttoncmid', 'f' => 'forcemigrate' ] ); if ($unrecognized) { $unrecognized = implode("\n ", $unrecognized); cli_error(get_string('cliunknowoption', 'admin', $unrecognized)); } if ($options['help']) { $help = "Execute / Rexecute migration for recordings. Sometimes when the remote BigBlueButton server is temporarily not accessible and we upgrade to the new way of storing recordings some activities end up without recordings. This script will allow to go through each meeting and fetch all recordings that have not yet been fetched from the remote BigblueButton server. Options: -h, --help Print out this help -c, --courseid Course identifier (id) in which we look for BigblueButton activities and recordings. If not specified we check every single BigblueButton activity. -b, --bigbluebuttoncmid Identifier for the bigbluebutton activity we would like to specifically retrieve recordings for. If not specified we check every single BigblueButton activity (scoped or not to a course depending on -c option). -f,--forcemigrate Force the 'remigration' of recordings, so even if a recording has been marked as migrated in the logs we still fetch the data from the Bigbluebutton server. Example: \$ sudo -u www-data /usr/bin/php mod/bigbluebuttonbn/cli/migrate_recordings.php -c=4 -f=1 "; echo $help; die; } global $DB; $bbcms = []; if (!empty($options['courseid'])) { $courseid = $options['courseid']; $modinfos = get_fast_modinfo($courseid)->get_instances_of('bigbluebuttonbn'); $bbcms = array_values($modinfos); } else if (!empty($options['bigbluebuttoncmid'])) { [$course, $bbcm] = get_course_and_cm_from_cmid($options['bigbluebuttoncmid']); $bbcms = [$bbcm]; } else { // All bigbluebutton activities. foreach ($DB->get_fieldset_select('bigbluebuttonbn', 'id', '') as $bbid) { [$course, $bbcm] = get_course_and_cm_from_instance($bbid, 'bigbluebuttonbn'); array_push($bbcms, $bbcm); } } foreach ($bbcms as $bbcm) { $instance = instance::get_from_cmid($bbcm->id); $adhoctask = new upgrade_recordings_task(); cli_writeln("Processing BigbluebButton {$instance->get_meeting_name()}(id:{$instance->get_instance_id()})," . " in course {$bbcm->get_course()->fullname}(id:{$bbcm->get_course()->id})...."); if ($options['forcemigrate']) { // We set the value of the log back to the original value so it get evalated once again. $DB->set_field('bigbluebuttonbn_logs', 'log', logger::EVENT_IMPORT, [ 'courseid' => $instance->get_course_id(), 'bigbluebuttonbnid' => $instance->get_instance_id(), 'log' => logger::EVENT_IMPORT_MIGRATED ]); $DB->set_field('bigbluebuttonbn_logs', 'log', logger::EVENT_CREATE, [ 'courseid' => $instance->get_course_id(), 'bigbluebuttonbnid' => $instance->get_instance_id(), 'log' => logger::EVENT_CREATE_MIGRATED ]); } $meetingids = $DB->get_fieldset_sql( 'SELECT DISTINCT meetingid FROM {bigbluebuttonbn_logs} WHERE log = :createlog OR log = :importlog', [ 'importlog' => logger::EVENT_IMPORT, 'createlog' => logger::EVENT_CREATE ] ); if (empty($meetingids)) { cli_writeln("\t->No meetings logs found..."); } foreach ($meetingids as $mid) { cli_write("\t->Processing meeting ID {$mid} ...recordings"); $adhoctask->set_custom_data((object) [ 'meetingid' => $mid, 'isimported' => false ]); $adhoctask->execute(); cli_writeln("...imported recordings...."); $adhoctask->set_custom_data((object) [ 'meetingid' => $mid, 'isimported' => true ]); $adhoctask->execute(); } } PK\-`hll#cli/update_dismissed_recordings.phpnu[. /** * Recordings CLI Migration script. * * @package mod_bigbluebuttonbn * @copyright 2022 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent [at] call-learning [dt] fr) */ use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\recording; define('CLI_SCRIPT', true); require(__DIR__ . '/../../../config.php'); global $CFG; require_once($CFG->libdir . '/clilib.php'); // Now get cli options. list($options, $unrecognized) = cli_get_params( [ 'help' => false, 'courseid' => 0, 'bigbluebuttonid' => 0, 'run' => false ], [ 'h' => 'help', 'c' => 'courseid', 'b' => 'bigbluebuttoncmid', 'r' => 'run' ] ); if ($unrecognized) { $unrecognized = implode("\n ", $unrecognized); cli_error(get_string('cliunknowoption', 'admin', $unrecognized)); } if ($options['help']) { $help = "Check for dismissed recording and see if they appear on the server. Sometimes when the remote BigBlueButton server is temporarily not accessible it has been seen that the recordings are set to 'dismissed' status. For now this is a workaround until we refactor slightly the recording API. Options: -h, --help Print out this help -c, --courseid Course identifier (id) in which we look for BigBlueButton activities and recordings. If not specified we check every single BigBlueButton activity. -b, --bigbluebuttoncmid Identifier for the BigBlueButton activity we would like to specifically retrieve recordings for. If not specified we check every single BigBlueButton activity (scoped or not to a course depending on -c option). -r,--run If false (default, just display information. By default we just display the information. Example: \$ sudo -u www-data /usr/bin/php mod/bigbluebuttonbn/cli/update_dismissed_recordings.php -c=4 -r=1 "; echo $help; die; } $bbcms = []; if (!empty($options['courseid'])) { $courseid = $options['courseid']; $modinfos = get_fast_modinfo($courseid)->get_instances_of('bigbluebuttonbn'); $bbcms = array_values($modinfos); } else if (!empty($options['bigbluebuttoncmid'])) { [$course, $bbcm] = get_course_and_cm_from_cmid($options['bigbluebuttoncmid']); $bbcms = [$bbcm]; } else { // All bigbluebutton activities. foreach ($DB->get_fieldset_select('bigbluebuttonbn', 'id', '') as $bbid) { [$course, $bbcm] = get_course_and_cm_from_instance($bbid, 'bigbluebuttonbn'); array_push($bbcms, $bbcm); } } foreach ($bbcms as $bbcm) { $instance = instance::get_from_cmid($bbcm->id); cli_writeln("Processing BigBlueButton {$instance->get_meeting_name()}(id:{$instance->get_instance_id()})," . " in course {$bbcm->get_course()->fullname}(id:{$bbcm->get_course()->id})...."); $recordings = recording::get_records(['status' => recording::RECORDING_STATUS_DISMISSED, 'bigbluebuttonbnid' => $instance->get_instance_id()]); $recordingkeys = array_map(function($rec) { return $rec->get('recordingid'); }, $recordings); $recordingmeta = \mod_bigbluebuttonbn\local\proxy\recording_proxy::fetch_recordings($recordingkeys); if (empty($recordings)) { cli_writeln("\t->No recordings found ..."); } else { foreach ($recordings as $recording) { if (!empty($recordingmeta[$recording->get('recordingid')])) { $recordingwithmeta = new recording(0, $recording->to_record(), $recordingmeta[$recording->get('recordingid')]); cli_writeln("\t-> Recording data found for " . $recordingwithmeta->get('name') . ' ID:' . $recordingwithmeta->get('recordingid')); if ($options['run']) { $recordingwithmeta->set('status', recording::RECORDING_STATUS_PROCESSED); $recordingwithmeta->save(); cli_writeln("\t\t-> Metadata and status updated..."); } } else { cli_writeln("\t-> No recording data found for " . $recording->get('recordingid')); } } } } PK\l^F"tests/fixtures/bbpresentation.pptxnu[PKhp,S _rels/.relsMKA ܻVED3̄L z̛7!杤99iZk(4:rx\]Z+ez*?QdT7CZGѿHm[ /f}Xs`")T,ZtR(#!sd 0{z-RK^tRJu%dftFƷ  _BPK =@PKhp,SdocProps/app.xmlOk0 8 #⸬ !VZVJnIzlXS &]OWz>) zrD6E TdK=9"5ITeeg 2&-al!m꺥 8 W #O_r_!8#8r(̠-:=`{wmfn^Ϯۧn`/|ܰڨa-5PKڝzPKhp,SdocProps/core.xmlN0E|E}<Q⊇";LSXRiu3̌,Q(ڊ˺D/Yxch%hMETZ U-849S%ZYr [ACM҉V7ԺPXQEkiOpVRC5YZx@0h@Z(ݘrl)8kmhn6V~Ԑ Rəj  kvw!ia.Iy؟[M:sia/$\,R'oS*5vPLn?R'G# _YÆwdv]'0ۏ4l@nK/)8PKVlPKhp,Sppt/_rels/presentation.xml.relsœ0>EM[uFGŒr 'N4qi*Tv܇pq54?3 0ɶd`e [Wԡ/>br[gU~I`@_;tp,qǝGc ZJǴozYc."PKxqHPKhp,S ppt/slides/_rels/slide2.xml.rels 0>EػI "M{IdTۛc <7L|DvCI hSmUDʑ8jIØ0FD<. BĀP' 7L*U%nxur6hӏ '.7aā}:\,ePKdb&PKhp,S ppt/slides/_rels/slide1.xml.rels 0>EػI+""M{IdTۛc <7L|DvCI hSmUDʑ8jIØ0FD<. BĀP' 7L*U%nxur6hӏ '.7aā}*4w{fpPK& _'PKhp,Sppt/slides/slide1.xmlVmO0_a{q+U[lIVQ\i,6-~NP(} ;;hŝFqvňkfrc3s*cO>C:evHg%W5)`$w@$n(*4!qu1E!bRVLoCʎ)G̨X5"eݼv@;$1EvB2s!Eؤd9ΣW7MPDC":HSۍmGWbDjt.ڎ@1;jgWYe (fфx8k-Q Sw߬ p+hҬ#db:iLO2<"1ɜuY 7 EagG=]}2mLZk!eZC4cV& io'j]#F7լ4Шs\_,)DIŽIr|Ҧ|J QVőj4+0x'v8>ilжCߚfz滋[jXMQ: H{~Ulx!w~et4pɣ[{)1t-ˋd yfNXZ|k-bRΡ_ zQy7 PKƄ PKhp,Sppt/slides/slide2.xmlVmo0_a;5aTm7IӆJ!V6PدIhԵTjOL/vJ-w^=Y#)^EoTTgx=ډO W! !U\Qf,0Wh[U !QThܬ/Yo\ S % , (A WCJ<%3ޔ@kF$\W5 #^R~c{i\H<EػIۃ4EH}l`CE[qvfam˜wjZA'2NsnpDlB"%sX3Z3hE.2j|kt&8 Oxi,-FU`EԘ9PoB -\}PKSc&PKhp,S-ppt/slideLayouts/_rels/slideLayout23.xml.rels 0>EػIۃ4EH}l`CE[qvfam˜wjZA'2NsnpDlB"%sX3Z3hE.2j|kt&8 Oxi,-FU`EԘ9PoB -\}PKSc&PKhp,S-ppt/slideLayouts/_rels/slideLayout22.xml.rels 0>EػIۃ4EH}l`CE[qvfam˜wjZA'2NsnpDlB"%sX3Z3hE.2j|kt&8 Oxi,-FU`EԘ9PoB -\}PKSc&PKhp,S-ppt/slideLayouts/_rels/slideLayout21.xml.rels 0>EػIۃ4EH}l`CE[qvfam˜wjZA'2NsnpDlB"%sX3Z3hE.2j|kt&8 Oxi,-FU`EԘ9PoB -\}PKSc&PKhp,S-ppt/slideLayouts/_rels/slideLayout20.xml.rels 0>EػIۃ4EH}l`CE[qvfam˜wjZA'2NsnpDlB"%sX3Z3hE.2j|kt&8 Oxi,-FU`EԘ9PoB -\}PKSc&PKhp,S-ppt/slideLayouts/_rels/slideLayout19.xml.rels 0>EػIۃ4EH}l`CE[qvfam˜wjZA'2NsnpDlB"%sX3Z3hE.2j|kt&8 Oxi,-FU`EԘ9PoB -\}PKSc&PKhp,S,ppt/slideLayouts/_rels/slideLayout6.xml.rels 0>Eػփ"B^>@Hm!oo-x8;0mgwj^#6np/tYI L9#bRY3he.2zȑpWU{K4+&봀X Qtif*SX2pB5/e.6_PKCV&PKhp,S,ppt/slideLayouts/_rels/slideLayout5.xml.rels 0>Eػփ"B^>@Hm!oo-x8;0mgwj^#6np/tYI L9#bRY3he.2zȑpWU{K4+&봀X Qtif*SX2pB5/e.6_PKCV&PKhp,S-ppt/slideLayouts/_rels/slideLayout14.xml.rels 0>EػIۃ4EH}l`CE[qvfam˜wjZA'2NsnpDlB"%sX3Z3hE.2j|kt&8 Oxi,-FU`EԘ9PoB -\}PKSc&PKhp,S-ppt/slideLayouts/_rels/slideLayout13.xml.rels 0>EػIۃ4EH}l`CE[qvfam˜wjZA'2NsnpDlB"%sX3Z3hE.2j|kt&8 Oxi,-FU`EԘ9PoB -\}PKSc&PKhp,S-ppt/slideLayouts/_rels/slideLayout18.xml.rels 0>EػIۃ4EH}l`CE[qvfam˜wjZA'2NsnpDlB"%sX3Z3hE.2j|kt&8 Oxi,-FU`EԘ9PoB -\}PKSc&PKhp,S,ppt/slideLayouts/_rels/slideLayout4.xml.rels 0>Eػփ"B^>@Hm!oo-x8;0mgwj^#6np/tYI L9#bRY3he.2zȑpWU{K4+&봀X Qtif*SX2pB5/e.6_PKCV&PKhp,S-ppt/slideLayouts/_rels/slideLayout12.xml.rels 0>Eػփ"B^>@Hm!oo-x8;0mgwj^#6np/tYI L9#bRY3he.2zȑpWU{K4+&봀X Qtif*SX2pB5/e.6_PKCV&PKhp,S-ppt/slideLayouts/_rels/slideLayout17.xml.rels 0>EػIۃ4EH}l`CE[qvfam˜wjZA'2NsnpDlB"%sX3Z3hE.2j|kt&8 Oxi,-FU`EԘ9PoB -\}PKSc&PKhp,S,ppt/slideLayouts/_rels/slideLayout3.xml.rels 0>Eػփ"B^>@Hm!oo-x8;0mgwj^#6np/tYI L9#bRY3he.2zȑpWU{K4+&봀X Qtif*SX2pB5/e.6_PKCV&PKhp,S-ppt/slideLayouts/_rels/slideLayout11.xml.rels 0>Eػփ"B^>@Hm!oo-x8;0mgwj^#6np/tYI L9#bRY3he.2zȑpWU{K4+&봀X Qtif*SX2pB5/e.6_PKCV&PKhp,S,ppt/slideLayouts/_rels/slideLayout9.xml.rels 0>Eػփ"B^>@Hm!oo-x8;0mgwj^#6np/tYI L9#bRY3he.2zȑpWU{K4+&봀X Qtif*SX2pB5/e.6_PKCV&PKhp,S-ppt/slideLayouts/_rels/slideLayout10.xml.rels 0>Eػփ"B^>@Hm!oo-x8;0mgwj^#6np/tYI L9#bRY3he.2zȑpWU{K4+&봀X Qtif*SX2pB5/e.6_PKCV&PKhp,S,ppt/slideLayouts/_rels/slideLayout7.xml.rels 0>Eػփ"B^>@Hm!oo-x8;0mgwj^#6np/tYI L9#bRY3he.2zȑpWU{K4+&봀X Qtif*SX2pB5/e.6_PKCV&PKhp,S,ppt/slideLayouts/_rels/slideLayout1.xml.rels 0>Eػփ"B^>@Hm!oo-x8;0mgwj^#6np/tYI L9#bRY3he.2zȑpWU{K4+&봀X Qtif*SX2pB5/e.6_PKCV&PKhp,S-ppt/slideLayouts/_rels/slideLayout15.xml.rels 0>EػIۃ4EH}l`CE[qvfam˜wjZA'2NsnpDlB"%sX3Z3hE.2j|kt&8 Oxi,-FU`EԘ9PoB -\}PKSc&PKhp,S,ppt/slideLayouts/_rels/slideLayout8.xml.rels 0>Eػփ"B^>@Hm!oo-x8;0mgwj^#6np/tYI L9#bRY3he.2zȑpWU{K4+&봀X Qtif*SX2pB5/e.6_PKCV&PKhp,S,ppt/slideLayouts/_rels/slideLayout2.xml.rels 0>Eػփ"B^>@Hm!oo-x8;0mgwj^#6np/tYI L9#bRY3he.2zȑpWU{K4+&봀X Qtif*SX2pB5/e.6_PKCV&PKhp,S-ppt/slideLayouts/_rels/slideLayout16.xml.rels 0>EػIۃ4EH}l`CE[qvfam˜wjZA'2NsnpDlB"%sX3Z3hE.2j|kt&8 Oxi,-FU`EԘ9PoB -\}PKSc&PKhp,S!ppt/slideLayouts/slideLayout6.xmlTao _]Eq[I5Ǭ7ޯ߁q͑;`vk$juBggcbjR+;|=73S'#PnJs\{o8V3mXmC=,톔>u#d<$  |sJq婇r"lWOb=]kʟ1$*bFȖ䈤a4P5#4L!]l#.~+au-2  \"וH}r,$%*i*;};w'glpY *VRBʸFZR `$8DqT02VT9-Ħ)q_eOf{ j.PKF8sPKhp,S!ppt/slideLayouts/slideLayout5.xmlTmo _g[Fqu[I5 (Ax~j|^WZFW4O(FBmN=,G(񁛂kkdN[ͼ.nyk 3*7c̋J܏c–l:\+C||Kxr_#زTB~b_#WGR'R_)J>૕m=sQ*e󾲏C2=#[9n(Bڣ$w/-%IIhЙnX^!Hňw eLsnV ?UDg>mc^#d|5>;P?͓#m㗝G!хkqQ3_T0gg.nfDAB~@2d`]G[+vD}Nq]{TQId.xr2ؑǁ7$9)~AHõ-ڈ?OZ` 7x7i c?-ULǓjJ-bŁ#&PVhy/olx;o( i:Yi o*J4pDij{bҮ9]=IxKqw}NѠ ;D]7<Ë?PKY&U.kPKhp,S!ppt/slideLayouts/slideLayout4.xmlVQo0~߯P]Ri4{|>}3c/5B12;&ij%Wԟ5* t R:$~H9qu=bJ0Ѱq@RX_RvO 7U8aFPĚY#Z?+{biLQZ`ZhRmj0=S(qjXfx dsY"Ml܉ [titf;yt}OLZekA0yЦlz1!!d4:TNݯp69n79M@qc~d>[~3„JN5u=Ig#KPMXAưG5_ { .86q|ޑn؇'1КdGYñ·knFgQ;tL.$Uz”Y/#hv: {^#1 N)uPIV-kA[xzYX̓FJHnQ\Jj*(=-a6i*8sv9͜A0U*qqC:)=!1A "Sv6:"lpȩ i0|2?/Ҝ+QBKdwo?oPKLPKhp,S!ppt/slideLayouts/slideLayout1.xmln0E {pڪH>6U8/MH΃VZfwfNVU0:4ˆkfr)\L0rJy"6fQ@hۄJua [=dEDQPoמ%\1E!2lԕº3 32h+d6(v1EaFHF6F Bpl%s cVR 9ە^24x"Iu(X篹P02l98z|څĊ\[]|"Wv"ն3|g:*Vj8⫸ҟ^]5T>* Tpwu Woh:sV/  RENaRƁ]Hj*(>-Ga"QqW /28VT94]oC U)y=)Av|ATVkR),"XR";473}ʧPKOjPKhp,S!ppt/slideLayouts/slideLayout7.xmlWQO0~߯$Vm&QQ\i=۲$URrϟrJ'R3+98BLRs9Do/ZYGdNlN_gE~EVj@H#hal錕($ʔLqn=@q%2}k,8wD웢*D3CHI( w@UYCaъ$+9z HR}+`jHfw^JCܽ99#4 O!TE$)a;+$ hka[rqiX՗s]CFCmkZ.}\O#eaʇqJrWW!Z-]tv~:> gфx8kDuFPCrV pER5_pgM}5kjo`kv W`6pn ƍ^hc%S% 2zY\YW6.!"[g*_y <Si  pTs ^SᄘB 2Cnz*owi]@ >3k%~DJ5V8* .D\ U曌 U)ƃh24&ҢQ JWo\GYQ8jTwҷ5zJʲn֩難JF|kP{Ƭi)wiݴt)kw޹=}iSL.1e1eYeci ?7qZ5@PKwz PKhp,S!ppt/slideLayouts/slideLayout8.xmlWN"1}߯h #Ytڦ-{ۙQ5Kܶ{Oׅ@+f,WrG1IUl\6:Ȝ% 0uϊl!G99+=RI*S]3ù!]4m\.j>d62鈃pC@t)0@kR>+85ʪ;VhZ+>~ HRC+Y,u05$q&!P=ݝ|rn2 hc#I p'PCʏ\Ƽ%WWF Q< { 1Oe;r2{魧x`y,[;Dioo^\O?;,o%guKTL93(ҭ\AZхER5_pUϊ.J@ ιw)^ɩQVZ2hq{9 HIRXIBSC1ܭCyrk s*y"4KtND$)abʝ`QΔt@R SØ/}96ƌ:lkZ.7 .08h5@kW!Z-}t~h<3o%hRU5 QIaj,e_ȁ+*¥7Ix~Hw=oYU~FYo; &#׬{zopycFvH,/\qSgwG|6pm5FDҹ? [HiTm*S(Acb5@gA0k8~AjxZ? "+a9" ,;-pllSFa4Q Ci(t 7YVTnp#GuOPk=ŽvkA%o[*)_&!# +y--uiʹtwOLp0i;oyӽkk o~ngj5PKNn PKhp,S"ppt/slideLayouts/slideLayout24.xmln0VGMҐ[IՆJc ±-PIJij+$D8Rg낣&b'1Ae0s{X"2¥``Lp64P}ókK@B>skUcC D*&`l*uA,\4NpArTK+ XHIDDDN9e$]UhƽܧOjiԞPYTRX;̽Z]WyH#RЗ, zT-T4nT/D,]+Nx)6}F ‚;~ܨ[͘kՕV5#TnX$VQ}+=Fu۵ScN(!y4 A +gPta`DwVU+LޚwTMY][!>#Sc2۸`)Z [7ҺAKxS !.mJ_DN= 6խ‚#2knr[F<9z^pV?*7v܎"|UPa0KMaXKt_TYVyjop3@M@E-k"d9JQa6:٨C!퀌szsV{4+)ܐ?] ]\.z~zme1QE) 0@k{R+95ʪPUPX?hZޭ+<$)c +Y,u 05q&!нB-e;BG8C3TH&3Jѩ ~ cޒszbƨ~򟫉A߻/4=ꧽZPI[𚊪dd )>GH1iJA_JKGtjZ괻G1O1)} S&iw>-u!7(&vgN5|PKIPKhp,S"ppt/slideLayouts/slideLayout22.xmlVmO0_a1ڢFE8NoݮݯI(!@Jپ{|˃ӵhŝFqvňkfJgCcz9K*C ln2 оOxٜ+* 0t3R: $y{D7)qubJ0ٰq@\XR{sƛ*0(b/ -֕}xHS S-)a(l,t?5FI l"KGFh۩3&zz7׮5BkA5􋴅6T٦I9t9t*i<*׹7ʼOω$Yio(/ifŹthE%ac-0:u,hBGcg~nJܐVJpT%zN*; *c j92!G/R9Bp% kiOKoAK^H}PK*u PKhp,S"ppt/slideLayouts/slideLayout21.xmlWao0_a4jnR~ؖm($VUmnRDݻ'R%3+98BLRUp9D?&N# BI6F'}ݳ$kp A4wN0tJbf֦ʔpa-@q%2SaIG<DMϊ.J@ ιw)%^ɩQVMUe eF+Вx9 HIRXIBSC19ܭCyrk s*y"4KO!tE$)ab`QΔt@R Ø!/}92ƌ:lkZ.7} .jjʻqNjoCZx:g>JWѤx8kڝ }U@ܪX$P|ϣ"ogW-99d!M+95ʪPUPX{f-no$)>ɸnҋ[kd>W7]f q4ұȑ$% L ](!GI VO cޒ+zdnrd=r'a spp>ݍV)aEAb^=Vjُ#V@>&[YIFPSrV p"/8Ggu՝/fWM19D!ӆM<|D^tۍhcS% 2zY^[W6.!"[V*_{ @%m4k13? [Hup6N) 1PAmW-v!gf5V/@@T`=4H>) NW%GKM!01AeV0}wX"2¥``Lp:2P}ók B>3kUcCg$H*&`-$z3M8.I!z:d0apC@d}tQVfV(R<+ =L+Yx(+:~ HRC+/T 0$k^ZA콼I d%G*J'BcJ8I>|n+kPKd PKhp,S"ppt/slideLayouts/slideLayout18.xmlTmo _/[8֭ݤh`cV Y_MvS5G=̮&; ^YdL4–lrc}3@ܔ\[#sJOfnuOyNܔ1/jpb4,4<6 &k2wow 4,5x *%g+ ru$ u"r_Xop"lS1gIpV%l|Wv?$#F֍a3*=JBpzygtKIv)V$7FM$Ȓ>wk2Zfw n0's@Tzʞ#3!_}:yP>)6(6ADE}|Q;ÌJNu P痃RKͅju)d +<1"=At"ƷmVA2|O ?wz1'ЛlD!58ޟf`">JېhOnC $lFdoH3> 荈66J]r*$n%~|EJ⫕m=sI*eӾ!9;!{9lwnY+Bڣ$w/-%IIhЙ9nX^cQ$bĻH-܁[I?bfD'O;qټ\O%A>xpeQDtaV\Tշ3IA[0?u~9(\VHpocQ/1]u= |Ϝ>b#)Лl~$\M;8NښD# Eϛ{:IEk>y7l4vZJ4pDij{bҮ9]IxKnp}NѠ ;D]7n#PnHx؂i mKaj$KI:vJJ*s+O={>D`f*eu a]JI^)N鲆"FrkhI{HɞSXIreZiLHᷱ<@~aikXIzf2Gp#Qk偞hwrFj՚ c=HCֻy .ᦰ<h3Ơm'VUfvWOƳŗ#^B&[Y35LM%e9(ʭp!/8{mMCkHj Od==0h7dc_.Q,hʵq:96xfzw"׶f5BǞ-6N))PIV5nNZI8XJ(2N<;l칅#EŮ1eͨrhj5:/]UH Ay A>_d,ndׂwVT%#[NH5BJXDH3^ :VZ:9wRϊMTkqPK&sh PKhp,S"ppt/slideLayouts/slideLayout10.xmlVmO0_a!ڢFE8No$B Kl={ZI 8;b53г!9c%F!pOGeyE7f@hߧC< lr kq)h%IEM}Nusht瀘1lq\&P?ߥ) ̨Xs˝5"eݺO/w@{NavB1"lRy54{(1V0Md4U01A  z;uGK.رH?VcDDL84a;z2{+iD!%o?]XlPM9Vs֎Dv[ƒ28v#( \H& GMb|yՋ~ANYo;<“hL2GI 8dc(!vE>]]P.)#gD@}ڵFh5fs~2J48)T..B%G]:7pXux9"$+児2 ܬ84lFT!cMh :/m5UJ Ae"SvrGPVPy[/Uȩ )?z2/҄+Q@JK>--zy_KKKd{C k?PKUm`u PKhp,S"ppt/slideLayouts/slideLayout11.xmlWaO0_a4-jle?q֫c[۵;; !HD*5gݳ ץ@+f,Wr1IUl>kE:"s"dhlt<4+ QKBEscK$@i&W(SC3ù!8i{$\Fz0ˤ#bSUe Xa"9&8WrjU;Vъ[W} HRVX`j(&セM(/Bn{ZwfSq4fҩȑ$%L\s'gS%V_Ƽ%WFOČQ?Wxj LvbCnd.Ly33 Z"]/[;D+7# NB&[Y34LM93(*ZABхER5_"*[.;:r99d%kM1obJ$M?tOK7q1} o]pS5PK̩UPKhp,S"ppt/slideLayouts/slideLayout12.xmln0VGMBڭݤjC{8%±-PIJij+$D8R낣&b'1AeQ DKFtiWd#02 ֪ƆYẢTLX&uA,\[jrQ"櫗W&,n~̲o. *E4^seS 'zEN42'TVi%swv_#% +^,U 4,fnd7 7D,]+1Ny)&}F :‚;~ܨ͘kեVS5cTnD$VQ}+=FuzSN(!y4 A +gPta`DwVU+LޚIpx&?0_;:ou6(Ќ:vȀ-C3n\ `)Z [7fu:-2˫۔:>'N& =3{[[װGneF3 v)Է=yzs/k"Qv᫒AGT.5ehJA-~QMfYZ=5Jo*zo)ݤ*\ >#] (y H!u׀4eE>KqDZ'~t0a `? |{G >8#KRd/rG /{;´0W\~)jPK PKhp,S"ppt/slideLayouts/slideLayout13.xmln0E {pڪH>6U8/MH΃VZfwfNVU0:4ˆkfr)\L0rJy"6fQ@hۄJua [=dEDQPoמ%\1E!2lԕº3 32h+d6(v1EaFHF6F Bpl%s cVR 9ە^24c/yj7;ggYrTP-9Vs֎;.L%e9(:Π0l@Mȃ6eaIXuGa@c.[ XoP0Rl98: >Dfzw"׶5|kdA+5,.ׅhqbMϩwEJnN Bg%ᚳb D*V\i8 7B8쳴hK%ŧa(,$*J{X*VkuUYS9j!BPn-߫rtBTɨpUZ)OHi0|~oLJ )R'_ϵPKʧPKhp,S"ppt/slideLayouts/slideLayout15.xmlUmo0_a mE@vk7P~8o )-v҂g瞘VI 8;c53)~waF)qg&veqCwf@h?S\`DŽxVqE\4NK$?ZI2DQqo_o\C % , [+A W'xJ0g)3"в~_k@{ 1Ejm{i\Hv=Lġ23$[i`NߣK09Σ7.P䯛C` &m6&)8,FҩuvAM|쐟Uߞgէ?#L^A[Y:=nKg# PuXAưG5_ "j㯭8&v59Uݑw<ÓhL2{@YX57 EcgQ;tL77>ԡmH7)v12_G~>qZ#FT{#15N)9uPIV-kA[xzYX,FJHn_J6TPz6mTJ/1T{4w]WMbWeMU冴RzFP'5|EO耠Q'?+So`!e,~^#W"Hw?oPKDށPKhp,Sppt/theme/theme2.xmlYMs6W`pOHLy"e:M5=ӎg?.m+c)vA.@墬5޻*Ӳ]J +T>kYE0d )k 4nh}YD#ϝ{0{`u΁F9߳L(+xW%jH%zA=G ~b0kZW#BBނ36|V [jjp/ VW)95IG)M:I`ijAyqosjڶ7t?[UG|`#w?ϮVkPꋅV+ezd5e sd't8!۝A=F|6.'"֪M(e" *\"}L&rNΣsֶc&׷]ޣKK!aՄA:1um;7iaMA=LZc'[Mn.C({8߽*x^5:>^#̳"Ё!@ A%OӰաz_B;]%1n"+5_F򳣘]7bb$K-pva3w)RM3__׉e }PKhI7PKhp,Sppt/theme/theme1.xmlYMs6W`pOHLy"e:M5=ӎg?.m+c)vA.@墬5޻*Ӳ]J +T>kYE0d )k 4nh}YD#ϝ{0{`u΁F9߳L(+xW%jH%zA=G ~b0kZW#BBނ36|V [jjp/ VW)95IG)M:I`ijAyqosjڶ7t?[UG|`#w?ϮVkPꋅV+ezd5e sd't8!۝A=F|6.'"֪M(e" *\"}L&rNΣsֶc&׷]ޣKK!aՄA:1um;7iaMA=LZc'[Mn.C({8߽*x^5:>^#̳"Ё!@ A%OӰաz_B;]%1n"+5_F򳣘]7bb$K-pva3w)RM3__׉e }PKhI7PKhp,Sppt/presentation.xmln E >c;VlJU'! Mү/qKQw jZ*FWL1BsSKͤk-*xW [Z'Ё}* yBG1NnjvZ`!Ť}^ .\\Uu8vP֟Do<%34aʍQȚpȎF/BIF/iL1ȴgpڃ83n;YW0!i,sw%* Dy32Ύ^K +| O>n/ ?T0t&>Ep6'IzpPK1;%{GPKhp,S,ppt/slideMasters/_rels/slideMaster2.xml.relsj0}Bh_qlJ!UI?@%!) B!0`ituV.D芊S1~^ zW;L*?֓xF!g|3|f,7_]Ϭj>ULr3wAE&9uVPrXK麱Ӝg,ij!T4ˮ+קbe ?-9f}1$mh-F&-OJa]RZъ=FۧIOj>I A 6mU? PK|t`C [PKhp,S,ppt/slideMasters/_rels/slideMaster1.xml.relsKj0}Nae9ٔB@c[ԖB ah!0^Ռ4&_༶F2f,V^SzQxzIcdC3`R>3Y7j>U<ϲ V/2c+;% ɶ]xy͒Pg}*A-PŌGN+Ғ66$kJF,UHk&v77}nWQ]g{ |?}ٛ9׵?7%s6s^,W:bK"X@bW+u\",)@et;ZWb Bh4br9qK4O_Y6 \}rWM_7Ooe)%c:ҒI" >> \HuC^W]Tm c $ܑ9V-([c >PD9$!qM"qԼyH}#l\\?əS ϶%ҁ)g[.b\ev$]Oi@&켼=C}?u:U}1N0+ƊV:vvgZ~v,x?q鏿3P0vĎFvĞOZ6"cYՎՈ!y/Sy{snu" vfƹxĈ7OΝ<1<ُJ:DTFa[?zFċ lgCbcC&ؔ6!&|!Vso~ЃcɛJ gN⸼ܠ͝pSt;?_X'!q[V?:[E9U`ׯ2!NcLeԓ}K{1)K>⽟(!D*ْT[J*I"LmeqW[\m_[sDRz3ʃFV]h]\d^ nve( k:-Zк-ZZNCh݆6hz mآn6hh"b"n-&:_WxWPK־_U(PKhp,S!ppt/slideMasters/slideMaster2.xmlZr8ߧ`蝋in̤l}];> ;^c;83 :;@3, Yߴ_vM'_:iQp\|}y~V*$`)3* P I8c++Ψt!̺O\3$ w$oy7d<qYb02h8V\TCjaM~v.~ǿ~(ŧV;Ů8 Axz?>R)ݍv u~ac9R[NNpRX[)%II߃Aux .N]9o+;eNIdWwuOIikShlM*aZ:[rgZ`Cq{G[W[\m_[|m VJ],wr2V5oЂOuR~{]U^EnęĹ{6n k`-ؠ Za`6jnlm`n np9Ieb5?PKZ#PKhp,S[Content_Types].xml͚Ko0m+%bytY,I耱ljH *) Mؾ|~syOMZȹσHEm2gEsmkk'L\P$]Ͳй^1%bE, kҒ[/z_]q}sݮB,uլeq2NLt-Y"6f*sޭ!eGE!U[ yB۟"w ؟',8rڔ"ׅ2%bLcr96 vBDmJiDž_ZF難ݎdPbKuBD߄{-zA4wRlDEr26f~t8,  r>aA" 78,H+,t,-\E Gr񞡚rGIs 4Nsяi t !A֋ =*∻#Gsp!:%pD G<1vM9՟WQi/oܹH^0λ~]4C'wT(Mo:GN;Lg3VgPKiƁ^F $PKhp,S =@ _rels/.relsPKhp,SڝzdocProps/app.xmlPKhp,SVl;docProps/core.xmlPKhp,SxqHppt/_rels/presentation.xml.relsPKhp,Sdb& ppt/slides/_rels/slide2.xml.relsPKhp,S& _' ppt/slides/_rels/slide1.xml.relsPKhp,SƄ ppt/slides/slide1.xmlPKhp,S͑5 ppt/slides/slide2.xmlPKhp,SSc&-G ppt/slideLayouts/_rels/slideLayout24.xml.relsPKhp,SSc&-Nppt/slideLayouts/_rels/slideLayout23.xml.relsPKhp,SSc&-Uppt/slideLayouts/_rels/slideLayout22.xml.relsPKhp,SSc&-\ppt/slideLayouts/_rels/slideLayout21.xml.relsPKhp,SSc&-cppt/slideLayouts/_rels/slideLayout20.xml.relsPKhp,SSc&-jppt/slideLayouts/_rels/slideLayout19.xml.relsPKhp,SCV&,qppt/slideLayouts/_rels/slideLayout6.xml.relsPKhp,SCV&,vppt/slideLayouts/_rels/slideLayout5.xml.relsPKhp,SSc&-{ppt/slideLayouts/_rels/slideLayout14.xml.relsPKhp,SSc&-ppt/slideLayouts/_rels/slideLayout13.xml.relsPKhp,SSc&-ppt/slideLayouts/_rels/slideLayout18.xml.relsPKhp,SCV&,ppt/slideLayouts/_rels/slideLayout4.xml.relsPKhp,SCV&-ppt/slideLayouts/_rels/slideLayout12.xml.relsPKhp,SSc&-ppt/slideLayouts/_rels/slideLayout17.xml.relsPKhp,SCV&,ppt/slideLayouts/_rels/slideLayout3.xml.relsPKhp,SCV&-ppt/slideLayouts/_rels/slideLayout11.xml.relsPKhp,SCV&,ppt/slideLayouts/_rels/slideLayout9.xml.relsPKhp,SCV&-ppt/slideLayouts/_rels/slideLayout10.xml.relsPKhp,SCV&,ppt/slideLayouts/_rels/slideLayout7.xml.relsPKhp,SCV&, ppt/slideLayouts/_rels/slideLayout1.xml.relsPKhp,SSc&-!ppt/slideLayouts/_rels/slideLayout15.xml.relsPKhp,SCV&,"ppt/slideLayouts/_rels/slideLayout8.xml.relsPKhp,SCV&,#ppt/slideLayouts/_rels/slideLayout2.xml.relsPKhp,SSc&-$ppt/slideLayouts/_rels/slideLayout16.xml.relsPKhp,SF8s!%ppt/slideLayouts/slideLayout6.xmlPKhp,SY&U.k!(ppt/slideLayouts/slideLayout5.xmlPKhp,S h !V+ppt/slideLayouts/slideLayout4.xmlPKhp,SL!o.ppt/slideLayouts/slideLayout3.xmlPKhp,S\ix!i1ppt/slideLayouts/slideLayout1.xmlPKhp,SOj!!3ppt/slideLayouts/slideLayout2.xmlPKhp,Swz !6ppt/slideLayouts/slideLayout7.xmlPKhp,Sij !U9ppt/slideLayouts/slideLayout8.xmlPKhp,SNn !<ppt/slideLayouts/slideLayout9.xmlPKhp,Smi "?ppt/slideLayouts/slideLayout24.xmlPKhp,SI"&Cppt/slideLayouts/slideLayout23.xmlPKhp,S*u "cFppt/slideLayouts/slideLayout22.xmlPKhp,SXnX "Ippt/slideLayouts/slideLayout21.xmlPKhp,Sð "Lppt/slideLayouts/slideLayout20.xmlPKhp,Sd "Oppt/slideLayouts/slideLayout19.xmlPKhp,Sq1u"8Sppt/slideLayouts/slideLayout18.xmlPKhp,SC+l"Uppt/slideLayouts/slideLayout17.xmlPKhp,S&sh "Xppt/slideLayouts/slideLayout16.xmlPKhp,SUm`u "[ppt/slideLayouts/slideLayout10.xmlPKhp,S̩U"^ppt/slideLayouts/slideLayout11.xmlPKhp,S "+bppt/slideLayouts/slideLayout12.xmlPKhp,S\ix"eppt/slideLayouts/slideLayout13.xmlPKhp,Sʧ"=gppt/slideLayouts/slideLayout14.xmlPKhp,SDށ"4jppt/slideLayouts/slideLayout15.xmlPKhp,ShI71mppt/theme/theme2.xmlPKhp,ShI7qppt/theme/theme1.xmlPKhp,S1;%{G#vppt/presentation.xmlPKhp,S|t`C [,wppt/slideMasters/_rels/slideMaster2.xml.relsPKhp,SUG R,yppt/slideMasters/_rels/slideMaster1.xml.relsPKhp,S־_U(!wzppt/slideMasters/slideMaster1.xmlPKhp,SZ#!ppt/slideMasters/slideMaster2.xmlPKhp,SiƁ^F $[Content_Types].xmlPK@@t+PK\i.tests/fixtures/extension/simple/db/install.xmlnu[
PK\hDJvv9tests/fixtures/extension/simple/lang/en/bbbext_simple.phpnu[. /** * Language File. * * @package mod_bigbluebuttonbn * @copyright 2023 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ defined('MOODLE_INTERNAL') || die(); $string['config_extension'] = 'Sample config extension setting'; $string['newfield'] = 'New field'; $string['newfielderror'] = 'New field cannot be empty'; $string['pluginname'] = 'Simple BigBlueButtonPlugin'; $string['completionextraisehandtwice'] = 'Raise hand twice'; $string['completionextraisehandtwice_desc'] = 'Raise hand twice in a meeting.'; $string['completionextraisehandtwice_help'] = 'Raise hand twice in a meeting.'; PK\z??Xtests/fixtures/extension/simple/backup/moodle2/restore_bbbext_simple_subplugin.class.phpnu[. /** * Provides the information for restore. * * * @package mod_bigbluebuttonbn * @copyright 2023 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ class restore_bbbext_simple_subplugin extends restore_subplugin { /** * Returns the paths to be handled by the subplugin. * * @return array */ protected function define_bigbluebuttonbn_subplugin_structure() { $paths = []; $elename = $this->get_namefor('bigbluebuttonbn'); // We used get_recommended_name() so this works. $elepath = $this->get_pathfor('/bbbext_simple'); $paths[] = new restore_path_element($elename, $elepath); return $paths; } /** * Processes one subplugin instance additional parameter. * * @param mixed $data */ public function process_bbbext_simple_bigbluebuttonbn($data) { global $DB; $data = (object) $data; $data->bigbluebuttonbnid = $this->get_new_parentid('bigbluebuttonbn'); $DB->insert_record('bbbext_simple', $data); } } PK\[>Wtests/fixtures/extension/simple/backup/moodle2/backup_bbbext_simple_subplugin.class.phpnu[. /** * Provides the information for backup. * * @package mod_bigbluebuttonbn * @copyright 2023 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ class backup_bbbext_simple_subplugin extends backup_subplugin { /** * Returns the subplugin information to attach the BigBlueButton instance. * * @return backup_subplugin_element */ protected function define_bigbluebuttonbn_subplugin_structure() { // Create XML elements. $subplugin = $this->get_subplugin_element(); $subpluginwrapper = new backup_nested_element($this->get_recommended_name()); $subpluginelement = new backup_nested_element( 'bbbext_simple', null, ['newfield', 'completionextraisehandtwice', 'meetingevents'] ); // Connect XML elements into the tree. $subplugin->add_child($subpluginwrapper); $subpluginwrapper->add_child($subpluginelement); // Set source to populate the data. $subpluginelement->set_source_table( 'bbbext_simple', ['bigbluebuttonbnid' => backup::VAR_PARENTID] ); return $subplugin; } } PK\͢߀+tests/fixtures/extension/simple/version.phpnu[. /** * This file contains the version information for the sample bigbluebuttonbn subplugin * * @package mod_bigbluebuttonbn * @copyright 2023 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent [at] call-learning [dt] fr) */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2023020800; $plugin->requires = 2023020300; $plugin->component = 'bbbext_simple'; PK\R?Ktests/fixtures/extension/simple/classes/bigbluebuttonbn/mod_form_addons.phpnu[. namespace bbbext_simple\bigbluebuttonbn; use stdClass; /** * A class for the main mod form extension * * @package mod_bigbluebuttonbn * @copyright 2023 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ class mod_form_addons extends \mod_bigbluebuttonbn\local\extension\mod_form_addons { /** * Allows modules to modify the data returned by form get_data(). * This method is also called in the bulk activity completion form. * * Only available on moodleform_mod. * * @param stdClass $data passed by reference */ public function data_postprocessing(\stdClass &$data): void { // Nothing for now. } /** * Allow module to modify the data at the pre-processing stage. * * This method is also called in the bulk activity completion form. * * @param array|null $defaultvalues */ public function data_preprocessing(?array &$defaultvalues): void { // This is where we can add the data from the flexurl table to the data provided. if (!empty($defaultvalues['id'])) { global $DB; $flexurlrecord = $DB->get_record('bbbext_simple', [ 'bigbluebuttonbnid' => $defaultvalues['id'], ]); if ($flexurlrecord) { $defaultvalues['newfield'] = $flexurlrecord->newfield; } } } /** * Can be overridden to add custom completion rules if the module wishes * them. If overriding this, you should also override completion_rule_enabled. *

* Just add elements to the form as needed and return the list of IDs. The * system will call disabledIf and handle other behaviour for each returned * ID. * * @return array Array of string IDs of added items, empty array if none */ public function add_completion_rules(): array { $this->mform->addElement('advcheckbox', 'completionextraisehandtwice', get_string('completionextraisehandtwice', 'bbbext_simple'), get_string('completionextraisehandtwice_desc', 'bbbext_simple')); $this->mform->addHelpButton('completionextraisehandtwice', 'completionextraisehandtwice', 'bbbext_simple'); $this->mform->disabledIf('completionextraisehandtwice', 'completion', 'neq', COMPLETION_AGGREGATION_ANY); return ['completionextraisehandtwice' . $this->suffix]; } /** * Called during validation. Override to indicate, based on the data, whether * a custom completion rule is enabled (selected). * * @param array $data Input data (not yet validated) * @return bool True if one or more rules is enabled, false if none are; * default returns false */ public function completion_rule_enabled(array $data): bool { return !empty($data['completionextraisehandtwice' . $this->suffix]); } /** * Form adjustments after setting data * * @return void */ public function definition_after_data() { // Nothing for now. } /** * Add new form field definition */ public function add_fields(): void { $this->mform->addElement('header', 'simple', get_string('pluginname', 'bbbext_simple')); $this->mform->addElement('text', 'newfield', get_string('newfield', 'bbbext_simple')); $this->mform->setType('newfield', PARAM_TEXT); } /** * Validate form and returns an array of errors indexed by field name * * @param array $data * @param array $files * @return array */ public function validation(array $data, array $files): array { $errors = []; if (empty($data['newfield' . $this->suffix])) { $errors['newfield'] = get_string('newfielderror', 'bbbext_simple'); } return $errors; } } PK\wAmA Otests/fixtures/extension/simple/classes/bigbluebuttonbn/mod_instance_helper.phpnu[. namespace bbbext_simple\bigbluebuttonbn; use stdClass; /** * Class defining a way to deal with instance save/update/delete in extension * * @package mod_bigbluebuttonbn * @copyright 2023 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ class mod_instance_helper extends \mod_bigbluebuttonbn\local\extension\mod_instance_helper { /** * Runs any processes that must run before a bigbluebuttonbn insert/update. * * @param stdClass $bigbluebuttonbn BigBlueButtonBN form data **/ public function add_instance(stdClass $bigbluebuttonbn) { global $DB; $DB->insert_record('bbbext_simple', (object) [ 'bigbluebuttonbnid' => $bigbluebuttonbn->id, 'newfield' => $bigbluebuttonbn->newfield ?? '', 'completionextraisehandtwice' => $bigbluebuttonbn->completionextraisehandtwice ?? '', 'meetingevents' => $bigbluebuttonbn->meetingevents ?? '', ]); } /** * Runs any processes that must be run after a bigbluebuttonbn insert/update. * * @param stdClass $bigbluebuttonbn BigBlueButtonBN form data **/ public function update_instance(stdClass $bigbluebuttonbn): void { global $DB; $record = $DB->get_record('bbbext_simple', [ 'bigbluebuttonbnid' => $bigbluebuttonbn->id, ]); // Just in case the instance was created before the extension was installed. if (empty($record)) { $record = new stdClass(); $record->bigbluebuttonbnid = $bigbluebuttonbn->id; $record->newfield = $bigbluebuttonbn->newfield ?? ''; $record->completionextraisehandtwice = $bigbluebuttonbn->completionextraisehandtwice ?? 0; $record->meetingevents = $bigbluebuttonbn->meetingevents ?? ''; $DB->insert_record('bbbext_simple', $record); } else { $record->newfield = $bigbluebuttonbn->newfield ?? ''; $record->completionextraisehandtwice = $bigbluebuttonbn->completionextraisehandtwice ?? 0; $record->meetingevents = $bigbluebuttonbn->meetingevents ?? ''; $DB->update_record('bbbext_simple', $record); } } /** * Runs any processes that must be run after a bigbluebuttonbn delete. * * @param int $id */ public function delete_instance(int $id): void { global $DB; $DB->delete_records('bbbext_simple', [ 'bigbluebuttonbnid' => $id, ]); } /** * Get any join table name that is used to store additional data for the instance. * @return string[] */ public function get_join_tables(): array { return ['bbbext_simple']; } } PK\WXtests/fixtures/extension/simple/classes/bigbluebuttonbn/broker_meeting_events_addons.phpnu[. namespace bbbext_simple\bigbluebuttonbn; /** * When meeting_events callback is implemented by BigBlueButton, Moodle receives a POST request * which is processed in the function using super globals. * * @package mod_bigbluebuttonbn * @copyright 2024 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) */ class broker_meeting_events_addons extends \mod_bigbluebuttonbn\local\extension\broker_meeting_events_addons { /** * Data processing action */ public function process_action() { global $DB; if ($this->instance) { $bigbluebuttonbnid = $this->instance->get_instance_id(); $record = $DB->get_record('bbbext_simple', [ 'bigbluebuttonbnid' => $bigbluebuttonbnid, ]); if ($record) { $record->meetingevents = $this->data; return $DB->update_record('bbbext_simple', $record); } } return false; } } PK\ Ttests/fixtures/extension/simple/classes/bigbluebuttonbn/custom_completion_addons.phpnu[. namespace bbbext_simple\bigbluebuttonbn; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\logger; /** * A class to deal with completion rules addons in a subplugin * * @package mod_bigbluebuttonbn * @copyright 2023 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ class custom_completion_addons extends \mod_bigbluebuttonbn\local\extension\custom_completion_addons { /** * Get defined custom rule * * @return string[] */ public static function get_defined_custom_rules(): array { return ['completionextraisehandtwice']; } /** * Get state * * @param string $rule * @return int */ public function get_state(string $rule): int { $instance = instance::get_from_cmid($this->cm->id); $logs = logger::get_user_completion_logs($instance, $this->userid, [logger::EVENT_SUMMARY]); $raisehandcount = 0; foreach ($logs as $log) { $summary = json_decode($log->meta); $raisehandcount += intval($summary->data->engagement->raisehand ?? 0); } if ($raisehandcount >= 2) { return COMPLETION_COMPLETE; } return COMPLETION_INCOMPLETE; } /** * Get rule description * * @return array * @throws \coding_exception */ public function get_custom_rule_descriptions(): array { return [ 'completionextraisehandtwice' => get_string('completionextraisehandtwice_desc', 'bbbext_simple'), ]; } /** * Get sort order * * @return string[] */ public function get_sort_order(): array { return [ 'completionattendance', ]; } } PK\(9Mtests/fixtures/extension/simple/classes/bigbluebuttonbn/action_url_addons.phpnu[. namespace bbbext_simple\bigbluebuttonbn; /** * A single action class to mutate the action URL. * * @package mod_bigbluebuttonbn * @copyright 2023 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ class action_url_addons extends \mod_bigbluebuttonbn\local\extension\action_url_addons { /** * Sample mutate the action URL. * * * @param string $action * @param array $data * @param array $metadata * @param int|null $instanceid * @return array associative array with the additional data and metadata (indexed by 'data' and * 'metadata' keys) */ public function execute( string $action = '', array $data = [], array $metadata = [], ?int $instanceid = null ): array { if ($action == 'create' || $action == 'join') { global $DB; $record = $DB->get_record('bbbext_simple', [ 'bigbluebuttonbnid' => $instanceid, ]); if ($record) { $metadata['newfield'] = $record->newfield ?? ''; } } return [ 'data' => $data, 'metadata' => $metadata, ]; } } PK\i~,tests/fixtures/extension/simple/settings.phpnu[. /** * This file defines the admin settings for this plugin * * @package mod_bigbluebuttonbn * @copyright 2023 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent [at] call-learning [dt] fr) */ defined('MOODLE_INTERNAL') || die; $settings->add(new admin_setting_configtext( 'bbbext_simple/config_extension', new lang_string('config_extension', 'bbbext_simple'), new lang_string('config_extension', 'bbbext_simple'), 1024, PARAM_TEXT) ); PK\m^(^(tests/backup_restore_test.phpnu[. /** * BBB Library tests class. * * @package mod_bigbluebuttonbn * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ namespace mod_bigbluebuttonbn; use mod_bigbluebuttonbn\test\testcase_helper_trait; use restore_date_testcase; use stdClass; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->libdir . "/phpunit/classes/restore_date_testcase.php"); /** * BBB Library tests class. * * @package mod_bigbluebuttonbn * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) * @covers \backup_bigbluebuttonbn_activity_task * @covers \restore_bigbluebuttonbn_activity_task */ final class backup_restore_test extends restore_date_testcase { use testcase_helper_trait; /** * Setup */ public function setUp(): void { parent::setUp(); } /** * All instance types */ const ALL_TYPES = [ 'Instance Type ALL' => instance::TYPE_ALL, 'Instance Type Recording Only' => instance::TYPE_RECORDING_ONLY, 'Instance Room Only' => instance::TYPE_ROOM_ONLY ]; /** * Test backup restore (basic) */ public function test_backup_restore(): void { global $DB; $this->resetAfterTest(); $bbactivity = []; foreach (self::ALL_TYPES as $key => $type) { list($bbactivitycontext, $bbactivitycm, $bbactivity[$type]) = $this->create_instance($this->get_course(), ['type' => $type]); } $newcourseid = $this->backup_and_restore($this->get_course()); foreach (self::ALL_TYPES as $key => $type) { $newbbb = $DB->get_record('bigbluebuttonbn', ['course' => $newcourseid, 'type' => $type], '*', MUST_EXIST); // One record. $this->assert_bbb_activities_same($bbactivity[$type], $newbbb); $this->assertEquals($bbactivity[$type]->meetingid, $newbbb->meetingid); } } /** * @var $RECORDINGS_DATA array fake recording data. */ const RECORDINGS_DATA = [ ['name' => 'Recording 1'], ['name' => 'Recording 2'], ]; /** * Check backup restore with recordings * */ public function test_backup_restore_with_recordings(): void { global $DB; $this->resetAfterTest(); $this->initialise_mock_server(); set_config('bigbluebuttonbn_importrecordings_enabled', 1); // This is for imported recording. $generator = $this->getDataGenerator(); $othercourse = $generator->create_course(); $otherbbbactivities = []; $recordingstoimport = []; list('activity' => $otherbbbactivities[], 'recordings' => $recordings) = $this->create_activity_with_recordings($othercourse, instance::TYPE_ALL, self::RECORDINGS_DATA ); $recordingstoimport = array_merge($recordingstoimport, $recordings); list('activity' => $otherbbbactivities[], 'recordings' => $recordings) = $this->create_activity_with_recordings($this->get_course(), instance::TYPE_ALL, self::RECORDINGS_DATA ); $recordingstoimport = array_merge($recordingstoimport, $recordings); // Create a set of recordings and imported recordings. // We have nbrecording per bb activity, except for roomonly recordings which have the imported recordings. $bbactivity = []; foreach (self::ALL_TYPES as $key => $type) { $bbactivity[$type] = $this->getDataGenerator()->create_module( 'bigbluebuttonbn', ['course' => $this->get_course()->id, 'type' => $type, 'name' => 'BBB Activity:' . $key], ['visible' => true] ); $instance = instance::get_from_instanceid($bbactivity[$type]->id); // Create recording except for TYPE_RECORDING_ONLY Only. if ($instance->is_feature_enabled('showroom')) { $this->create_recordings_for_instance(instance::get_from_instanceid($bbactivity[$type]->id), self::RECORDINGS_DATA); } // Then import the recordings into the instance. if ($instance->is_feature_enabled('importrecordings')) { foreach ($recordingstoimport as $rec) { $rentity = recording::get_record(['id' => $rec->id]); if ($rentity->get('bigbluebuttonbnid') != $instance->get_instance_id()) { $rentity->create_imported_recording($instance); } } } } // Backup and restore steps. $nbrecordings = count(self::RECORDINGS_DATA); $newcourseid = $this->backup_and_restore($this->get_course()); // Now checks. foreach (self::ALL_TYPES as $key => $type) { $newbbb = $DB->get_record('bigbluebuttonbn', ['course' => $newcourseid, 'type' => $type, 'name' => 'BBB Activity:' . $key], '*', MUST_EXIST); // One record. $this->assert_bbb_activities_same($bbactivity[$type], $newbbb); $newinstance = instance::get_from_instanceid($newbbb->id); $instancerecordings = $newinstance->get_recordings(); // Type ROOM_ONLY & TYPE_ALL : all assigned recordings (NB_RECORDINGS). // Type TYPE_RECORDING_ONLY: all recordings from this course (i.e. // existing recording (NB_RECORDING) + ROOM_ONLY(NB_RECORDING) + TYPE_ALL (NB_RECORDING)). $expectedcount = $type == instance::TYPE_RECORDING_ONLY ? $nbrecordings * 3 : $nbrecordings; // Type ROOM_ONLY & TYPE_ALL : The imported recording (NB_RECORDING*2 here) // Type TYPE_RECORDING_ONLY: imported recordings we add the imported recording from the other activity (TYPE_ALL). $expectedcount += $type == instance::TYPE_RECORDING_ONLY ? count($recordingstoimport) : 0; // We managed to import recording in this activity, so let's add them. $expectedcount += $newinstance->is_feature_enabled('importrecordings') ? count($recordingstoimport) : 0; $this->assertCount($expectedcount, $instancerecordings, 'Wrong count for instance Type:' . $key); // Then check imported recordings. foreach ($instancerecordings as $rec) { if ($rec->get('imported')) { $importeddata = json_decode($rec->get('importeddata')); $this->assertNotEmpty($importeddata); } } } } /** * Check duplicating activity does not duplicate meeting id * * @dataProvider bbb_type_provider */ public function test_duplicate_module_no_meetingid(int $type): void { list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance($this->get_course(), ['type' => $type]); $newcm = duplicate_module($this->get_course(), $bbactivitycm); $oldinstance = instance::get_from_cmid($bbactivitycm->id); $newinstance = instance::get_from_cmid($newcm->id); $this->assertNotEquals($oldinstance->get_instance_var('meetingid'), $newinstance->get_instance_var('meetingid')); } /** * Check that using the recycle bin keeps the meeting id * * @dataProvider bbb_type_provider */ public function test_recycle_module_keep_meetingid(int $type): void { list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance($this->get_course(), ['type' => $type]); // Delete the course module. course_delete_module($bbactivitycm->id); // Now, run the course module deletion adhoc task. \phpunit_util::run_all_adhoc_tasks(); $currentinstances = instance::get_all_instances_in_course($this->course->id); $this->assertEmpty($currentinstances); // Try restoring. $recyclebin = new \tool_recyclebin\course_bin($this->course->id); foreach ($recyclebin->get_items() as $item) { $recyclebin->restore_item($item); } $restoredinstance = instance::get_all_instances_in_course($this->course->id); $restoredinstance = end($restoredinstance); $this->assertEquals($restoredinstance->get_instance_var('meetingid'), $bbactivity->meetingid); } /** * Return an array of BigBlueButton types * @return array[] */ public static function bbb_type_provider(): array { return [ 'All' => [instance::TYPE_ALL], 'Recording Only' => [instance::TYPE_RECORDING_ONLY], 'Room Only' => [instance::TYPE_ROOM_ONLY] ]; } /** * Check two bbb activities are the same * * @param stdClass $bbboriginal * @param stdClass $bbbdest */ protected function assert_bbb_activities_same(stdClass $bbboriginal, stdClass $bbbdest) { $this->assertNotFalse($bbbdest); $filterfunction = function($key) { return !in_array($key, ['course', 'cmid', 'id', 'guestlinkuid', 'guestpassword']); }; $this->assertEquals( array_filter((array) $bbboriginal, $filterfunction, ARRAY_FILTER_USE_KEY), array_filter((array) $bbbdest, $filterfunction, ARRAY_FILTER_USE_KEY) ); } } PK\1!7tests/generator/behat_mod_bigbluebuttonbn_generator.phpnu[. /** * Behat data generator for mod_bigbluebuttonbn. * * @package mod_bigbluebuttonbn * @category test * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ /** * Behat data generator for behat_mod_bigbluebuttonbn_generator. * * @package mod_bigbluebuttonbn * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ class behat_mod_bigbluebuttonbn_generator extends behat_generator_base { /** * Get all entities that can be create through this behat_generator * * @return array */ protected function get_creatable_entities(): array { return [ 'recordings' => [ 'singular' => 'recording', 'datagenerator' => 'recording', 'required' => ['bigbluebuttonbn', 'name'], 'switchids' => [ 'bigbluebuttonbn' => 'bigbluebuttonbnid', 'group' => 'groupid', ], ], 'logs' => [ 'singular' => 'log', 'datagenerator' => 'override', 'required' => ['bigbluebuttonbn', 'user'], 'switchids' => [ 'bigbluebuttonbn' => 'bigbluebuttonbnid', 'user' => 'userid', ], ], 'meetings' => [ 'singular' => 'meeting', 'datagenerator' => 'meeting', 'required' => [ 'activity', ], 'switchids' => [ 'activity' => 'instanceid', 'group' => 'groupid', ], ], ]; } /** * Look up the id of a bigbluebutton activity from its name. * * @param string $bbactivityname the bigbluebutton activity name, for example 'Test meeting'. * @return int corresponding id. * @throws Exception */ protected function get_bigbluebuttonbn_id(string $bbactivityname): int { global $DB; if (!$id = $DB->get_field('bigbluebuttonbn', 'id', ['name' => $bbactivityname])) { throw new Exception('There is no bigbluebuttonbn with name "' . $bbactivityname . '" does not exist'); } return $id; } /** * Get the activity id from its name * * @param string $activityname * @return int * @throws Exception */ protected function get_activity_id(string $activityname): int { global $DB; $sql = <<get_field_sql($sql, ['idnumber' => $activityname, 'name' => $activityname]); if (empty($id)) { throw new Exception("There is no bigbluebuttonbn with name '{$activityname}' does not exist"); } return $id; } } PK\IDvCCtests/generator/lib.phpnu[. /** * mod_bigbluebuttonbn data generator * * @package mod_bigbluebuttonbn * @category test * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) */ use core\plugininfo\mod; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\local\config; use mod_bigbluebuttonbn\logger; use mod_bigbluebuttonbn\recording; /** * bigbluebuttonbn module data generator * * @package mod_bigbluebuttonbn * @category test * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) */ class mod_bigbluebuttonbn_generator extends \testing_module_generator { /** * Creates an instance of bigbluebuttonbn for testing purposes. * * @param array|stdClass $record data for module being generated. * @param null|array $options general options for course module. * @return stdClass record from module-defined table with additional field cmid */ public function create_instance($record = null, ?array $options = null) { // Prior to creating the instance, make sure that the BigBlueButton module is enabled. $modules = \core_plugin_manager::instance()->get_plugins_of_type('mod'); if (!$modules['bigbluebuttonbn']->is_enabled()) { mod::enable_plugin('bigbluebuttonbn', true); } $now = time(); $defaults = [ "type" => 0, "meetingid" => sha1(rand()), "record" => true, "moderatorpass" => "mp", "viewerpass" => "ap", "participants" => "{}", "timecreated" => $now, "timemodified" => $now, "presentation" => null, "recordings_preview" => 0, "grade" => 0, ]; $record = (array) $record; $record['participants'] = json_encode($this->get_participants_from_record($record)); foreach ($defaults as $key => $value) { if (!isset($record[$key])) { $record[$key] = $value; } } if ($record['presentation']) { global $USER; // Here we replace the original presentation file with a draft area in which we store this file. $draftareaid = file_get_unused_draft_itemid(); $bbbfilerecord['contextid'] = context_user::instance($USER->id)->id; $bbbfilerecord['component'] = 'user'; $bbbfilerecord['filearea'] = 'draft'; $bbbfilerecord['itemid'] = $draftareaid; $bbbfilerecord['filepath'] = '/'; $bbbfilerecord['filename'] = basename($record['presentation']); $fs = get_file_storage(); $fs->create_file_from_pathname($bbbfilerecord, $record['presentation']); // Now the $record['presentation'] must contain the draftareaid. $record['presentation'] = $draftareaid; } return parent::create_instance((object) $record, (array) $options); } /** * Create the participants field data from create_instance data. * * @param array $record * @return array */ protected function get_participants_from_record(array $record): array { $roles = []; if (array_key_exists('moderators', $record) && !empty($record['moderators'])) { $roles = array_merge( $roles, $this->get_participant_configuration($record['moderators'], 'moderator') ); unset($record['moderators']); } if (array_key_exists('viewers', $record) && !empty($record['viewers'])) { $roles = array_merge( $roles, $this->get_participant_configuration($record['viewers'], 'viewer') ); unset($record['viewers']); } if (!empty($roles)) { array_unshift($roles, (object) [ 'selectiontype' => 'all', 'selectionid' => 'all', 'role' => 'viewer', ]); } return $roles; } /** * Get the participant configuration for a field and role for use in get_participants_from_record. * * @param string $field * @param string $role * @return array */ protected function get_participant_configuration(string $field, string $role): array { global $DB; $values = explode(',', $field); $roles = $DB->get_records_menu('role', [], '', 'shortname, id'); $configuration = []; foreach ($values as $value) { if (empty($value)) { // Empty value. continue; } [$type, $name] = explode(':', $value); $participant = (object) [ 'selectiontype' => $type, 'role' => $role, ]; switch ($type) { case 'role': if (!array_key_exists($name, $roles)) { throw new \coding_exception("Unknown role '{$name}'"); } $participant->selectionid = $roles[$name]; break; case 'user': $participant->selectionid = $DB->get_field('user', 'id', ['username' => $name], MUST_EXIST); break; default: throw new \coding_exception("Unknown participant type: '{$type}'"); } $configuration[] = $participant; } return $configuration; } /** * Create a recording for the given bbb activity. * * The recording is created both locally, and a recording record is created on the mocked BBB server. * * @param array $data * @param bool $serveronly create it only on the server, not in the database. * @return stdClass the recording object */ public function create_recording(array $data, $serveronly = false): stdClass { $instance = instance::get_from_instanceid($data['bigbluebuttonbnid']); if (isset($data['imported']) && filter_var($data['imported'], FILTER_VALIDATE_BOOLEAN)) { if (empty($data['importedid'])) { throw new moodle_exception('error'); } $recording = recording::get_record(['recordingid' => $data['importedid']]); $recording->imported = true; } else { $recording = (object) [ 'headless' => false, 'imported' => false, 'status' => $data['status'] ?? recording::RECORDING_STATUS_NOTIFIED, ]; } if (!empty($data['groupid'])) { $instance->set_group_id($data['groupid']); $recording->groupid = $data['groupid']; } $recording->bigbluebuttonbnid = $instance->get_instance_id(); $recording->courseid = $instance->get_course_id(); if (isset($options['imported']) && $options['imported']) { $precording = $recording->create_imported_recording($instance); } else { if ($recording->status == recording::RECORDING_STATUS_DISMISSED) { $recording->recordingid = sprintf( "%s-%s", md5($instance->get_meeting_id()), time() + rand(1, 100000) ); } else { $recording->recordingid = $this->create_mockserver_recording($instance, $recording, $data); } $precording = new recording(0, $recording); if (!$serveronly) { $precording->create(); } } return $precording->to_record(); } /** * Add a recording in the mock server * * @param instance $instance * @param stdClass $recordingdata * @param array $data * @return string */ protected function create_mockserver_recording(instance $instance, stdClass $recordingdata, array $data): string { $now = time(); $mockdata = array_merge((array) $recordingdata, [ 'sequence' => 1, 'meta' => [ 'bn-presenter-name' => $data['presentername'] ?? 'Fake presenter', 'bbb-recording-ready-url' => new moodle_url('/mod/bigbluebuttonbn/bbb_broker.php', [ 'action' => 'recording_ready', 'bigbluebuttonbn' => $instance->get_instance_id() ]), 'bbb-recording-description' => $data['description'] ?? '', 'bbb-recording-name' => $data['name'] ?? '', 'bbb-recording-tags' => $data['tags'] ?? '', ], ]); $mockdata['startTime'] = $data['starttime'] ?? $now; $mockdata['endTime'] = $data['endtime'] ?? $mockdata['startTime'] + HOURSECS; if (!empty($data['playback'])) { $mockdata['playback'] = json_encode($data['playback']); } if (!empty($data['isBreakout'])) { // If it is a breakout meeting, we do not have any way to know the real Id of the meeting // unless we query the list of submeetings. // For now we will just send the parent ID and let the mock server deal with the sequence + parentID // to find the meetingID. $mockdata['parentMeetingID'] = $instance->get_meeting_id(); } else { $mockdata['meetingID'] = $instance->get_meeting_id(); } $result = $this->send_mock_request('backoffice/createRecording', [], $mockdata); return (string) $result->recordID; } /** * Utility to send a request to the mock server * * @param string $endpoint * @param array $params * @param array $mockdata * @return SimpleXMLElement|bool * @throws moodle_exception */ protected function send_mock_request(string $endpoint, array $params = [], array $mockdata = []): SimpleXMLElement { $url = $this->get_mocked_server_url($endpoint, $params); foreach ($mockdata as $key => $value) { if (is_array($value)) { foreach ($value as $subkey => $subvalue) { $paramname = "{$key}_{$subkey}"; $url->param($paramname, $subvalue); } } else { $url->param($key, $value); } } $curl = new \curl(); $result = $curl->get($url->out_omit_querystring(), $url->params()); $retvalue = @simplexml_load_string($result, 'SimpleXMLElement', LIBXML_NOCDATA | LIBXML_NOBLANKS); if ($retvalue === false) { throw new moodle_exception('mockserverconnfailed', 'mod_bigbluebutton'); } return $retvalue; } /** * Get a URL for a mocked BBB server endpoint. * * @param string $endpoint * @param array $params * @return moodle_url */ protected function get_mocked_server_url(string $endpoint = '', array $params = []): moodle_url { return new moodle_url(TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER . '/' . $endpoint, $params); } /** * Mock an in-progress meeting on the remote server. * * @param array $data * @return stdClass */ public function create_meeting(array $data): stdClass { $instance = instance::get_from_instanceid($data['instanceid']); if (array_key_exists('groupid', $data)) { $instance = instance::get_group_instance_from_instance($instance, $data['groupid']); } $meetingid = $instance->get_meeting_id(); // Default room configuration. $roomconfig = array_merge($data, [ 'meetingName' => $instance->get_meeting_name(), 'attendeePW' => $instance->get_viewer_password(), 'moderatorPW' => $instance->get_moderator_password(), 'voiceBridge' => $instance->get_voice_bridge(), 'meta' => [ 'bbb-context' => $instance->get_course()->fullname, 'bbb-context-id' => $instance->get_course()->id, 'bbb-context-label' => $instance->get_course()->shortname, 'bbb-context-name' => $instance->get_course()->fullname, 'bbb-origin' => 'Moodle', 'bbb-origin-tag' => 'moodle-mod_bigbluebuttonbn (TODO version)', 'bbb-recording-description' => $instance->get_meeting_description(), 'bbb-recording-name' => $instance->get_meeting_name(), ], ]); if ((boolean) config::get('recordingready_enabled')) { $roomconfig['meta']['bbb-recording-ready-url'] = $instance->get_record_ready_url()->out(false); } if ((boolean) config::get('meetingevents_enabled')) { $roomconfig['meta']['analytics-callback-url'] = $instance->get_meeting_event_notification_url()->out(false); } if (!empty($roomconfig['isBreakout'])) { // If it is a breakout meeting, we do not have any way to know the real Id of the meeting // For now we will just send the parent ID and let the mock server deal with the sequence + parentID // to find the meetingID. $roomconfig['parentMeetingID'] = $instance->get_meeting_id(); } else { $roomconfig['meetingID'] = $meetingid; } $this->send_mock_request('backoffice/createMeeting', [], $roomconfig); return (object) $roomconfig; } /** * Create a log record * * @param mixed $record * @param array|null $options */ public function create_log($record, ?array $options = null) { $instance = instance::get_from_instanceid($record['bigbluebuttonbnid']); $record = array_merge([ 'meetingid' => $instance->get_meeting_id(), ], (array) $record); $testlogclass = new class extends logger { /** * Log test event * * @param instance $instance * @param array $record */ public static function log_test_event(instance $instance, array $record): void { self::log( $instance, logger::EVENT_CREATE, $record ); } }; $testlogclass::log_test_event($instance, $record); } /** * Set a value in the Mock server * * @param string $name * @param mixed $value * @return void * @throws moodle_exception */ public function set_value(string $name, $value): void { if (defined('TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER')) { $this->send_mock_request('backoffice/set', [], ['name' => $name, 'value' => json_encode($value)]); } } /** * Trigger a meeting event on BBB side * * @param object $user * @param instance $instance * @param string $eventtype * @param string|null $eventdata * @return void */ public function add_meeting_event(object $user, instance $instance, string $eventtype, string $eventdata = ''): void { $this->send_mock_request('backoffice/addMeetingEvent', [ 'secret' => \mod_bigbluebuttonbn\local\config::DEFAULT_SHARED_SECRET, 'meetingID' => $instance->get_meeting_id(), 'attendeeID' => $user->id, 'attendeeName' => fullname($user), 'eventType' => $eventtype, 'eventData' => $eventdata ] ); } /** * Send all previously store events * * @param instance $instance * @return object|null */ public function send_all_events(instance $instance): ?object { if (defined('TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER')) { return $this->send_mock_request('backoffice/sendAllEvents', [ 'meetingID' => $instance->get_meeting_id(), 'sendQuery' => false, // We get the result directly here. 'secret' => \mod_bigbluebuttonbn\local\config::DEFAULT_SHARED_SECRET, ]); } return null; } /** * Reset the mock server */ public function reset_mock(): void { if (defined('TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER')) { $this->send_mock_request('backoffice/reset'); } } } PK\)t..tests/recording_test.phpnu[. /** * Recording tests. * * @package mod_bigbluebuttonbn * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) */ namespace mod_bigbluebuttonbn; use mod_bigbluebuttonbn\test\testcase_helper_trait; /** * Recording test class. * * @package mod_bigbluebuttonbn * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) * @covers \mod_bigbluebuttonbn\recording * @coversDefaultClass \mod_bigbluebuttonbn\recording */ final class recording_test extends \advanced_testcase { use testcase_helper_trait; /** * Setup for test */ public function setUp(): void { parent::setUp(); $this->initialise_mock_server(); } /** * Test for bigbluebuttonbn_get_allrecordings status refresh. * * @param int $status * @dataProvider get_status_provider * @covers ::get */ public function test_get_allrecordings_status_refresh(int $status): void { $this->resetAfterTest(); ['recordings' => $recordings] = $this->create_activity_with_recordings( $this->get_course(), instance::TYPE_ALL, [['status' => $status]] ); $this->assertEquals($status, (new recording($recordings[0]->id))->get('status')); } /** * Get name * * @covers ::get_name */ public function test_get_name(): void { $this->resetAfterTest(); ['recordings' => $recordings] = $this->create_activity_with_recordings( $this->get_course(), instance::TYPE_ALL, [['name' => 'Example name']] ); $this->assertEquals('Example name', (new recording($recordings[0]->id))->get('name')); } /** * Test get description * * @covers ::get_description */ public function test_get_description(): void { $this->resetAfterTest(); ['recordings' => $recordings] = $this->create_activity_with_recordings( $this->get_course(), instance::TYPE_ALL, [['description' => 'Example description']] ); $this->assertEquals('Example description', (new recording($recordings[0]->id))->get('description')); } /** * Get possible status * * @return array[] */ public static function get_status_provider(): array { return [ [recording::RECORDING_STATUS_PROCESSED], [recording::RECORDING_STATUS_DISMISSED], ]; } /** * Test for bigbluebuttonbn_get_allrecordings() * * @param int $type The activity type * @dataProvider get_allrecordings_types_provider * @covers ::get_recordings_for_instance */ public function test_get_allrecordings(int $type): void { $this->resetAfterTest(); $recordingcount = 2; // Two recordings only. ['activity' => $activity] = $this->create_activity_with_recordings( $this->get_course(), $type, array_pad([], $recordingcount, []) ); // Fetch the recordings for the instance. // The count shoudl match the input count. $recordings = recording::get_recordings_for_instance(instance::get_from_instanceid($activity->id)); $this->assertCount($recordingcount, $recordings); } /** * Get possible type for recording / tests * * @return array[] */ public static function get_allrecordings_types_provider(): array { return [ 'Instance Type ALL' => [ 'type' => instance::TYPE_ALL ], 'Instance Type ROOM Only' => [ 'type' => instance::TYPE_ROOM_ONLY, ], 'Instance Type Recording only' => [ 'type' => instance::TYPE_RECORDING_ONLY ], ]; } /** * Test for bigbluebuttonbn_get_allrecordings(). * * @param int $type * @dataProvider get_allrecordings_types_provider */ public function test_get_recording_for_group($type): void { $this->resetAfterTest(); $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); $testcourse = $this->getDataGenerator()->create_course(['groupmodeforce' => true, 'groupmode' => VISIBLEGROUPS]); $teacher = $this->getDataGenerator()->create_and_enrol($testcourse, 'editingteacher'); $group1 = $this->getDataGenerator()->create_group(['G1', 'courseid' => $testcourse->id]); $student1 = $this->getDataGenerator()->create_and_enrol($testcourse); $this->getDataGenerator()->create_group_member(['userid' => $student1, 'groupid' => $group1->id]); $group2 = $this->getDataGenerator()->create_group(['G2', 'courseid' => $testcourse->id]); $student2 = $this->getDataGenerator()->create_and_enrol($testcourse); $this->getDataGenerator()->create_group_member(['userid' => $student2, 'groupid' => $group2->id]); // No group. $student3 = $this->getDataGenerator()->create_and_enrol($testcourse); $activity = $plugingenerator->create_instance([ 'course' => $testcourse->id, 'type' => $type, 'name' => 'Example' ]); $instance = instance::get_from_instanceid($activity->id); $instance->set_group_id(0); $this->create_recordings_for_instance($instance, [['name' => "Pre-Recording 1"], ['name' => "Pre-Recording 2"]]); $instance->set_group_id($group1->id); $this->create_recordings_for_instance($instance, [['name' => "Group 1 Recording 1"]]); $instance->set_group_id($group2->id); $this->create_recordings_for_instance($instance, [['name' => "Group 2 Recording 1"]]); $this->setUser($student1); $instance1 = instance::get_from_instanceid($activity->id); $instance1->set_group_id($group1->id); $recordings = recording::get_recordings_for_instance($instance1); $this->assertCount(1, $recordings); $this->assert_has_recording_by_name('Group 1 Recording 1', $recordings); $this->setUser($student2); $instance2 = instance::get_from_instanceid($activity->id); $instance2->set_group_id($group2->id); $recordings = recording::get_recordings_for_instance($instance2); $this->assertCount(1, $recordings); $this->assert_has_recording_by_name('Group 2 Recording 1', $recordings); $this->setUser($student3); $instance3 = instance::get_from_instanceid($activity->id); $recordings = recording::get_recordings_for_instance($instance3); $this->assertIsArray($recordings); $this->assertCount(4, $recordings); $this->assert_has_recording_by_name('Pre-Recording 1', $recordings); $this->assert_has_recording_by_name('Pre-Recording 2', $recordings); } /** * Test that we can get recordings from a deleted activity * * @param int $type * @dataProvider get_allrecordings_types_provider */ public function test_get_recordings_from_deleted_activity($type): void { $this->resetAfterTest(true); $this->initialise_mock_server(); $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); $testcourse = $this->getDataGenerator()->create_course(); $activity = $plugingenerator->create_instance([ 'course' => $testcourse->id, 'type' => $type, 'name' => 'Example' ]); $instance = instance::get_from_instanceid($activity->id); $this->create_recordings_for_instance($instance, [['name' => "Deleted Recording 1"]]); $activity2 = $plugingenerator->create_instance([ 'course' => $testcourse->id, 'type' => $type, 'name' => 'Example' ]); $instance2 = instance::get_from_instanceid($activity2->id); $this->create_recordings_for_instance($instance2, [['name' => "Recording 1"]]); bigbluebuttonbn_delete_instance($activity->id); $recordings = recording::get_recordings_for_course($testcourse->id, [], false, false, true); $this->assertCount(2, $recordings); $this->assert_has_recording_by_name('Deleted Recording 1', $recordings); $this->assert_has_recording_by_name('Recording 1', $recordings); $recordings = recording::get_recordings_for_course($testcourse->id, [], false, false, false); $this->assertCount(1, $recordings); } /** * Check that a recording exist in the list of recordings * * @param string $recordingname * @param array $recordings */ public function assert_has_recording_by_name($recordingname, $recordings) { $recordingnames = array_map(function ($r) { return $r->get('name'); }, $recordings); $this->assertContains($recordingname, $recordingnames); } /** * Simple recording with breakoutroom fetcher test * * @return void */ public function test_recordings_breakoutroom(): void { $this->resetAfterTest(); $this->initialise_mock_server(); [$context, $cm, $bbbactivity] = $this->create_instance(); $instance = instance::get_from_instanceid($bbbactivity->id); $bbbgenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); $mainmeeting = $bbbgenerator->create_meeting([ 'instanceid' => $instance->get_instance_id(), 'groupid' => $instance->get_group_id(), ]); // This creates a meeting to receive the recordings (specific to the mock server implementation). See recording_proxy_test. $bbbgenerator->create_meeting([ 'instanceid' => $instance->get_instance_id(), 'groupid' => $instance->get_group_id(), 'isBreakout' => true, 'sequence' => 1 ]); $bbbgenerator->create_meeting([ 'instanceid' => $instance->get_instance_id(), 'groupid' => $instance->get_group_id(), 'isBreakout' => true, 'sequence' => 2 ]); // For now only recording from the main room have been created. $this->create_recordings_for_instance($instance, [ ['name' => 'Recording 1'], ]); $recordings = recording::get_recordings_for_instance($instance); $this->assertCount(1, $recordings); // Now the breakoutroom recordings appears. $this->create_recordings_for_instance($instance, [ ['name' => 'Recording 2', 'isBreakout' => true, 'sequence' => 1], ['name' => 'Recording 3', 'isBreakout' => true, 'sequence' => 2] ]); $recordings = recording::get_recordings_for_instance($instance); $this->assertCount(3, $recordings); } } PK\jǪFbb5tests/external/get_bigbluebuttons_by_courses_test.phpnu[. namespace mod_bigbluebuttonbn\external; use core_external\external_api; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\test\testcase_helper_trait; use moodle_exception; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); /** * Tests for the get_bigbluebuttons_by_courses class. * * @package mod_bigbluebuttonbn * @category test * @copyright 2021 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) * @covers \mod_bigbluebuttonbn\external\get_bigbluebuttonbns_by_courses */ final class get_bigbluebuttons_by_courses_test extends \externallib_advanced_testcase { use testcase_helper_trait; /** * Setup for test */ public function setUp(): void { parent::setUp(); $this->initialise_mock_server(); } /** * Helper * * @param mixed ...$params * @return mixed */ protected function get_bigbluebuttons_by_courses(...$params) { $returnvalue = get_bigbluebuttonbns_by_courses::execute(...$params); return external_api::clean_returnvalue(get_bigbluebuttonbns_by_courses::execute_returns(), $returnvalue); } /** * Test execute API CALL with no instance */ public function test_execute_no_instance(): void { $this->resetAfterTest(); $bbbactivities = $this->get_bigbluebuttons_by_courses([1234, 5678]); $this->assertIsArray($bbbactivities); $this->assertArrayHasKey('bigbluebuttonbns', $bbbactivities); $this->assertArrayHasKey('warnings', $bbbactivities); $this->assertEmpty($bbbactivities['bigbluebuttonbns']); } /** * Test execute API CALL without login */ public function test_execute_without_login(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $record = $this->getDataGenerator()->create_module('bigbluebuttonbn', ['course' => $course->id]); $instance = instance::get_from_instanceid($record->id); $this->expectException(moodle_exception::class); $this->get_bigbluebuttons_by_courses($instance->get_cm_id()); } /** * Test execute API CALL with invalid login */ public function test_execute_with_invalid_login(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); $instance = instance::get_from_instanceid($record->id); $user = $generator->create_user(); $this->setUser($user); $this->expectException(moodle_exception::class); $this->get_bigbluebuttons_by_courses($instance->get_cm_id()); } /** * Test get bbbactivities */ public function test_execute_with_valid_login(): void { $this->resetAfterTest(true); $generator = $this->getDataGenerator(); // Create a user. $user = $generator->create_user(); // Create courses to add the modules. $course1 = $generator->create_course(); $course2 = $generator->create_course(); $bbbs = []; // First bbb activity. $bbbs[] = $generator->create_module('bigbluebuttonbn', ['course' => $course1->id]); // Second bbb activity. $bbbs[] = $generator->create_module('bigbluebuttonbn', ['course' => $course2->id]); $generator->enrol_user($user->id, $course1->id, null, 'manual'); $generator->enrol_user($user->id, $course2->id, null, 'manual'); // Set to the user. self::setUser($user); // Call the external function passing course ids. $bbbactivities = $this->get_bigbluebuttons_by_courses([$course1->id, $course2->id]); $this->assert_same_bbb_activities($bbbs, $bbbactivities['bigbluebuttonbns']); // Call the external function without passing course id. $bbbactivities = $this->get_bigbluebuttons_by_courses(); $this->assert_same_bbb_activities($bbbs, $bbbactivities['bigbluebuttonbns']); // Unenrol user from second course and alter expected bbb activity. $enrol = enrol_get_plugin('manual'); $enrolinstances = enrol_get_instances($course2->id, true); foreach ($enrolinstances as $courseenrolinstance) { if ($courseenrolinstance->enrol == "manual") { $instance2 = $courseenrolinstance; break; } } $enrol->unenrol_user($instance2, $user->id); // Call the external function without passing course id. $bbbactivities = $this->get_bigbluebuttons_by_courses(); $this->assertCount(1, $bbbactivities['bigbluebuttonbns']); $this->assertEquals($bbbs[0]->id, $bbbactivities['bigbluebuttonbns'][0]['id']); // Call for the second course we unenrolled the user from. $bbbactivities = $this->get_bigbluebuttons_by_courses([$course2->id]); $this->assertCount(0, $bbbactivities['bigbluebuttonbns']); } /** * Check if the two arrays containing the activities are the same. * * @param mixed $expected * @param mixed $actual */ protected function assert_same_bbb_activities($expected, $actual) { $this->assertCount(count($expected), $actual); $getids = function($bbb) { return is_array($bbb) ? $bbb['id'] : $bbb->id; }; $expectedids = array_map($getids, $expected); $actualid = array_map($getids, $actual); sort($expectedids); sort($actualid); $this->assertEquals($expectedids, $actualid); } } PK\"+tests/external/completion_validate_test.phpnu[. namespace mod_bigbluebuttonbn\external; use core_external\external_api; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\test\testcase_helper_trait; use require_login_exception; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); /** * Tests for the completion_validate class. * * @package mod_bigbluebuttonbn * @category test * @copyright 2021 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) * @covers \mod_bigbluebuttonbn\external\completion_validate */ final class completion_validate_test extends \externallib_advanced_testcase { use testcase_helper_trait; /** * Setup for test */ public function setUp(): void { parent::setUp(); $this->initialise_mock_server(); } /** * Helper * * @param mixed ...$params * @return mixed */ protected function completion_validate(...$params) { $returnvalue = completion_validate::execute(...$params); return external_api::clean_returnvalue(completion_validate::execute_returns(), $returnvalue); } /** * Test execute API CALL with no instance */ public function test_execute_no_instance(): void { $this->resetAfterTest(); $result = $this->completion_validate(1234); $this->assertIsArray($result); $this->assertArrayHasKey('warnings', $result); $this->assertEquals([ 'item' => 'mod_bigbluebuttonbn', 'itemid' => 1234, 'warningcode' => 'indexerrorbbtn', 'message' => 'BigBlueButton ID 1234 is incorrect', ], $result['warnings'][0]); } /** * Test execute API CALL without login */ public function test_execute_without_login(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $record = $this->getDataGenerator()->create_module('bigbluebuttonbn', ['course' => $course->id]); $instance = instance::get_from_instanceid($record->id); $this->expectException(require_login_exception::class); $this->completion_validate($instance->get_instance_id()); } /** * Test execute API CALL with invalid login */ public function test_execute_with_invalid_login(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); $instance = instance::get_from_instanceid($record->id); $user = $generator->create_user($course); $this->setUser($user); $this->expectException(require_login_exception::class); $this->completion_validate($instance->get_instance_id()); } /** * When login as a student */ public function test_execute_with_valid_login_but_student(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); $instance = instance::get_from_instanceid($record->id); $user = $generator->create_and_enrol($course, 'student'); $this->setUser($user); $returnvalue = $this->completion_validate($instance->get_instance_id()); $this->assertArrayHasKey('warnings', $returnvalue); $this->assertEquals([ 'item' => 'mod_bigbluebuttonbn', 'itemid' => $record->id, 'warningcode' => 'nopermissions', 'message' => 'Sorry, but you do not currently have permissions to do that (completion_validate).', ], $returnvalue['warnings'][0]); } /** * When login as a student */ public function test_execute_with_valid_login_with_teacher(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); $instance = instance::get_from_instanceid($record->id); $user = $generator->create_and_enrol($course, 'editingteacher'); $this->setUser($user); $returnvalue = $this->completion_validate($instance->get_instance_id()); $this->assertArrayHasKey('warnings', $returnvalue); $this->assertEmpty($returnvalue['warnings']); } } PK\_|E=R=R&tests/external/get_recordings_test.phpnu[. namespace mod_bigbluebuttonbn\external; use core_external\external_api; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\test\testcase_helper_trait; use require_login_exception; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); /** * Tests for the update_course class. * * @package mod_bigbluebuttonbn * @category test * @copyright 2021 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) * @covers \mod_bigbluebuttonbn\external\get_recordings */ final class get_recordings_test extends \externallib_advanced_testcase { use testcase_helper_trait; /** * Setup for test */ public function setUp(): void { parent::setUp(); $this->initialise_mock_server(); } /** * Helper * * @param mixed ...$params * @return array|bool|mixed */ protected function get_recordings(...$params) { $recordings = get_recordings::execute(...$params); return external_api::clean_returnvalue(get_recordings::execute_returns(), $recordings); } /** * Test execute API CALL with no instance */ public function test_execute_wrong_instance(): void { $this->resetAfterTest(); $getrecordings = $this->get_recordings(1234); $this->assertIsArray($getrecordings); $this->assertArrayHasKey('status', $getrecordings); $this->assertEquals(false, $getrecordings['status']); $this->assertStringContainsString('nosuchinstance', $getrecordings['warnings'][0]['warningcode']); } /** * Test execute API CALL without login */ public function test_execute_without_login(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $record = $this->getDataGenerator()->create_module('bigbluebuttonbn', ['course' => $course->id]); $instance = instance::get_from_instanceid($record->id); $this->expectException(require_login_exception::class); $this->get_recordings($instance->get_instance_id()); } /** * Test execute API CALL with invalid login */ public function test_execute_with_invalid_login(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); $instance = instance::get_from_instanceid($record->id); $user = $generator->create_user(); $this->setUser($user); $this->expectException(require_login_exception::class); $this->get_recordings($instance->get_instance_id()); } /** * When login as a student */ public function test_execute_with_valid_login(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); $instance = instance::get_from_instanceid($record->id); $user = $generator->create_and_enrol($course, 'student'); $this->setUser($user); $getrecordings = $this->get_recordings($instance->get_instance_id()); $this->assertIsArray($getrecordings); $this->assertArrayHasKey('status', $getrecordings); $this->assertEquals(true, $getrecordings['status']); $this->assertNotEmpty($getrecordings['tabledata']); $this->assertEquals('[]', $getrecordings['tabledata']['data']); } /** * Check if tools are present for teacher/moderator */ public function test_get_recordings_tools(): void { $this->resetAfterTest(); $dataset = [ 'type' => instance::TYPE_ALL, 'groups' => null, 'users' => [['username' => 't1', 'role' => 'editingteacher'], ['username' => 's1', 'role' => 'student']], 'recordingsdata' => [ [['name' => 'Recording1']], [['name' => 'Recording2']] ], ]; $activityid = $this->create_from_dataset($dataset); $instance = instance::get_from_instanceid($activityid); $context = \context_course::instance($instance->get_course_id()); foreach ($dataset['users'] as $userdef) { $user = \core_user::get_user_by_username($userdef['username']); $this->setUser($user); $getrecordings = $this->get_recordings($instance->get_instance_id()); // Check users see or do not see recording dependings on their groups. foreach ($dataset['recordingsdata'] as $recordingdata) { foreach ($recordingdata as $recording) { if (has_capability('moodle/course:update', $context)) { $this->assertStringContainsString('data-action=\"delete\"', $getrecordings['tabledata']['data'], "User $user->username, should be able to delete the recording {$recording['name']}"); $this->assertStringContainsString('data-action=\"publish\"', $getrecordings['tabledata']['data'], "User $user->username, should be able to publish the recording {$recording['name']}"); } else { $this->assertStringNotContainsString('data-action=\"delete\"', $getrecordings['tabledata']['data'], "User $user->username, should not be able to delete the recording {$recording['name']}"); } } } } // Now without delete. foreach ($dataset['users'] as $userdef) { $user = \core_user::get_user_by_username($userdef['username']); $this->setUser($user); $getrecordings = $this->get_recordings($instance->get_instance_id(), 'protect'); // Check users see or do not see recording dependings on their groups. foreach ($dataset['recordingsdata'] as $recordingdata) { foreach ($recordingdata as $recording) { $this->assertStringNotContainsString('data-action=\"delete\"', $getrecordings['tabledata']['data'], "User $user->username, should not be able to delete the recording {$recording['name']}"); } } } } /** * Check preview is present and displayed */ public function test_get_recordings_preview(): void { $this->resetAfterTest(); $dataset = [ 'type' => instance::TYPE_ALL, 'additionalsettings' => [ 'recordings_preview' => 1 ], 'groups' => null, 'users' => [['username' => 't1', 'role' => 'editingteacher'], ['username' => 's1', 'role' => 'student']], 'recordingsdata' => [ [['name' => 'Recording1']], [['name' => 'Recording2']] ], ]; $activityid = $this->create_from_dataset($dataset); $instance = instance::get_from_instanceid($activityid); $context = \context_course::instance($instance->get_course_id()); foreach ($dataset['users'] as $userdef) { $user = \core_user::get_user_by_username($userdef['username']); $this->setUser($user); $getrecordings = $this->get_recordings($instance->get_instance_id()); $this->assertNotEmpty($getrecordings['tabledata']['columns']['3']); $this->assertEquals('preview', $getrecordings['tabledata']['columns']['3']['key']); } } /** * Check we can see all recording from a course in a room only instance * @covers \mod_bigbluebuttonbn\external\get_recordings::execute */ public function test_get_recordings_room_only(): void { $this->resetAfterTest(); set_config('bigbluebuttonbn_importrecordings_enabled', 1); $dataset = [ 'type' => instance::TYPE_ALL, 'groups' => null, 'users' => [['username' => 't1', 'role' => 'editingteacher'], ['username' => 's1', 'role' => 'student']], 'recordingsdata' => [ [['name' => 'Recording1']], [['name' => 'Recording2']] ], ]; $activityid = $this->create_from_dataset($dataset); $instance = instance::get_from_instanceid($activityid); // Now create a recording only activity. $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); // Now create a new activity and import the first record. $newactivity = $plugingenerator->create_instance([ 'course' => $instance->get_course_id(), 'type' => instance::TYPE_RECORDING_ONLY, 'name' => 'Example 2' ]); $plugingenerator->create_meeting([ 'instanceid' => $newactivity->id, ]); // We need to have a meeting created in order to import recordings. $newinstance = instance::get_from_instanceid($newactivity->id); $this->create_recordings_for_instance($newinstance, [['name' => 'Recording3']]); foreach ($dataset['users'] as $userdef) { $user = \core_user::get_user_by_username($userdef['username']); $this->setUser($user); $getrecordings = $this->get_recordings($newinstance->get_instance_id()); // Check users see or do not see recording dependings on their groups. $data = json_decode($getrecordings['tabledata']['data']); $this->assertCount(3, $data); } } /** * Check if we can see the imported recording in a new instance * @covers \mod_bigbluebuttonbn\external\get_recordings::execute */ public function test_get_recordings_imported(): void { $this->resetAfterTest(); set_config('bigbluebuttonbn_importrecordings_enabled', 1); $dataset = [ 'type' => instance::TYPE_ALL, 'groups' => null, 'users' => [['username' => 't1', 'role' => 'editingteacher'], ['username' => 's1', 'role' => 'student']], 'recordingsdata' => [ [['name' => 'Recording1']], [['name' => 'Recording2']] ], ]; $activityid = $this->create_from_dataset($dataset); $instance = instance::get_from_instanceid($activityid); $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); // Now create a new activity and import the first record. $newactivity = $plugingenerator->create_instance([ 'course' => $instance->get_course_id(), 'type' => instance::TYPE_ALL, 'name' => 'Example 2' ]); $plugingenerator->create_meeting([ 'instanceid' => $newactivity->id, ]); // We need to have a meeting created in order to import recordings. $newinstance = instance::get_from_instanceid($newactivity->id); $recordings = $instance->get_recordings(); foreach ($recordings as $recording) { if ($recording->get('name') == 'Recording1') { $recording->create_imported_recording($newinstance); } } foreach ($dataset['users'] as $userdef) { $user = \core_user::get_user_by_username($userdef['username']); $this->setUser($user); $getrecordings = $this->get_recordings($newinstance->get_instance_id()); // Check users see or do not see recording dependings on their groups. foreach ($dataset['recordingsdata'] as $index => $recordingdata) { foreach ($recordingdata as $recording) { if ($instance->can_manage_recordings()) { $this->assertStringContainsString('data-action=\"delete\"', $getrecordings['tabledata']['data'], "User $user->username, should be able to delete the recording {$recording['name']}"); } else { $this->assertStringNotContainsString('data-action=\"delete\"', $getrecordings['tabledata']['data'], "User $user->username, should not be able to delete the recording {$recording['name']}"); } if ($index === 0) { $this->assertStringContainsString($recording['name'], $getrecordings['tabledata']['data']); } else { $this->assertStringNotContainsString($recording['name'], $getrecordings['tabledata']['data']); } } } } } /** * Check we can see only imported recordings in a recordings only instance when "Show only imported links" enabled. * @covers \mod_bigbluebuttonbn\external\get_recordings::execute */ public function test_get_imported_recordings_only(): void { $this->resetAfterTest(); set_config('bigbluebuttonbn_importrecordings_enabled', 1); $dataset = [ 'type' => instance::TYPE_ALL, 'groups' => null, 'users' => [['username' => 's1', 'role' => 'student']], 'recordingsdata' => [ [['name' => 'Recording1']], [['name' => 'Recording2']] ], ]; $activityid = $this->create_from_dataset($dataset); $instance = instance::get_from_instanceid($activityid); // Now create a recording only activity. $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); // Now create a new activity and import the first record. $newactivity = $plugingenerator->create_instance([ 'course' => $instance->get_course_id(), 'type' => instance::TYPE_RECORDING_ONLY, 'name' => 'Example 2' ]); $plugingenerator->create_meeting([ 'instanceid' => $newactivity->id, ]); // We need to have a meeting created in order to import recordings. $newinstance = instance::get_from_instanceid($newactivity->id); $recordings = $instance->get_recordings(); foreach ($recordings as $recording) { if ($recording->get('name') == 'Recording1') { $recording->create_imported_recording($newinstance); } } $user = \core_user::get_user_by_username('s1'); $this->setUser($user); $getrecordings = $this->get_recordings($newinstance->get_instance_id()); $data = json_decode($getrecordings['tabledata']['data']); // Check that all recordings including the imported recording appear. $this->assertCount(3, $data); // Set the flags to enable "Show only imported links". set_config('bigbluebuttonbn_recordings_imported_default', 1); set_config('bigbluebuttonbn_recordings_imported_editable', 0); $getrecordings = $this->get_recordings($newinstance->get_instance_id()); $data = json_decode($getrecordings['tabledata']['data']); $this->assertCount(1, $data); } /** * Check if recording are visible/invisible depending on the group. * * @param string $type * @param array $groups * @param array $users * @param array $recordingsdata * @param array $test * @param int $coursemode * * @covers \mod_bigbluebuttonbn\external\get_recordings::execute * @dataProvider recording_group_test_data */ public function test_get_recordings_groups($type, $groups, $users, $recordingsdata, $test, $coursemode): void { $this->resetAfterTest(); $dataset = compact('type', 'groups', 'users', 'recordingsdata', 'test', 'coursemode'); $activityid = $this->create_from_dataset($dataset); $instance = instance::get_from_instanceid($activityid); foreach ($dataset['users'] as $userdef) { $user = \core_user::get_user_by_username($userdef['username']); $this->setUser($user); $groups = array_values(groups_get_my_groups()); $mygroup = !empty($groups) ? end($groups) : null; $getrecordings = $this->get_recordings( $instance->get_instance_id(), null, !empty($mygroup) ? $mygroup->id : null); $allrecordingsnames = []; foreach ($recordingsdata as $groups => $rsinfo) { $rnames = array_map(function($rdata) { return $rdata['name']; }, $rsinfo); $allrecordingsnames = array_merge($allrecordingsnames, $rnames); } // Check users see or do not see recording dependings on their groups. foreach ($dataset['test'][$user->username] as $viewablerecordings) { $viewablerecordings = $dataset['test'][$user->username]; $invisiblerecordings = array_diff($allrecordingsnames, $viewablerecordings); foreach ($viewablerecordings as $viewablerecordingname) { $this->assertStringContainsString($viewablerecordingname, $getrecordings['tabledata']['data'], "User $user->username, should see recording {$viewablerecordingname}"); } foreach ($invisiblerecordings as $invisiblerecordingname) { $this->assertStringNotContainsString($invisiblerecordingname, $getrecordings['tabledata']['data'], "User $user->username, should not see recording {$viewablerecordingname}"); } } } } /** * Recording group test * * @return array[] */ public static function recording_group_test_data(): array { return [ 'visiblegroups' => [ 'type' => instance::TYPE_ALL, 'groups' => ['G1' => ['s1'], 'G2' => ['s2']], 'users' => [ ['username' => 't1', 'role' => 'editingteacher'], ['username' => 's1', 'role' => 'student'], ['username' => 's2', 'role' => 'student'], ['username' => 's3', 'role' => 'student'] ], 'recordingsdata' => [ 'G1' => [['name' => 'Recording1']], 'G2' => [['name' => 'Recording2']], '' => [['name' => 'Recording3']] ], 'test' => [ 't1' => ['Recording1', 'Recording2', 'Recording3'], // A moderator should see all recordings. 's1' => ['Recording1'], // S1 can only see the recordings from his group. 's2' => ['Recording2'], // S2 can only see the recordings from his group. 's3' => ['Recording3', 'Recording2', 'Recording1'] // S3 should see recordings which have no groups and his groups's recording. ], 'coursemode' => VISIBLEGROUPS ], 'separategroups' => [ 'type' => instance::TYPE_ALL, 'groups' => ['G1' => ['s1'], 'G2' => ['s2']], 'users' => [ ['username' => 't1', 'role' => 'editingteacher'], ['username' => 's1', 'role' => 'student'], ['username' => 's2', 'role' => 'student'] ], 'recordingsdata' => [ 'G1' => [['name' => 'Recording1']], 'G2' => [['name' => 'Recording2']], '' => [['name' => 'Recording3']] ], 'test' => [ 't1' => ['Recording1', 'Recording2', 'Recording3'], // A moderator should see all recordings. 's1' => ['Recording1'], // S1 can only see the recordings from his group. 's2' => ['Recording2'], // S2 can only see the recordings from his group. 's3' => ['Recording3'] // S3 should see recordings which have no groups. ], 'coursemode' => SEPARATEGROUPS ] ]; } } PK\xx tests/external/can_join_test.phpnu[. namespace mod_bigbluebuttonbn\external; use core_external\external_api; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\test\testcase_helper_trait; use moodle_exception; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); /** * Tests for the update_course class. * * @package mod_bigbluebuttonbn * @category test * @copyright 2021 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \mod_bigbluebuttonbn\external\can_join */ final class can_join_test extends \externallib_advanced_testcase { use testcase_helper_trait; /** * Setup for test */ public function setUp(): void { parent::setUp(); $this->initialise_mock_server(); } /** * Helper * * @param mixed ...$params * @return mixed */ protected function can_join(...$params) { $canjoin = can_join::execute(...$params); return external_api::clean_returnvalue(can_join::execute_returns(), $canjoin); } /** * Test execute API CALL with no instance */ public function test_execute_no_instance(): void { $this->resetAfterTest(); $canjoin = $this->can_join(1234, 5678); $this->assertIsArray($canjoin); $this->assertArrayHasKey('can_join', $canjoin); $this->assertEquals(false, $canjoin['can_join']); } /** * Test execute API CALL without login */ public function test_execute_without_login(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $record = $this->getDataGenerator()->create_module('bigbluebuttonbn', ['course' => $course->id]); $instance = instance::get_from_instanceid($record->id); $this->expectException(moodle_exception::class); $this->can_join($instance->get_cm_id()); } /** * Test execute API CALL with invalid login */ public function test_execute_with_invalid_login(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); $instance = instance::get_from_instanceid($record->id); $user = $generator->create_user(); $this->setUser($user); $this->expectException(moodle_exception::class); $this->can_join($instance->get_cm_id()); } /** * When login as a student */ public function test_execute_with_valid_login(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); $instance = instance::get_from_instanceid($record->id); $user = $generator->create_and_enrol($course, 'student'); $this->setUser($user); $canjoin = $this->can_join($instance->get_cm_id()); $this->assertIsArray($canjoin); $this->assertArrayHasKey('can_join', $canjoin); $this->assertEquals(true, $canjoin['can_join']); } } PK\rOO#tests/external/end_meeting_test.phpnu[. namespace mod_bigbluebuttonbn\external; use core_external\external_api; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\meeting; use mod_bigbluebuttonbn\test\testcase_helper_trait; use moodle_exception; use require_login_exception; use core_external\restricted_context_exception; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); /** * Tests for the update_course class. * * @package mod_bigbluebuttonbn * @copyright 2021 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \mod_bigbluebuttonbn\external\end_meeting */ final class end_meeting_test extends \externallib_advanced_testcase { use testcase_helper_trait; /** * Setup for test */ public function setUp(): void { parent::setUp(); $this->initialise_mock_server(); } /** * Helper * * @param mixed ...$params * @return array|bool|mixed */ protected function end_meeting(...$params) { $returnvalue = end_meeting::execute(...$params); return external_api::clean_returnvalue(end_meeting::execute_returns(), $returnvalue); } /** * Test execute API CALL with no instance */ public function test_execute_no_instance(): void { $this->resetAfterTest(); $this->expectException(moodle_exception::class); $endmeeting = $this->end_meeting(1234, 5678); } /** * Test execute API CALL without login */ public function test_execute_without_login(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $record = $this->getDataGenerator()->create_module('bigbluebuttonbn', ['course' => $course->id]); $instance = instance::get_from_instanceid($record->id); $this->expectException(require_login_exception::class); $this->end_meeting($instance->get_instance_id(), 0); } /** * Test execute API CALL with invalid login */ public function test_execute_with_invalid_login(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); $instance = instance::get_from_instanceid($record->id); $user = $generator->create_user(); $this->setUser($user); $this->expectException(require_login_exception::class); $this->end_meeting($instance->get_instance_id(), 0); } /** * When login as a student */ public function test_execute_with_student_login(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); $instance = instance::get_from_instanceid($record->id); $user = $generator->create_and_enrol($course, 'student'); $this->setUser($user); $this->expectException(restricted_context_exception::class); $this->end_meeting($instance->get_instance_id(), 0); } /** * Test execute admin logic */ public function test_execute_with_admin_login(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); $instance = instance::get_from_instanceid($record->id); $plugingenerator = $generator->get_plugin_generator('mod_bigbluebuttonbn'); $plugingenerator->create_meeting([ 'instanceid' => $instance->get_instance_id(), ]); $this->setAdminUser(); $result = $this->end_meeting($instance->get_instance_id(), 0); $this->assertIsArray($result); // TODO Check that the meeting was ended on the remote. } /** * Test execute admin logic */ public function test_execute_end_meeting_already_ended(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); $instance = instance::get_from_instanceid($record->id); $plugingenerator = $generator->get_plugin_generator('mod_bigbluebuttonbn'); $plugingenerator->create_meeting([ 'instanceid' => $instance->get_instance_id(), ]); // Then end the meeting. // Execute the end command. $meeting = new meeting($instance); $meeting->end_meeting(); $this->setAdminUser(); $result = $this->end_meeting($instance->get_instance_id(), 0); $this->assertIsArray($result); // TODO Check that the meeting was ended on the remote. } } PK\㿪&&$tests/external/get_join_url_test.phpnu[. namespace mod_bigbluebuttonbn\external; use context_course; use core_external\external_api; use core_external\restricted_context_exception; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\test\testcase_helper_trait; use moodle_exception; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); /** * Tests for the get_join_url class. * * @package mod_bigbluebuttonbn * @category test * @copyright 2021 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) * @covers \mod_bigbluebuttonbn\external\get_join_url */ final class get_join_url_test extends \externallib_advanced_testcase { use testcase_helper_trait; /** * Setup for test */ public function setUp(): void { parent::setUp(); $this->initialise_mock_server(); } /** * Helper * * @param mixed ...$params * @return mixed */ protected function get_join_url(...$params) { $getjoinurl = get_join_url::execute(...$params); return external_api::clean_returnvalue(get_join_url::execute_returns(), $getjoinurl); } /** * Test execute API CALL with no instance */ public function test_execute_no_instance(): void { $this->resetAfterTest(); $this->expectExceptionMessageMatches('/No such instance.*/'); $joinurl = $this->get_join_url(1234, 5678); $this->assertIsArray($joinurl); $this->assertArrayNotHasKey('join_url', $joinurl); $this->assertEquals(false, $joinurl['join_url']); } /** * Test execute API CALL without login */ public function test_execute_without_login(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $record = $this->getDataGenerator()->create_module('bigbluebuttonbn', ['course' => $course->id]); $instance = instance::get_from_instanceid($record->id); $this->expectException(moodle_exception::class); $this->get_join_url($instance->get_cm_id()); } /** * Test execution with a user who doesn't have the capability to join the meeting */ public function test_execute_without_capability(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $record = $this->getDataGenerator()->create_module('bigbluebuttonbn', ['course' => $course->id]); $instance = instance::get_from_instanceid($record->id); $user = $this->getDataGenerator()->create_and_enrol($course); $this->setUser($user); $student = $DB->get_field('role', 'id', ['shortname' => 'student'], MUST_EXIST); assign_capability('mod/bigbluebuttonbn:join', CAP_PROHIBIT, $student, context_course::instance($course->id), true); $this->expectException(restricted_context_exception::class); $this->get_join_url($instance->get_cm_id()); } /** * Test execute API CALL with invalid login */ public function test_execute_with_invalid_login(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); $instance = instance::get_from_instanceid($record->id); $user = $generator->create_user(); $this->setUser($user); $this->expectException(moodle_exception::class); $this->get_join_url($instance->get_cm_id()); } /** * When login as a student */ public function test_execute_with_valid_login(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); $instance = instance::get_from_instanceid($record->id); $user = $generator->create_and_enrol($course, 'student'); $bbbgenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); // Make sure the meeting is running (this is not the default with the mock server). $bbbgenerator->create_meeting([ 'instanceid' => $instance->get_instance_id(), 'groupid' => $instance->get_group_id() ]); $this->setUser($user); $joinurl = $this->get_join_url($instance->get_cm_id()); $this->assertIsArray($joinurl); $this->assertArrayHasKey('join_url', $joinurl); $this->assertEmpty($joinurl['warnings']); $this->assertNotNull($joinurl['join_url']); } /** * Check that URL are different depending on the group. */ public function test_execute_with_group(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $g1 = $generator->create_group(['courseid' => $course->id]); $g2 = $generator->create_group(['courseid' => $course->id]); $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); $instance = instance::get_from_instanceid($record->id); $user = $generator->create_and_enrol($course, 'student'); $bbbgenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); // Make sure the meeting is running (this is not the default with the mock server). $bbbgenerator->create_meeting([ 'instanceid' => $instance->get_instance_id(), 'groupid' => $instance->get_group_id() ]); $this->setUser($user); $joinurlnogroup = $this->get_join_url($instance->get_cm_id()); $joinurlnogroupv2 = $this->get_join_url($instance->get_cm_id()); $joinurlg1 = $this->get_join_url($instance->get_cm_id(), $g1->id); $joinurlg2 = $this->get_join_url($instance->get_cm_id(), $g2->id); foreach ([$joinurlnogroup, $joinurlnogroupv2, $joinurlg1, $joinurlg2] as $join) { $this->assertIsArray($join); $this->assertArrayHasKey('join_url', $join); $this->assertEmpty($join['warnings']); $this->assertNotNull($join['join_url']); } $this->assertNotEquals($joinurlnogroup['join_url'], $joinurlg1['join_url']); $this->assertNotEquals($joinurlg2['join_url'], $joinurlg1['join_url']); } /** * Check that we return the same URL once meeting is started. */ public function test_execute_with_same_url(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); $instance = instance::get_from_instanceid($record->id); $user = $generator->create_and_enrol($course, 'student'); $bbbgenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); // Make sure the meeting is running (this is not the default with the mock server). $bbbgenerator->create_meeting([ 'instanceid' => $instance->get_instance_id(), 'groupid' => $instance->get_group_id() ]); $this->setUser($user); $joinurl = $this->get_join_url($instance->get_cm_id()); $joinurlv2 = $this->get_join_url($instance->get_cm_id()); foreach ([$joinurl, $joinurlv2] as $join) { $this->assertIsArray($join); $this->assertArrayHasKey('join_url', $join); $this->assertEmpty($join['warnings']); $this->assertNotNull($join['join_url']); } $this->assertEquals($joinurl['join_url'], $joinurlv2['join_url']); } /** * Check that we return the same URL once meeting is started. */ public function test_user_limit(): void { $this->resetAfterTest(); set_config('bigbluebuttonbn_userlimit_editable', true); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id, 'userlimit' => 2]); $user1 = $generator->create_and_enrol($course, 'student'); $user2 = $generator->create_and_enrol($course, 'student'); $instance = instance::get_from_instanceid($record->id); $bbbgenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); // Make sure the meeting is running (this is not the default with the mock server). $bbbgenerator->create_meeting([ 'instanceid' => $instance->get_instance_id(), 'groupid' => $instance->get_group_id(), 'participants' => 2 ]); $this->setUser($user1); $joinurl = $this->get_join_url($instance->get_cm_id()); $this->assertNotNull($joinurl['warnings']); $this->assertEquals('userlimitreached', $joinurl['warnings'][0]['warningcode']); } } PK\+;D,tests/external/view_bigbluebuttonbn_test.phpnu[. namespace mod_bigbluebuttonbn\external; use core_external\external_api; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\test\testcase_helper_trait; use require_login_exception; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); /** * Tests for the view_bigbluebuttonbn class. * * @package mod_bigbluebuttonbn * @category test * @copyright 2021 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) * @covers \mod_bigbluebuttonbn\external\view_bigbluebuttonbn */ final class view_bigbluebuttonbn_test extends \externallib_advanced_testcase { use testcase_helper_trait; /** * Setup for test */ public function setUp(): void { parent::setUp(); $this->initialise_mock_server(); } /** * Helper * * @param mixed ...$params * @return mixed */ protected function view_bigbluebuttonbn(...$params) { $returnvalue = view_bigbluebuttonbn::execute(...$params); return external_api::clean_returnvalue(view_bigbluebuttonbn::execute_returns(), $returnvalue); } /** * Test execute API CALL with no instance */ public function test_execute_no_instance(): void { $this->resetAfterTest(); $bbbactivities = $this->view_bigbluebuttonbn(1234); $this->assertIsArray($bbbactivities); $this->assertArrayHasKey('status', $bbbactivities); $this->assertArrayHasKey('warnings', $bbbactivities); $this->assertFalse($bbbactivities['status']); } /** * Test execute API CALL without login */ public function test_execute_without_login(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $record = $this->getDataGenerator()->create_module('bigbluebuttonbn', ['course' => $course->id]); $instance = instance::get_from_instanceid($record->id); $this->expectException(require_login_exception::class); $this->view_bigbluebuttonbn($instance->get_instance_id()); } /** * Test execute API CALL with invalid login */ public function test_execute_with_invalid_login(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); $instance = instance::get_from_instanceid($record->id); $user = $generator->create_user($course); $this->setUser($user); $this->expectException(require_login_exception::class); $this->view_bigbluebuttonbn($instance->get_instance_id()); } /** * When login as a student */ public function test_execute_with_valid_login(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); $instance = instance::get_from_instanceid($record->id); $user = $generator->create_and_enrol($course, 'student'); $this->setUser($user); $returnvalue = $this->view_bigbluebuttonbn($instance->get_instance_id()); $this->assertArrayHasKey('status', $returnvalue); $this->assertArrayHasKey('warnings', $returnvalue); $this->assertTrue($returnvalue['status']); } } PK\{`tests/coverage.phpnu[. /** * Coverage information for the mod_bigbluebuttonbn component. * * @package mod_bigbluebuttonbn * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ defined('MOODLE_INTERNAL') || die(); /** * Class bbb_coverage information for the core subsystem. * * Note that we had to change the definition of this class due to a bug in local_moodlecheck * https://github.com/moodlehq/moodle-local_moodlecheck/issues/50 * @package mod_bigbluebuttonbn * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ class bbb_coverage extends phpunit_coverage_info { /** @var array The list of folders relative to the plugin root to includelist in coverage generation. */ protected $includelistfolders = [ 'classes', 'backup', ]; /** @var array The list of files relative to the plugin root to includelist in coverage generation. */ protected $includelistfiles = [ 'lib.php', ]; /** @var array The list of folders relative to the plugin root to excludelist in coverage generation. */ protected $excludelistfolders = []; /** @var array The list of files relative to the plugin root to excludelist in coverage generation. */ protected $excludelistfiles = []; } return new bbb_coverage; PK\ / /tests/privacy/provider_test.phpnu[. /** * Privacy provider tests. * * @package mod_bigbluebuttonbn * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) */ namespace mod_bigbluebuttonbn\privacy; use context_module; 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\userlist; /** * Privacy provider tests class. * * @package mod_bigbluebuttonbn * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) * @covers \mod_bigbluebuttonbn\privacy\provider */ final class provider_test extends \core_privacy\tests\provider_testcase { /** * Test for provider::get_metadata(). */ public function test_get_metadata(): void { $this->resetAfterTest(true); $collection = new collection('mod_bigbluebuttonbn'); $newcollection = provider::get_metadata($collection); $itemcollection = $newcollection->get_collection(); $this->assertCount(4, $itemcollection); $instancetable = array_shift($itemcollection); $this->assertEquals('bigbluebuttonbn', $instancetable->get_name()); $instancelogstable = array_shift($itemcollection); $this->assertEquals('bigbluebuttonbn_logs', $instancelogstable->get_name()); $recordings = array_shift($itemcollection); $this->assertEquals('bigbluebuttonbn_recordings', $recordings->get_name()); $bigbluebuttonserver = array_shift($itemcollection); $this->assertEquals('bigbluebutton', $bigbluebuttonserver->get_name()); $privacyfields = $instancetable->get_privacy_fields(); $this->assertArrayHasKey('participants', $privacyfields); $this->assertEquals('privacy:metadata:bigbluebuttonbn', $instancetable->get_summary()); $privacyfields = $instancelogstable->get_privacy_fields(); $this->assertArrayHasKey('userid', $privacyfields); $this->assertArrayHasKey('timecreated', $privacyfields); $this->assertArrayHasKey('meetingid', $privacyfields); $this->assertArrayHasKey('log', $privacyfields); $this->assertArrayHasKey('meta', $privacyfields); $this->assertEquals('privacy:metadata:bigbluebuttonbn_logs', $instancelogstable->get_summary()); $privacyfields = $bigbluebuttonserver->get_privacy_fields(); $this->assertArrayHasKey('userid', $privacyfields); $this->assertArrayHasKey('fullname', $privacyfields); $this->assertEquals('privacy:metadata:bigbluebutton', $bigbluebuttonserver->get_summary()); } /** * Test for provider::get_contexts_for_userid(). */ public function test_get_contexts_for_userid(): void { $this->resetAfterTest(); $e = $this->get_bigbluebuttonbn_environemnt(); $bigbluebuttonbn = $e['instance']; $course = $e['course']; // Another bigbluebuttonbn activity that has no user activity. $this->getDataGenerator()->create_module('bigbluebuttonbn', ['course' => $course]); // Create a user which will make a submission. $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn') ->create_log(['bigbluebuttonbnid' => $bigbluebuttonbn->id, 'userid' => $user->id]); // Check the contexts supplied are correct. $contextlist = provider::get_contexts_for_userid($user->id); $this->assertCount(1, $contextlist); $contextformodule = $contextlist->current(); $cmcontext = context_module::instance($bigbluebuttonbn->cmid); $this->assertEquals($cmcontext->id, $contextformodule->id); } /** * Test for provider::export_user_data(). */ public function test_export_for_context_logs(): void { $this->resetAfterTest(); $e = $this->get_bigbluebuttonbn_environemnt(); $bigbluebuttonbn = $e['instance']; // Create users which will make submissions. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn') ->create_log(['bigbluebuttonbnid' => $bigbluebuttonbn->id, 'userid' => $user1->id]); $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn') ->create_log(['bigbluebuttonbnid' => $bigbluebuttonbn->id, 'userid' => $user1->id]); $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn') ->create_log(['bigbluebuttonbnid' => $bigbluebuttonbn->id, 'userid' => $user2->id]); // Export all of the data for the context for user 1. $cmcontext = context_module::instance($bigbluebuttonbn->cmid); $this->export_context_data_for_user($user1->id, $cmcontext, 'mod_bigbluebuttonbn'); $writer = \core_privacy\local\request\writer::with_context($cmcontext); $this->assertTrue($writer->has_any_data()); $data = $writer->get_data(); $this->assertCount(2, $data->logs); } /** * Test that only users with relevant contexts are fetched. */ public function test_get_users_in_context(): void { // For backward compatibility with old versions of Moodle. if (!class_exists('\core_privacy\local\request\userlist')) { return; } $this->resetAfterTest(); $e = $this->get_bigbluebuttonbn_environemnt(); $bigbluebuttonbn = $e['instance']; // Users which will make submissions. $user1 = $e['users'][0]; $user2 = $e['users'][1]; $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn') ->create_log(['bigbluebuttonbnid' => $bigbluebuttonbn->id, 'userid' => $user1->id]); $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn') ->create_log(['bigbluebuttonbnid' => $bigbluebuttonbn->id, 'userid' => $user1->id]); $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn') ->create_log(['bigbluebuttonbnid' => $bigbluebuttonbn->id, 'userid' => $user2->id]); // Export all of the data for the context for user 1. $cmcontext = context_module::instance($bigbluebuttonbn->cmid); $userlist = new userlist($cmcontext, 'mod_bigbluebuttonbn'); provider::get_users_in_context($userlist); // Ensure correct users are found in relevant contexts. $this->assertCount(2, $userlist); $expected = [intval($user1->id), intval($user2->id)]; $actual = $userlist->get_userids(); $this->assertEquals(sort($expected), sort($actual)); } /** * Test for provider::delete_data_for_all_users_in_context(). */ public function test_delete_data_for_all_users_in_context(): void { global $DB; $this->resetAfterTest(); $e = $this->get_bigbluebuttonbn_environemnt(); // Before deletion, we should have 3 responses, 1 Add event and 2 Create events (1 per user). $count = $DB->count_records('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => $e['instance']->id]); $this->assertEquals(3, $count); // Delete data based on context. $cmcontext = context_module::instance($e['instance']->cmid); provider::delete_data_for_all_users_in_context($cmcontext); // After deletion, the bigbluebuttonbn logs for that activity should have been deleted. $count = $DB->count_records('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => $e['instance']->id]); $this->assertEquals(0, $count); } /** * Test for provider::delete_data_for_user(). */ public function test_delete_data_for_user(): void { global $DB; $this->resetAfterTest(); $e = $this->get_bigbluebuttonbn_environemnt(); // Delete data for the first user. $context = \context_module::instance($e['instance']->cmid); $contextlist = new approved_contextlist($e['users'][0], 'bigbluebuttonbn', [$context->id]); provider::delete_data_for_user($contextlist); // After deletion the bigbluebuttonbn logs for the first user should have been deleted. $count = $DB->count_records('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => $e['instance']->id, 'userid' => $e['users'][0]->id]); $this->assertEquals(0, $count); // Check the logs for the other user is still there. $count = $DB->count_records('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => $e['instance']->id, 'userid' => $e['users'][1]->id]); $this->assertEquals(1, $count); } /** * Test that data for users in approved userlist is deleted. */ public function test_delete_data_for_users(): void { global $DB; // For backward compatibility with old versions of Moodle. if (!class_exists('\core_privacy\local\request\approved_userlist')) { return; } $this->resetAfterTest(); $e = $this->get_bigbluebuttonbn_environemnt(); // Delete user 1 and 2 data from chat 1 context only. $context = \context_module::instance($e['instance']->cmid); $approveduserids = [$e['users'][0]->id]; $approvedlist = new approved_userlist($context, 'mod_bigbluebuttonbn', $approveduserids); provider::delete_data_for_users($approvedlist); // After deletion the bigbluebuttonbn logs for the first user should have been deleted. $count = $DB->count_records('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => $e['instance']->id, 'userid' => $e['users'][0]->id]); $this->assertEquals(0, $count); // Check the logs for the other user is still there. $count = $DB->count_records('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => $e['instance']->id, 'userid' => $e['users'][1]->id]); $this->assertEquals(1, $count); } /** * Prepares the environment for testing. * * @return array $e */ protected function get_bigbluebuttonbn_environemnt() { $e = []; // Create a course. $e['course'] = $this->getDataGenerator()->create_course(); // Create a bigbluebuttonbn instance. $e['instance'] = $this->getDataGenerator()->create_module('bigbluebuttonbn', ['course' => $e['course']->id]); // Create users that will use the bigbluebuttonbn instance. $e['users'][] = $this->getDataGenerator()->create_user(); $e['users'][] = $this->getDataGenerator()->create_user(); // Create the bigbluebuttonbn logs. $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn') ->create_log(['bigbluebuttonbnid' => $e['instance']->id, 'userid' => $e['users'][0]->id]); $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn') ->create_log(['bigbluebuttonbnid' => $e['instance']->id, 'userid' => $e['users'][1]->id]); return $e; } } PK\z\XlLL.tests/local/proxy/bigbluebutton_proxy_test.phpnu[. namespace mod_bigbluebuttonbn\local\proxy; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\test\testcase_helper_trait; /** * Recording proxy tests class. * * @package mod_bigbluebuttonbn * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) * @covers \mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy * @coversDefaultClass \mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy */ final class bigbluebutton_proxy_test extends \advanced_testcase { /** * Test poll interval value * * @covers \mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy::get_poll_interval * @return void */ public function test_get_poll_interval(): void { global $CFG; $this->resetAfterTest(); $CFG->bigbluebuttonbn['poll_interval'] = 15; $this->assertEquals(15, bigbluebutton_proxy::get_poll_interval()); $CFG->bigbluebuttonbn['poll_interval'] = 0; $this->assertEquals(bigbluebutton_proxy::MIN_POLL_INTERVAL, bigbluebutton_proxy::get_poll_interval()); } } PK\g %tests/local/proxy/proxy_base_test.phpnu[. /** * Tests for BigBlueButton Proxy server (and checksum). * * @package mod_bigbluebuttonbn * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent [at] call-learning [dt] fr) */ namespace mod_bigbluebuttonbn\local\proxy; use mod_bigbluebuttonbn\test\testcase_helper_trait; /** * Proxy base test * * @package mod_bigbluebuttonbn * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent [at] call-learning [dt] fr) * @covers \mod_bigbluebuttonbn\local\proxy\proxy_base * @coversDefaultClass \mod_bigbluebuttonbn\local\proxy\proxy_base */ final class proxy_base_test extends \advanced_testcase { use testcase_helper_trait; /** * Setup */ public function setUp(): void { parent::setUp(); $this->initialise_mock_server(); } /** * Test that different checksum algorithm work * * @return void */ public function test_get_checksum(): void { $this->resetAfterTest(); foreach (['SHA1', 'SHA512', 'SHA256'] as $algo) { set_config('bigbluebuttonbn_checksum_algorithm', $algo); $xmlinfo = self::get_status(); $this->assertNotEmpty($xmlinfo); $this->assertEquals('SUCCESS', $xmlinfo->returncode); } } /** * Test that we send a checksumError whenever the algorithm is not supported * * @return void */ public function test_get_checksum_not_supported(): void { $this->resetAfterTest(); $bbbgenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); $bbbgenerator->set_value('checksum_algorithms', ['SHA1', 'SHA256']); // This should not be supported. set_config('bigbluebuttonbn_checksum_algorithm', 'SHA512'); $xmlinfo = self::get_status(); $this->assertEquals($xmlinfo->messageKey, 'checksumError'); } /** * Get the endpoint XML result. * * @return mixed */ protected static function get_status() { $rc = new \ReflectionClass(proxy_base::class); $rcm = $rc->getMethod('fetch_endpoint_xml'); return $rcm->invoke(null, ''); } } PK\=AO&&*tests/local/proxy/recording_proxy_test.phpnu[. namespace mod_bigbluebuttonbn\local\proxy; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\test\testcase_helper_trait; /** * Recording proxy tests class. * * @package mod_bigbluebuttonbn * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) * @covers \mod_bigbluebuttonbn\local\proxy\recording_proxy * @coversDefaultClass \mod_bigbluebuttonbn\local\proxy\recording_proxy */ final class recording_proxy_test extends \advanced_testcase { use testcase_helper_trait; /** * Simple recording fetcher test * * @return void */ public function test_fetch_recordings(): void { $this->resetAfterTest(); $this->initialise_mock_server(); [$context, $cm, $bbbactivity] = $this->create_instance(); $instance = instance::get_from_instanceid($bbbactivity->id); $recordings = $this->create_recordings_for_instance($instance, [['name' => 'Recording 1'], ['name' => 'Recording 2']]); $recordingsid = array_map(function ($r) { return $r->recordingid; }, $recordings); $recordings = recording_proxy::fetch_recordings($recordingsid); $this->assertCount(2, $recordings); } /** * Simple recording with breakoutroom fetcher test * * @return void */ public function test_fetch_recordings_breakoutroom(): void { $this->resetAfterTest(); $this->initialise_mock_server(); [$context, $cm, $bbbactivity] = $this->create_instance(); $instance = instance::get_from_instanceid($bbbactivity->id); $bbbgenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); $mainmeeting = $bbbgenerator->create_meeting([ 'instanceid' => $instance->get_instance_id(), 'groupid' => $instance->get_group_id(), ]); // TODO: here we artificially create breakout meetings but the current implementations does not handle // breakout rooms for one BBB instance. At this point we just have the ability to retrieve subrecordings // from breakout rooms and manage them as if they belong to the parent recording. // The meetingId is not sent to the server but autogenerated by the mock server and // parentID is the meetingID from the current instance. $submeeting1 = $bbbgenerator->create_meeting([ 'instanceid' => $instance->get_instance_id(), 'groupid' => $instance->get_group_id(), 'isBreakout' => true, 'sequence' => 1 ]); $submeeting2 = $bbbgenerator->create_meeting([ 'instanceid' => $instance->get_instance_id(), 'groupid' => $instance->get_group_id(), 'isBreakout' => true, 'sequence' => 2 ]); $recordings = $this->create_recordings_for_instance($instance, [ ['name' => 'Recording 1'], ['name' => 'Recording 2', 'isBreakout' => true, 'sequence' => 1], ['name' => 'Recording 3', 'isBreakout' => true, 'sequence' => 2] ]); $recordingsid = array_map(function ($r) { return $r->recordingid; }, $recordings); $recordings = recording_proxy::fetch_recordings([$recordingsid[0]]); $this->assertCount(3, $recordings); } } PK\Pkss<tests/local/bigbluebutton/recordings/recording_data_test.phpnu[. namespace mod_bigbluebuttonbn\local\bigbluebutton\recordings; /** * Recording data tests. * * @package mod_bigbluebuttonbn * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) * @coversDefaultClass \mod_bigbluebuttonbn\local\bigbluebutton\recordings\recording_data */ final class recording_data_test extends \advanced_testcase { /** * Test for the type_text provider. * * @covers ::type_text * @dataProvider type_text_provider * @param string $name * @param string $type */ public function test_get_recording_type_text(string $name, string $type): void { $this->assertEquals($name, recording_data::type_text($type)); } /** * Type of recordings (dataprovider) * * @return \string[][] */ public static function type_text_provider(): array { return [ ['Presentation', 'presentation'], ['Video', 'video'], ['Videos', 'videos'], ['Whatever', 'whatever'], ['Whatever It Can Be', 'whatever it can be'], ]; } } PK\ ,~NVJVJtests/local/extension_test.phpnu[. namespace mod_bigbluebuttonbn\local; use backup; use backup_controller; use mod_bigbluebuttonbn\broker; use mod_bigbluebuttonbn\completion\custom_completion; use mod_bigbluebuttonbn\extension; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\local\extension\mod_instance_helper; use mod_bigbluebuttonbn\meeting; use mod_bigbluebuttonbn\test\subplugins_test_helper_trait; use mod_bigbluebuttonbn\test\testcase_helper_trait; use restore_controller; use restore_dbops; /** * Extension helper class test * * @package mod_bigbluebuttonbn * @copyright 2023 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) * @coversDefaultClass \mod_bigbluebuttonbn\extension */ final class extension_test extends \advanced_testcase { use subplugins_test_helper_trait; use testcase_helper_trait; /** * Setup our fake plugin * * @return void */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(true); $this->setup_fake_plugin('simple'); $this->resetDebugging(); // We might have debugging messages issued from setup_fake_plugin here that we need to get rid of. } /** * Setup our fake plugin * * @return void */ public function tearDown(): void { $this->uninstall_fake_plugin('simple'); parent::tearDown(); } /** * Test for the type_text provider. * * @param bool $bbbenabled * @param string $apiclass * @param array $extensionclasses * * @dataProvider classes_implementing_class * @covers \mod_bigbluebuttonbn\extension::get_instances_implementing */ public function test_get_class_implementing(bool $bbbenabled, string $apiclass, array $extensionclasses): void { $this->enable_plugins($bbbenabled); // Make the method public so we can test it. $reflectionextension = new \ReflectionClass(extension::class); $getclassimplementing = $reflectionextension->getMethod('get_instances_implementing'); $allfoundinstances = $getclassimplementing->invoke(null, $apiclass); $foundclasses = array_map( function($instance) { return get_class($instance); }, $allfoundinstances ); $this->assertEquals($extensionclasses, $foundclasses); } /** * Test the add module callback * * @return void * @covers \mod_bigbluebuttonbn\local\extension\mod_instance_helper */ public function test_mod_instance_helper_add(): void { global $DB; // Enable plugin. $this->enable_plugins(true); $course = $this->getDataGenerator()->create_course(); $record = $this->getDataGenerator()->create_module( 'bigbluebuttonbn', ['course' => $course->id, 'newfield' => 2] ); $this->assertEquals(2, $DB->get_field('bbbext_simple', 'newfield', ['bigbluebuttonbnid' => $record->id])); } /** * Test the update module callback * * @return void * @covers \mod_bigbluebuttonbn\local\extension\mod_instance_helper */ public function test_mod_instance_helper_update(): void { global $DB; $this->setAdminUser(); // Enable plugin. $this->enable_plugins(true); $course = $this->getDataGenerator()->create_course(); $record = $this->getDataGenerator()->create_module('bigbluebuttonbn', ['course' => $course->id, 'newfield' => 2]); $cm = get_fast_modinfo($course)->instances['bigbluebuttonbn'][$record->id]; [$cm, $context, $moduleinfo, $data] = get_moduleinfo_data($cm, $course); $data->newfield = 3; bigbluebuttonbn_update_instance($data); $this->assertEquals(3, $DB->get_field('bbbext_simple', 'newfield', ['bigbluebuttonbnid' => $record->id])); } /** * Test delete module callback * * @return void * @covers \mod_bigbluebuttonbn\local\extension\mod_instance_helper */ public function test_mod_instance_helper_delete(): void { global $DB; $this->initialise_mock_server(); // Enable plugin. $this->enable_plugins(true); $course = $this->getDataGenerator()->create_course(); $record = $this->getDataGenerator()->create_module('bigbluebuttonbn', ['course' => $course->id, 'newfield' => 2]); $cm = get_fast_modinfo($course)->instances['bigbluebuttonbn'][$record->id]; course_delete_module($cm->id, false); $this->assertFalse($DB->get_field('bbbext_simple', 'newfield', ['bigbluebuttonbnid' => $record->id])); } /** * Test the action_url_addons with plugin enabled * * @return void * @covers \mod_bigbluebuttonbn\extension::action_url_addons */ public function test_action_url_addons(): void { // Enable plugin. $this->enable_plugins(true); $course = $this->get_course(); [$cm, $cminfo, $bbactivity] = $this->create_instance($course); $bbactivity->newfield = 4; extension::update_instance($bbactivity); ['data' => $additionalvar1, 'metadata' => $additionalvar2] = extension::action_url_addons('create', [], ['bbb-meta' => 'Test'], $bbactivity->id); $this->assertEmpty($additionalvar1); $this->assertCount(2, $additionalvar2); $this->assertEquals($additionalvar2['newfield'], 4); ['data' => $additionalvar1, 'metadata' => $additionalvar2] = extension::action_url_addons('delete'); $this->assertEmpty($additionalvar1); $this->assertEmpty($additionalvar2); } /** * Test the action_url_addons with plugin enabled * * @return void * @covers \mod_bigbluebuttonbn\extension::action_url_addons */ public function test_join_url_with_additional_field(): void { $this->initialise_mock_server(); // Enable plugin. $this->enable_plugins(true); $course = $this->get_course(); [$cm, $cminfo, $bbactivity] = $this->create_instance($course); $bbactivity->newfield = 4; extension::update_instance($bbactivity); $instance = instance::get_from_instanceid($bbactivity->id); $meeting = new meeting($instance); $meetingjoinurl = $meeting->get_join_url(); $this->assertStringContainsString('newfield=4', $meetingjoinurl); } /** * Test backup restore (with extension) * * @covers \backup_bigbluebuttonbn_activity_task */ public function test_backup_restore(): void { global $DB, $CFG, $USER; require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); $this->resetAfterTest(); $course = $this->get_course(); [$cm, $cminfo, $bbactivity] = $this->create_instance($course); $bbactivity->newfield = 4; extension::update_instance($bbactivity); $this->setAdminUser(); $CFG->backup_file_logger_level = backup::LOG_NONE; // Do backup with default settings. set_config('backup_general_users', 1, 'backup'); $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $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/test-restore-course'; $file->extract_to_pathname($fp, $filepath); $bc->destroy(); // Do restore to new course with default settings. $newcourseid = restore_dbops::create_new_course( $course->fullname, $course->shortname . '_2', $course->category); $rc = new restore_controller('test-restore-course', $newcourseid, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, backup::TARGET_NEW_COURSE); $rc->execute_precheck(); $rc->execute_plan(); $rc->destroy(); $cms = get_fast_modinfo($newcourseid)->get_cms(); $newmoduleid = null; foreach ($cms as $id => $cm) { if ($cm->modname == 'bigbluebuttonbn') { $newmoduleid = $cm->instance; } } // Check instance has been copied. $this->assertNotNull($newmoduleid); // Change the original instance value (this makes sure we are not looking at the same value in the // original and copy). $bbactivity->newfield = 5; extension::update_instance($bbactivity); // Now check the copied instance. $newfieldrecord = $DB->get_record('bbbext_simple', [ 'bigbluebuttonbnid' => $newmoduleid, ]); $this->assertNotNull($newfieldrecord); $this->assertEquals(4, $newfieldrecord->newfield); // And the original. $oldfieldrecord = $DB->get_record('bbbext_simple', [ 'bigbluebuttonbnid' => $bbactivity->id, ]); $this->assertNotNull($oldfieldrecord); $this->assertEquals(5, $oldfieldrecord->newfield); } /** * Test completion with and without addons. * * @param array $customcompletionrules * @param array $events * @param int $expectedstate * @return void * @dataProvider custom_completion_data_provider * @covers \mod_bigbluebuttonbn\local\extension\custom_completion_addons */ public function test_additional_completion(array $customcompletionrules, array $events, int $expectedstate): void { // Enable plugin. $this->enable_plugins(true); $this->initialise_mock_server(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance( $this->get_course(), array_merge([ 'completion' => '2', ], $customcompletionrules) ); $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Now create a couple of events. $instance = instance::get_from_instanceid($bbactivity->id); set_config('bigbluebuttonbn_meetingevents_enabled', true); $meeting = $plugingenerator->create_meeting([ 'instanceid' => $instance->get_instance_id(), 'groupid' => $instance->get_group_id(), 'participants' => json_encode([$user->id]), ]); foreach ($events as $edesc) { $plugingenerator->add_meeting_event($user, $instance, $edesc->name, $edesc->data ?? ''); } $result = $plugingenerator->send_all_events($instance); $this->assertNotEmpty($result->data); $data = json_decode(json_encode($result->data)); meeting::meeting_events($instance, $data); $completion = new custom_completion($bbactivitycm, $user->id); $result = $completion->get_overall_completion_state(); $this->assertEquals($expectedstate, $result); } /** * Data generator * * @return array[] */ public static function custom_completion_data_provider(): array { global $CFG; require_once($CFG->libdir . '/completionlib.php'); return [ 'simple' => [ 'customcompletionrules' => [ 'completionengagementtalks' => 1, 'completionextraisehandtwice' => 1, ], 'events' => [ (object) ['name' => 'talks'], (object) ['name' => 'raisehand'], (object) ['name' => 'raisehand'], ], 'expectedstate' => COMPLETION_COMPLETE, ], 'not right events' => [ 'customcompletionrules' => [ 'completionextraisehandtwice' => 1, ], 'events' => [ (object) ['name' => 'chats'], ], 'expectedstate' => COMPLETION_INCOMPLETE, ], 'not enough events' => [ 'customcompletionrules' => [ 'completionextraisehandtwice' => 1, ], 'events' => [ (object) ['name' => 'raisehand'], ], 'expectedstate' => COMPLETION_INCOMPLETE, ], 'more events' => [ 'customcompletionrules' => [ 'completionengagementtalks' => 1, ], 'events' => [ (object) ['name' => 'talks'], (object) ['name' => 'talks'], (object) ['name' => 'talks'], ], 'expectedstate' => COMPLETION_COMPLETE, ], 'basics are still working' => [ 'customcompletionrules' => [ 'completionengagementtalks' => 1, ], 'events' => [ (object) ['name' => 'talks'], ], 'expectedstate' => COMPLETION_COMPLETE, ], ]; } /** * Test broker meeting_events with and without addons. * @return void * @covers \mod_bigbluebuttonbn\local\extension\broker_meeting_events_addons */ public function test_broker_meeting_events_addons(): void { $this->resetAfterTest(); global $DB; // Enable plugin. $this->enable_plugins(true); $this->initialise_mock_server(); [$bbactivitycontext, $bbactivitycm, $bbactivity] = $this->create_instance( $this->get_course()); $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Now create a couple of events. $instance = instance::get_from_instanceid($bbactivity->id); set_config('bigbluebuttonbn_meetingevents_enabled', true); $meeting = $plugingenerator->create_meeting([ 'instanceid' => $instance->get_instance_id(), 'groupid' => $instance->get_group_id(), 'participants' => json_encode([$user->id]), ]); $events = [ (object) ['name' => 'talks'], (object) ['name' => 'raisehand'], (object) ['name' => 'raisehand'], ]; foreach ($events as $edesc) { $plugingenerator->add_meeting_event($user, $instance, $edesc->name, $edesc->data ?? ''); } $result = $plugingenerator->send_all_events($instance); $this->assertNotEmpty($result->data); $data = json_encode($result->data); $reflection = new \ReflectionClass(broker::class); $method = $reflection->getMethod('process_extension_actions'); $method->setAccessible(true); $method->invokeArgs(null, [$instance, $data]); $addondata = $DB->get_field('bbbext_simple', 'meetingevents', ['bigbluebuttonbnid' => $bbactivity->id]); $addondata = json_decode($addondata); // Check that the data is received. $this->assertEquals(json_encode($addondata), $data); } /** * Data provider for testing get_class_implementing * * @return array[] */ public static function classes_implementing_class(): array { return [ 'mod_instance_helper with plugin disabled' => [ 'bbbenabled' => false, 'apiclass' => mod_instance_helper::class, 'extensionclasses' => [], ], 'mod_instance_helper with plugin enabled' => [ 'bbbenabled' => true, 'apiclass' => mod_instance_helper::class, 'extensionclasses' => [ 'bbbext_simple\\bigbluebuttonbn\\mod_instance_helper', ], ], ]; } /** * Test the get_sorted_plugins_list * @param array $sortorders * @param array $expected * @return void * @dataProvider sorted_plugins_list_data_provider * @covers \mod_bigbluebuttonbn\extension::get_sorted_plugins_list */ public function test_get_sorted_plugins_list(array $sortorders, array $expected): void { $this->resetAfterTest(); // Enable plugin. $this->enable_plugins(true); // Create list of plugins we will then sort. $pluginlist = [ 'simpleone' => '/path/to/simpleone', 'simpletwo' => '/path/to/simpletwo', ]; // Set sortorder. foreach ($sortorders as $plugin => $sortorder) { set_config('sortorder', $sortorder, 'bbbext_' . $plugin); } $sortedlist = extension::get_sorted_plugins_list($pluginlist); $this->assertSame($expected, $sortedlist); } /** * Data provider for testing get_sorted_plugins_list * * @return array[] */ public static function sorted_plugins_list_data_provider(): array { return [ 'no sortorder' => [ [], ['simpleone', 'simpletwo'], ], 'default sortorder' => [ ['simpleone' => 0, 'simpletwo' => 1], ['simpleone', 'simpletwo'], ], 'changed sortorder' => [ ['simpleone' => 1, 'simpletwo' => 0], ['simpletwo', 'simpleone'], ], ]; } /** * Enable plugins * * @param bool $bbbenabled * @return void */ private function enable_plugins(bool $bbbenabled) { // First make sure that either BBB is enabled or not. \core\plugininfo\mod::enable_plugin('bigbluebuttonbn', $bbbenabled ? 1 : 0); $plugin = extension::BBB_EXTENSION_PLUGIN_NAME . '_simple'; if ($bbbenabled) { unset_config('disabled', $plugin); } else { set_config('disabled', 'disabled', $plugin); } } } PK\ߣ^"tests/local/helpers/reset_test.phpnu[. /** * BBB Library tests class. * * @package mod_bigbluebuttonbn * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ namespace mod_bigbluebuttonbn\local\helpers; use core_tag_tag; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\test\testcase_helper_trait; /** * BBB Library tests class. * * @package mod_bigbluebuttonbn * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) * @coversDefaultClass \mod_bigbluebuttonbn\local\helpers\reset * @covers \mod_bigbluebuttonbn\local\helpers\reset */ final class reset_test extends \advanced_testcase { use testcase_helper_trait; /** * Reset course item test */ public function test_reset_course_items(): void { global $CFG; $this->resetAfterTest(); $CFG->bigbluebuttonbn_recordings_enabled = false; $results = reset::reset_course_items(); $this->assertEquals(["events" => 0, "tags" => 0, "logs" => 0], $results); $CFG->bigbluebuttonbn_recordings_enabled = true; $results = reset::reset_course_items(); $this->assertEquals(["events" => 0, "tags" => 0, "logs" => 0, "recordings" => 0], $results); } /** * Reset get_status test */ public function test_reset_getstatus(): void { $this->resetAfterTest(); $result = reset::reset_getstatus('events'); $this->assertEquals([ 'component' => 'BigBlueButton', 'item' => 'Deleted events', 'error' => false, ], $result); } /** * Reset event test */ public function test_reset_events(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance( null, ['openingtime' => time()] ); $formdata = $this->get_form_data_from_instance($bbactivity); \mod_bigbluebuttonbn\local\helpers\mod_helper::process_post_save($formdata); $this->assertEquals(1, $DB->count_records( 'event', ['modulename' => 'bigbluebuttonbn', 'courseid' => $this->get_course()->id])); reset::reset_events($this->get_course()->id); $this->assertEquals(0, $DB->count_records( 'event', ['modulename' => 'bigbluebuttonbn', 'courseid' => $this->get_course()->id])); } /** * Reset tags test */ public function test_reset_tags(): void { $this->resetAfterTest(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(null, ['course' => $this->get_course()->id], ['visible' => true] ); core_tag_tag::add_item_tag('mod_bigbluebuttonbn', 'bbitem', $bbactivity->id, $bbactivitycontext, 'newtag'); $alltags = core_tag_tag::get_item_tags('mod_bigbluebuttonbn', 'bbitem', $bbactivity->id); $this->assertCount(1, $alltags); reset::reset_tags($this->get_course()->id); $alltags = core_tag_tag::get_item_tags('mod_bigbluebuttonbn', 'bbitem', $bbactivity->id); $this->assertCount(0, $alltags); } /** * Reset recordings test */ public function test_reset_recordings(): void { $this->initialise_mock_server(); $this->resetAfterTest(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(null, ['course' => $this->get_course()->id], ['visible' => true] ); $instance = instance::get_from_instanceid($bbactivity->id); $this->create_recordings_for_instance($instance, [ ['name' => 'Recording 1'], ['name' => 'Recording 2'], ]); $this->assertCount(2, $instance->get_recordings()); reset::reset_recordings($this->get_course()->id); $this->assertCount(0, $instance->get_recordings()); } } PK\ъ("tests/local/helpers/roles_test.phpnu[. namespace mod_bigbluebuttonbn\local\helpers; use context_course; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\test\testcase_helper_trait; /** * BBB Library tests class. * * @package mod_bigbluebuttonbn * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) * @covers \mod_bigbluebuttonbn\local\helpers\roles */ final class roles_test extends \advanced_testcase { use testcase_helper_trait; /** * Test select separate group prevent all * */ public function test_get_users_select_separate_groups_prevent_all(): void { $this->resetAfterTest(); $numstudents = 12; $numteachers = 3; $groupsnum = 3; list($course, $groups, $students, $teachers, $bbactivity, $roleids) = $this->setup_course_students_teachers( (object) ['enablecompletion' => true, 'groupmode' => strval(SEPARATEGROUPS), 'groupmodeforce' => 1], $numstudents, $numteachers, $groupsnum); $context = context_course::instance($course->id); // Prevent access all groups. role_change_permission($roleids['teacher'], $context, 'moodle/site:accessallgroups', CAP_PREVENT); $this->setUser($teachers[0]); $users = roles::get_users_array($context, $bbactivity); $this->assertCount(($numstudents + $numteachers) / $groupsnum, $users); $this->setUser($teachers[1]); $users = roles::get_users_array($context, $bbactivity); $this->assertCount(($numstudents + $numteachers) / $groupsnum, $users); $this->setUser($teachers[2]); $users = roles::get_users_array($context, $bbactivity); $this->assertCount(($numstudents + $numteachers) / $groupsnum, $users); $course->groupmode = strval(SEPARATEGROUPS); $course->groupmodeforce = "0"; update_course($course); $this->setUser($teachers[2]); $users = roles::get_users_array($context, $bbactivity); $this->assertCount($numstudents + $numteachers, $users); } /** * Test select separate groups * */ public function test_get_users_select_separate_groups(): void { $this->resetAfterTest(); $numstudents = 12; $numteachers = 3; $groupsnum = 3; list($course, $groups, $students, $teachers, $bbactivity, $roleids) = $this->setup_course_students_teachers( (object) ['enablecompletion' => true, 'groupmode' => strval(VISIBLEGROUPS), 'groupmodeforce' => 1], $numstudents, $numteachers, $groupsnum); $context = context_course::instance($course->id); $this->setUser($teachers[0]); $users = roles::get_users_array($context, $bbactivity); $this->assertCount($numstudents + $numteachers, $users); $this->setUser($teachers[1]); $users = roles::get_users_array($context, $bbactivity); $this->assertCount($numstudents + $numteachers, $users); $this->setUser($teachers[1]); $users = roles::get_users_array($context, $bbactivity); $this->assertCount($numstudents + $numteachers, $users); } /** * Test getting courses from which we can import */ public function test_import_get_courses_for_select(): void { $this->resetAfterTest(); $this->setAdminUser(); filter_set_global_state('multilang', TEXTFILTER_ON); filter_set_applies_to_strings('multilang', true); $courseone = $this->getDataGenerator()->create_course(); [$context, $cm, $activity] = $this->create_instance($courseone); $instance = instance::get_from_instanceid($activity->id); // Course two should be returned, because it has an activity instance. $coursetwo = $this->getDataGenerator()->create_course([ 'fullname' => 'EnglishSpanish', ]); $this->create_instance($coursetwo); // Course three should not be returned, because it has no activity instance. $coursethree = $this->getDataGenerator()->create_course(); $coursesforselect = roles::import_get_courses_for_select($instance); $this->assertEquals([ $courseone->id => $courseone->fullname, $coursetwo->id => 'English', ], $coursesforselect); // Display extended course names. set_config('courselistshortnames', 1); $coursesforselect = roles::import_get_courses_for_select($instance); $this->assertEquals([ $courseone->id => "{$courseone->shortname} {$courseone->fullname}", $coursetwo->id => "{$coursetwo->shortname} English", ], $coursesforselect); } } PK\"kx#x#-tests/local/helpers/mod_helper_trait_test.phpnu[. /** * BBB Library tests class. * * @package mod_bigbluebuttonbn * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ namespace mod_bigbluebuttonbn\local\helpers; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\test\testcase_helper_trait; /** * BBB Library tests class. * * @package mod_bigbluebuttonbn * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) * @covers \mod_bigbluebuttonbn\local\helpers\mod_helper * @coversDefaultClass \mod_bigbluebuttonbn\local\helpers\mod_helper */ final class mod_helper_trait_test extends \advanced_testcase { use testcase_helper_trait; /** * Presave test */ public function test_process_pre_save(): void { $this->resetAfterTest(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); $bbformdata = $this->get_form_data_from_instance($bbactivity); $bbformdata->participants = '

this -> "

\n'; $bbformdata->timemodified = time(); mod_helper::process_pre_save($bbformdata); $this->assertTrue($bbformdata->timemodified != 0); $this->assertEquals('

this -> "

\n', $bbformdata->participants); } /** * Presave instance */ public function test_process_pre_save_instance(): void { $this->resetAfterTest(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); $bbformdata = $this->get_form_data_from_instance($bbactivity); $bbformdata->instance = 0; $bbformdata->timemodified = time(); mod_helper::process_pre_save($bbformdata); $this->assertTrue($bbformdata->timemodified == 0); } /** * Presave checkboxes */ public function test_process_pre_save_checkboxes(): void { $this->resetAfterTest(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); $bbformdata = $this->get_form_data_from_instance($bbactivity); unset($bbformdata->wait); unset($bbformdata->recordallfromstart); mod_helper::process_pre_save($bbformdata); $this->assertTrue(isset($bbformdata->wait)); $this->assertTrue(isset($bbformdata->recordallfromstart)); } /** * Presave common */ public function test_process_pre_save_common(): void { global $CFG; $this->resetAfterTest(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(null, ['type' => instance::TYPE_RECORDING_ONLY]); $bbformdata = $this->get_form_data_from_instance($bbactivity); $bbformdata->groupmode = '1'; mod_helper::process_pre_save($bbformdata); $this->assertEquals(0, $bbformdata->groupmode); } /** * Post save */ public function test_process_post_save(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(null, ['type' => instance::TYPE_RECORDING_ONLY]); // Reset some static caches used by this test after enabling the plugin. get_module_types_names(false, true); $bbformdata = $this->get_form_data_from_instance($bbactivity); // Enrol users in a course so he will receive the message. $teacher = $generator->create_user(['role' => 'editingteacher']); $generator->enrol_user($teacher->id, $this->get_course()->id); // Mark the form to trigger notification. $bbformdata->coursecontentnotification = true; $bbformdata->update = false; $messagesink = $this->redirectMessages(); mod_helper::process_post_save($bbformdata); edit_module_post_actions($bbformdata, $this->course); // Now run cron. ob_start(); $this->runAdhocTasks(); ob_get_clean(); // Suppress output as it can fail the test. $this->assertEquals(1, $messagesink->count()); $firstmessage = $messagesink->get_messages()[0]; $this->assertStringContainsString('is new in', $firstmessage->smallmessage); } /** * Post save notification */ public function test_process_post_save_with_add(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(null, ['type' => instance::TYPE_RECORDING_ONLY]); // Reset some static caches used by this test after enabling the plugin. get_module_types_names(false, true); $bbformdata = $this->get_form_data_from_instance($bbactivity); $bbformdata->update = false; $messagesink = $this->redirectMessages(); // Enrol users in a course so he will receive the message. $teacher = $generator->create_user(['role' => 'editingteacher']); $generator->enrol_user($teacher->id, $this->get_course()->id); $bbformdata->coursecontentnotification = true; mod_helper::process_post_save($bbformdata); edit_module_post_actions($bbformdata, $this->course); // Now run cron. ob_start(); $this->runAdhocTasks(); ob_get_clean(); // Suppress output as it can fail the test. $messages = $messagesink->get_messages_by_component_and_type( component: 'core', type: 'coursecontentupdated', ); $this->assertEquals(1, count($messages)); $firstmessage = reset($messages); $this->assertStringContainsString('is new in', $firstmessage->smallmessage); } /** * Post save * * There was an issue when both the opening time and completion were set * and the form was saved twice. */ public function test_process_post_save_twice_with_completion(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(null, ['type' => instance::TYPE_RECORDING_ONLY]); // Reset some static caches used by this test after enabling the plugin. get_module_types_names(false, true); $bbformdata = $this->get_form_data_from_instance($bbactivity); $bbformdata->completionunlocked = 0; $bbformdata->completion = COMPLETION_AGGREGATION_ANY; $bbformdata->completionview = COMPLETION_VIEWED; $bbformdata->completionexpected = time(); $bbformdata->openingtime = time() - 1000; $bbformdata->closing = time() + 1000; // Enrol users in a course so he will receive the message. $teacher = $generator->create_user(); $generator->enrol_user($teacher->id, $this->get_course()->id, 'editingteacher'); $this->setUser($teacher); // Mark the form to trigger notification. $bbformdata->coursecontentnotification = true; $bbformdata->update = false; $messagesink = $this->redirectMessages(); mod_helper::process_post_save($bbformdata); edit_module_post_actions($bbformdata, $this->course); // Now run cron. ob_start(); $this->runAdhocTasks(); ob_get_clean(); // Suppress output as it can fail the test. $this->assertEquals(1, $messagesink->count()); $firstmessage = $messagesink->get_messages()[0]; $this->assertStringContainsString('is new in', $firstmessage->smallmessage); $messagesink->clear(); // Do it a again, so we check we still have one event. mod_helper::process_post_save($bbformdata); // Mark the form to trigger notification. $bbformdata->update = true; edit_module_post_actions($bbformdata, $this->course); // Now run cron. ob_start(); $this->runAdhocTasks(); ob_get_clean(); // Suppress output as it can fail the test. $this->assertEquals(1, $messagesink->count()); $firstmessage = $messagesink->get_messages()[0]; $this->assertStringContainsString('has been changed', $firstmessage->smallmessage); } } PK\yy&tests/local/helpers/user_info_test.phpnu[. namespace mod_bigbluebuttonbn\local\helpers; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\logger; use mod_bigbluebuttonbn\test\testcase_helper_trait; /** * User information printing test * * @package mod_bigbluebuttonbn * @copyright 2022 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) * @covers \mod_bigbluebuttonbn\local\helpers\user_info * @coversDefaultClass \mod_bigbluebuttonbn\local\helpers\user_info */ final class user_info_test extends \advanced_testcase { use testcase_helper_trait; /** * Test user info outline * * @return void */ public function test_get_user_info_outline(): void { $this->initialise_mock_server(); $this->resetAfterTest(); $generator = $this->getDataGenerator(); $user = $generator->create_and_enrol($this->get_course()); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); $this->setUser($user); // Now create a couple of logs. $instance = instance::get_from_instanceid($bbactivity->id); $recordings = $this->create_recordings_for_instance($instance, [['name' => "Pre-Recording 1"]]); logger::log_meeting_joined_event($instance, 0); logger::log_recording_played_event($instance, $recordings[0]->id); [$logjoins, $logtimes] = user_info::get_user_info_outline($this->get_course(), $user, $bbactivitycm); $this->assertEquals([ '1 meeting(s)', '1 recording(s) played' ], $logjoins); $this->assertCount(2, $logtimes); } /** * Test user info outline with several logs * * @return void */ public function test_get_user_info_outline_several_logs(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $user = $generator->create_and_enrol($this->get_course()); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); $this->setUser($user); // Now create a couple of logs. $instance = instance::get_from_instanceid($bbactivity->id); logger::log_meeting_joined_event($instance, 0); logger::log_meeting_joined_event($instance, 0); [$logjoins, $logtimes] = user_info::get_user_info_outline($this->get_course(), $user, $bbactivitycm); $this->assertEquals([ '2 meeting(s)', ], $logjoins); $this->assertCount(1, $logtimes); } /** * Test user info outline for view events * * @return void */ public function test_get_user_info_outline_view(): void { $this->initialise_mock_server(); $this->resetAfterTest(); $generator = $this->getDataGenerator(); $user = $generator->create_and_enrol($this->get_course()); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance( null, ['completion' => 2, 'completionview' => 1]); $this->setUser($user); // Now create a couple of logs. $instance = instance::get_from_instanceid($bbactivity->id); // View it twice. bigbluebuttonbn_view($instance->get_instance_data(), $instance->get_course(), $instance->get_cm(), $instance->get_context()); bigbluebuttonbn_view($instance->get_instance_data(), $instance->get_course(), $instance->get_cm(), $instance->get_context()); [$logjoins, $logtimes] = user_info::get_user_info_outline($this->get_course(), $user, $bbactivitycm); $this->assertEquals([ 'viewed', ], $logjoins); $this->assertCount(1, $logtimes); } } PK\D^, & &"tests/local/helpers/files_test.phpnu[. /** * BBB Library tests class. * * @package mod_bigbluebuttonbn * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ namespace mod_bigbluebuttonbn\local\helpers; use context_course; use context_module; use context_system; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\test\testcase_helper_trait; use stdClass; use stored_file; /** * BBB Library tests class. * * @package mod_bigbluebuttonbn * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) * @covers \mod_bigbluebuttonbn\local\helpers\files * @coversDefaultClass \mod_bigbluebuttonbn\local\helpers\files */ final class files_test extends \advanced_testcase { use testcase_helper_trait; /** @var string Filename used for the presentation */ const PRESENTATION_FILENAME = 'bbpresentation.pptx'; /** @var string Filename used for the presentation */ const PRESENTATION_FILEPATH = '/mod/bigbluebuttonbn/tests/fixtures/bbpresentation.pptx'; /** * Plugin valid test case */ public function test_pluginfile_valid(): void { $this->resetAfterTest(); $this->assertFalse(files::pluginfile_valid(context_course::instance($this->get_course()->id), 'presentation')); $this->assertTrue(files::pluginfile_valid(context_system::instance(), 'presentation')); $this->assertFalse(files::pluginfile_valid(context_system::instance(), 'otherfilearea')); } /** * Plugin file test case */ public function test_pluginfile_file(): void { global $CFG; $this->resetAfterTest(); list($user, $bbactivity) = $this->create_user_and_activity($CFG->dirroot . self::PRESENTATION_FILEPATH); $this->setUser($user); $instance = instance::get_from_instanceid($bbactivity->id); $cm = $instance->get_cm(); $cmrecord = $cm->get_course_module_record(); /** @var stored_file $mediafile */ $mediafile = files::pluginfile_file($this->get_course(), $cmrecord, $instance->get_context(), 'presentation', [self::PRESENTATION_FILENAME]); $this->assertEquals(self::PRESENTATION_FILENAME, $mediafile->get_filename()); } /** * Get presentation file */ public function test_default_presentation_get_file(): void { $this->resetAfterTest(); list($user, $bbactivity) = $this->create_user_and_activity(); $this->setUser($user); $instance = instance::get_from_instanceid($bbactivity->id); $cm = $instance->get_cm(); $cmrecord = $cm->get_course_module_record(); $mediafilename = files::get_plugin_filename($this->get_course(), $cmrecord, $instance->get_context(), ['presentation.pptx']); $this->assertEquals('presentation.pptx', $mediafilename); } /** * Test that file is accessible only once. */ public function test_presentation_file_accessible_twice(): void { global $CFG; $this->resetAfterTest(); list($user, $bbactivity) = $this->create_user_and_activity($CFG->dirroot . self::PRESENTATION_FILEPATH); $this->setUser($user); $CFG->bigbluebuttonbn_preuploadpresentation_editable = true; $instance = instance::get_from_instanceid($bbactivity->id); $presentation = $instance->get_presentation_for_bigbluebutton_upload(); $fulldirset = explode('/', $presentation['url']); $filename = array_pop($fulldirset); $nonce = array_pop($fulldirset); $cm = $instance->get_cm(); $cmrecord = $cm->get_course_module_record(); // The link should be valid twice. for ($i = 0; $i < 2; $i++) { $mediafile = files::pluginfile_file($this->get_course(), $cmrecord, $instance->get_context(), 'presentation', [$nonce, $filename]); $this->assertEquals($filename, $mediafile->get_filename()); } // Third time is a charm, this should be false. $mediafile = files::pluginfile_file($this->get_course(), $cmrecord, $instance->get_context(), 'presentation', [$nonce, $filename]); $this->assertFalse($mediafile); } /** * Test that file is accessible only once. */ public function test_presentation_file_not_accessible_externally(): void { global $CFG; $this->resetAfterTest(); list($user, $bbactivity) = $this->create_user_and_activity($CFG->dirroot . self::PRESENTATION_FILEPATH); $this->setUser($user); $CFG->bigbluebuttonbn_preuploadpresentation_editable = true; $instance = instance::get_from_instanceid($bbactivity->id); $presentation = $instance->get_presentation(); $fulldirset = explode('/', $presentation['url']); $filename = array_pop($fulldirset); $this->setGuestUser(); $this->expectException(\require_login_exception::class); $cm = $instance->get_cm(); $cmrecord = $cm->get_course_module_record(); files::pluginfile_file($this->get_course(), $cmrecord, $instance->get_context(), 'presentation', [$filename]); $this->setUser($user); $mediafile = files::pluginfile_file($this->get_course(), $cmrecord, $instance->get_context(), 'presentation', [$filename]); $this->assertNotNull($mediafile); } /** * Get filename test */ public function test_pluginfile_filename(): void { global $CFG; $this->resetAfterTest(); list($user, $bbactivity, $bbactivitycm, $bbactivitycontext) = $this->create_user_and_activity(); $this->setUser($user); $this->create_sample_file(self::PRESENTATION_FILENAME, $bbactivitycontext->id); $CFG->bigbluebuttonbn_preuploadpresentation_editable = true; $presentationdef = files::get_presentation($bbactivitycontext, self::PRESENTATION_FILENAME, $bbactivity->id, true); $pathparts = explode('/', $presentationdef['url']); $filename = array_pop($pathparts); $salt = array_pop($pathparts); $filename = files::get_plugin_filename($this->get_course(), $bbactivitycm->get_course_module_record(), $bbactivitycontext, [$salt, $filename]); $this->assertEquals(self::PRESENTATION_FILENAME, $filename); } /** * Get media files */ public function test_get_media_file(): void { $this->resetAfterTest(); list($user, $bbactivity) = $this->create_user_and_activity(); $this->setUser($user); $bbformdata = $this->get_form_data_from_instance($bbactivity); $mediafilepath = files::save_media_file($bbformdata); $this->assertEmpty($mediafilepath); // From test_delete_original_file_from_draft (lib/test/filelib_test.php) // Create a bbb private file. $this->create_sample_file(self::PRESENTATION_FILENAME, context_module::instance($bbformdata->coursemodule)->id); file_prepare_draft_area($bbformdata->presentation, context_module::instance($bbformdata->coursemodule)->id, 'mod_bigbluebuttonbn', 'presentation', 0); $mediafilepath = files::save_media_file($bbformdata); $this->assertEquals('/' . self::PRESENTATION_FILENAME, $mediafilepath); } /** * Create a user and an activity * * @param string|null $presentationpath * @param bool $closed * @return array */ protected function create_user_and_activity($presentationpath = null, $closed = false): array { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $this->setAdminUser(); $activitydata = ['closingtime' => time() + ($closed ? -3600 : +3600)]; if (!empty($presentationpath)) { $activitydata['presentation'] = $presentationpath; } list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(null, $activitydata); $generator->enrol_user($user->id, $this->get_course()->id, 'editingteacher'); return [$user, $bbactivity, $bbactivitycm, $bbactivitycontext]; } /** * Helper to create sample file for tests * * @param string $filename * @param int $contextid * @return stored_file */ protected function create_sample_file($filename, $contextid) { $bbbfilerecord = new stdClass; $bbbfilerecord->contextid = $contextid; $bbbfilerecord->component = 'mod_bigbluebuttonbn'; $bbbfilerecord->filearea = 'presentation'; $bbbfilerecord->itemid = 0; $bbbfilerecord->filepath = '/'; $bbbfilerecord->filename = $filename; $bbbfilerecord->source = 'test'; $fs = get_file_storage(); return $fs->create_file_from_string($bbbfilerecord, 'Presentation file content'); } } PK\zxxtests/lib_test.phpnu[. /** * BBB Library tests class. * * @package mod_bigbluebuttonbn * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ namespace mod_bigbluebuttonbn; use calendar_event; use context_module; use mod_bigbluebuttonbn\test\testcase_helper_trait; use mod_bigbluebuttonbn_mod_form; use MoodleQuickForm; use navigation_node; use ReflectionClass; use stdClass; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/bigbluebuttonbn/lib.php'); /** * BBB Library tests class. * * @package mod_bigbluebuttonbn * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ final class lib_test extends \advanced_testcase { use testcase_helper_trait; /** * Check support * * @covers ::bigbluebuttonbn_supports */ public function test_bigbluebuttonbn_supports(): void { $this->resetAfterTest(); $this->assertTrue(bigbluebuttonbn_supports(FEATURE_IDNUMBER)); $this->assertTrue(bigbluebuttonbn_supports(FEATURE_MOD_INTRO)); $this->assertTrue(bigbluebuttonbn_supports(FEATURE_GRADE_HAS_GRADE)); } /** * Check add instance * * @covers ::bigbluebuttonbn_add_instance */ public function test_bigbluebuttonbn_add_instance(): void { $this->resetAfterTest(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); $bbformdata = $this->get_form_data_from_instance($bbactivity); $id = bigbluebuttonbn_add_instance($bbformdata); $this->assertNotNull($id); } /** * Check update instance * * @covers ::bigbluebuttonbn_update_instance */ public function test_bigbluebuttonbn_update_instance(): void { $this->resetAfterTest(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); $bbformdata = $this->get_form_data_from_instance($bbactivity); $result = bigbluebuttonbn_update_instance($bbformdata); $this->assertTrue($result); } /** * Check delete instance * * @covers ::bigbluebuttonbn_delete_instance */ public function test_bigbluebuttonbn_delete_instance(): void { $this->resetAfterTest(); $this->initialise_mock_server(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); $result = bigbluebuttonbn_delete_instance($bbactivity->id); $this->assertTrue($result); } /** * Check delete instance * * @covers ::bigbluebuttonbn_delete_instance */ public function test_bigbluebuttonbn_delete_instance_with_running_meeting(): void { $this->resetAfterTest(); $this->initialise_mock_server(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); $bbbgenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); $instance = instance::get_from_instanceid($bbactivity->id); // Run the meeting. $bbbgenerator->create_meeting([ 'instanceid' => $instance->get_instance_id(), 'groupid' => $instance->get_group_id(), ]); $meeting = new meeting($instance); $meeting->update_cache(); $this->assertTrue($meeting->is_running()); $result = bigbluebuttonbn_delete_instance($bbactivity->id); $this->assertTrue($result); $meeting->update_cache(); $this->assertFalse($meeting->is_running()); } /** * Check delete instance * * @covers ::bigbluebuttonbn_delete_instance */ public function test_bigbluebuttonbn_delete_instance_with_running_group_meetings(): void { $this->resetAfterTest(); $this->initialise_mock_server(); $datagenerator = $this->getDataGenerator(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); $course = $this->get_course(); set_coursemodule_groupmode($bbactivitycm->id, VISIBLEGROUPS); $groups = []; foreach (['G1', 'G2'] as $gname) { $groups[] = $datagenerator->create_group(['courseid' => $course->id, 'name' => $gname]); } // Just create a user in one of the group so we check we don't just end meetings for this user... $user = $datagenerator->create_and_enrol($this->get_course()); $groupids = array_map(function($g) { return $g->id; }, $groups); $datagenerator->create_group_member(['userid' => $user->id, 'groupid' => $groupids[0]]); $this->setUser($user); $groupids[] = 0; // Add no group (id=0) - as an item so it is covered in the loop. $bbbgenerator = $datagenerator->get_plugin_generator('mod_bigbluebuttonbn'); $globalinstance = instance::get_from_instanceid($bbactivity->id); $meetings = []; foreach ($groupids as $groupid) { $instance = instance::get_group_instance_from_instance($globalinstance, $groupid); // Run the meetings. $bbbgenerator->create_meeting([ 'instanceid' => $instance->get_instance_id(), 'groupid' => $instance->get_group_id(), ]); $meeting = new meeting($instance); $meeting->update_cache(); $this->assertTrue($meeting->is_running()); $meetings[] = $meeting; } $result = bigbluebuttonbn_delete_instance($bbactivity->id); $this->assertTrue($result); foreach ($meetings as $meeting) { $meeting->update_cache(); $this->assertFalse($meeting->is_running()); } } /** * Check user outline page * * @covers ::bigbluebuttonbn_user_outline */ public function test_bigbluebuttonbn_user_outline(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $this->setUser($user); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance($this->get_course(), ['completion' => 2, 'completionview' => 1]); $result = bigbluebuttonbn_user_outline($this->get_course(), $user, $bbactivitycm, $bbactivity); $this->assertEquals((object) ['info' => '', 'time' => 0], $result); bigbluebuttonbn_view($bbactivity, $this->get_course(), $bbactivitycm, $bbactivitycontext); $result = bigbluebuttonbn_user_outline($this->get_course(), $user, $bbactivitycm, $bbactivity); $this->assertStringContainsString(get_string('report_room_view', 'mod_bigbluebuttonbn'), $result->info); } /** * Check user completion * * @covers ::bigbluebuttonbn_user_complete */ public function test_bigbluebuttonbn_user_complete(): void { $this->initialise_mock_server(); $this->resetAfterTest(); $generator = $this->getDataGenerator(); $user = $generator->create_and_enrol($this->get_course()); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance($this->get_course(), ['completion' => 2, 'completionview' => 1]); $this->setUser($user); // Now create a couple of logs. bigbluebuttonbn_view($bbactivity, $this->get_course(), $bbactivitycm, $bbactivitycontext); ob_start(); bigbluebuttonbn_user_complete($this->get_course(), $user, $bbactivitycm, $bbactivity); $output = ob_get_contents(); ob_end_clean(); $this->assertStringContainsString(get_string('report_room_view', 'mod_bigbluebuttonbn'), $output); } /** * Check get recent activity * * @covers ::bigbluebuttonbn_get_recent_mod_activity */ public function test_bigbluebuttonbn_get_recent_mod_activity(): void { $this->initialise_mock_server(); $this->resetAfterTest(); $generator = $this->getDataGenerator(); $user = $generator->create_and_enrol($this->get_course()); $this->setUser($user); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); // Now create a couple of logs. $instance = instance::get_from_instanceid($bbactivity->id); $recordings = $this->create_recordings_for_instance($instance, [['name' => "Pre-Recording 1"]]); logger::log_meeting_joined_event($instance, 0); logger::log_meeting_joined_event($instance, 0); logger::log_recording_played_event($instance, $recordings[0]->id); $activities = $this->prepare_for_recent_activity_array(0, $user->id, 0); $this->assertCount(4, $activities); $this->assertEquals( ["Meeting joined", "Meeting joined", "Recording viewed"], array_values( array_filter( array_map(function($activity) { return $activity->eventname ?? ""; }, $activities), function($e) { return !empty($e); } ) ) ); $this->assertEquals("Pre-Recording 1", $activities[3]->content); // The recording view event should contain the name // of the activity. } /** * Prepare the list of activities as is done in course recent activity * * @param int $date * @param int $user * @param int $group * @return array|void */ protected function prepare_for_recent_activity_array($date, $user, $group) { // Same algorithm as in cource/recent.php, but stops at the first bbb activity. $course = $this->get_course(); $modinfo = get_fast_modinfo($course->id); $sections = array(); $index = 0; foreach ($modinfo->get_section_info_all() as $i => $section) { if (!empty($section->uservisible)) { $sections[$i] = $section; } } foreach ($sections as $sectionnum => $section) { $activity = new stdClass(); $activity->type = 'section'; if ($section->section > 0) { $activity->name = get_section_name($this->get_course(), $section); } else { $activity->name = ''; } $activity->visible = $section->visible; $activities[$index++] = $activity; if (empty($modinfo->sections[$sectionnum])) { continue; } foreach ($modinfo->sections[$sectionnum] as $cmid) { $cm = $modinfo->cms[$cmid]; if (!$cm->uservisible) { continue; } if (!empty($filter) && $cm->modname != $filter) { continue; } if (!empty($filtermodid) && $cmid != $filtermodid) { continue; } if ($cm->modname == 'bigbluebuttonbn') { return bigbluebuttonbn_get_recent_mod_activity($activities, $index, $date, $course->id, $cmid, $user, $group); } } } } /** * Check user recent activity * * @covers ::bigbluebuttonbn_print_recent_mod_activity */ public function test_bigbluebuttonbn_print_recent_mod_activity(): void { $this->initialise_mock_server(); $this->resetAfterTest(); $generator = $this->getDataGenerator(); $user = $generator->create_and_enrol($this->get_course()); $this->setUser($user); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); // Now create a couple of logs. $instance = instance::get_from_instanceid($bbactivity->id); $recordings = $this->create_recordings_for_instance($instance, [['name' => "Pre-Recording 1"]]); logger::log_meeting_joined_event($instance, 0); logger::log_meeting_joined_event($instance, 0); logger::log_recording_played_event($instance, $recordings[0]->id); $activities = $this->prepare_for_recent_activity_array(0, $user->id, 0); ob_start(); bigbluebuttonbn_print_recent_mod_activity($activities[1], $this->get_course()->id, false, [], false); $output = ob_get_contents(); ob_end_clean(); $this->assertStringContainsString('Meeting joined', $output); } /** * Check recent activity for the course * * @covers ::bigbluebuttonbn_print_recent_activity */ public function test_bigbluebuttonbn_print_recent_activity(): void { global $CFG; $this->initialise_mock_server(); $this->resetAfterTest(); $generator = $this->getDataGenerator(); $user = $generator->create_and_enrol($this->get_course()); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); // Now create a couple of logs. $timestart = time() - HOURSECS; $instance = instance::get_from_instanceid($bbactivity->id); $recordings = $this->create_recordings_for_instance($instance, [['name' => "Pre-Recording 1"]]); $this->setUser($user); // Important so the logs are set to this user. logger::log_meeting_joined_event($instance, 0); logger::log_meeting_joined_event($instance, 0); logger::log_recording_played_event($instance, $recordings[0]->id); $this->setAdminUser(); // Test that everything is displayed. ob_start(); bigbluebuttonbn_print_recent_activity($this->get_course(), true, $timestart); $output = ob_get_contents(); ob_end_clean(); $this->assertStringContainsString('Meeting joined', $output); $this->assertStringContainsString(fullname($user), $output); // Test that username are displayed in a different format. $CFG->alternativefullnameformat = 'firstname lastname firstnamephonetic lastnamephonetic middlename alternatename'; $expectedname = "$user->firstname $user->lastname $user->firstnamephonetic " . "$user->lastnamephonetic $user->middlename $user->alternatename"; ob_start(); bigbluebuttonbn_print_recent_activity($this->get_course(), false, $timestart); $output = ob_get_contents(); ob_end_clean(); $this->assertStringContainsString('Meeting joined', $output); $this->assertStringNotContainsString($expectedname, $output); // Test that nothing is displayed as per timestart. ob_start(); bigbluebuttonbn_print_recent_activity($this->get_course(), true, time()); $output = ob_get_contents(); ob_end_clean(); $this->assertEmpty($output); } /** * Check extra capabilities return value * * @covers ::bigbluebuttonbn_get_extra_capabilities */ public function test_bigbluebuttonbn_get_extra_capabilities(): void { $this->resetAfterTest(); $this->assertEquals(['moodle/site:accessallgroups'], bigbluebuttonbn_get_extra_capabilities()); } /** * Check form definition * * @covers ::bigbluebuttonbn_reset_course_form_definition */ public function test_bigbluebuttonbn_reset_course_form_definition(): void { global $CFG, $PAGE; $this->initialise_mock_server(); $PAGE->set_course($this->get_course()); $this->setAdminUser(); $this->resetAfterTest(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); include_once($CFG->dirroot . '/mod/bigbluebuttonbn/mod_form.php'); $data = new stdClass(); $data->instance = $bbactivity; $data->id = $bbactivity->id; $data->course = $bbactivity->course; $form = new mod_bigbluebuttonbn_mod_form($data, 1, $bbactivitycm, $this->get_course()); $refclass = new ReflectionClass("mod_bigbluebuttonbn_mod_form"); $formprop = $refclass->getProperty('_form'); /* @var $mform MoodleQuickForm quickform object definition */ $mform = $formprop->getValue($form); bigbluebuttonbn_reset_course_form_definition($mform); $this->assertNotNull($mform->getElement('bigbluebuttonbnheader')); } /** * Check defaults for form * * @covers ::bigbluebuttonbn_reset_course_form_defaults */ public function test_bigbluebuttonbn_reset_course_form_defaults(): void { global $CFG; $this->resetAfterTest(); $results = bigbluebuttonbn_reset_course_form_defaults($this->get_course()); $this->assertEquals([ 'reset_bigbluebuttonbn_events' => 0, 'reset_bigbluebuttonbn_tags' => 0, 'reset_bigbluebuttonbn_logs' => 0, 'reset_bigbluebuttonbn_recordings' => 0, ], $results); } /** * Reset user data * * @covers ::bigbluebuttonbn_reset_userdata */ public function test_bigbluebuttonbn_reset_userdata(): void { global $DB; $this->resetAfterTest(); $data = new stdClass(); $user = $this->getDataGenerator()->create_user(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); $this->getDataGenerator()->enrol_user($user->id, $this->course->id); $this->setUser($user); logger::log_meeting_joined_event(instance::get_from_instanceid($bbactivity->id), 0); $data->courseid = $this->get_course()->id; $data->reset_bigbluebuttonbn_tags = true; $data->reset_bigbluebuttonbn_logs = true; $data->course = $bbactivity->course; // Add and Join. $this->assertCount(2, $DB->get_records('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => $bbactivity->id])); $results = bigbluebuttonbn_reset_userdata($data); $this->assertCount(0, $DB->get_records('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => $bbactivity->id])); $this->assertEquals([ 'component' => 'BigBlueButton', 'item' => 'Deleted tags', 'error' => false ], $results[0] ); } /** * Reset user data in a course and checks it does not delete logs elsewhere * * @covers ::bigbluebuttonbn_reset_userdata */ public function test_bigbluebuttonbn_reset_userdata_in_a_course(): void { global $DB; $this->resetAfterTest(); $data = new stdClass(); $user = $this->getDataGenerator()->create_user(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); $this->getDataGenerator()->enrol_user($user->id, $this->course->id); logger::log_meeting_joined_event(instance::get_from_instanceid($bbactivity->id), 0); // Now create another activity in a course and add a couple of logs. // Aim is to make sure that only logs from one course are deleted. $course1 = $this->getDataGenerator()->create_course(); list($bbactivitycontext1, $bbactivitycm1, $bbactivity1) = $this->create_instance($course1); logger::log_meeting_joined_event(instance::get_from_instanceid($bbactivity1->id), 0); $data->courseid = $this->get_course()->id; $data->reset_bigbluebuttonbn_tags = true; $data->reset_bigbluebuttonbn_logs = true; $data->course = $bbactivity->course; // Add and Join. $this->assertCount(2, $DB->get_records('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => $bbactivity->id])); $this->assertCount(2, $DB->get_records('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => $bbactivity1->id])); bigbluebuttonbn_reset_userdata($data); $this->assertCount(0, $DB->get_records('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => $bbactivity->id])); $this->assertCount(2, $DB->get_records('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => $bbactivity1->id])); } /** * Reset user data in a course but do not delete logs * * @covers ::bigbluebuttonbn_reset_userdata */ public function test_bigbluebuttonbn_reset_userdata_logs_not_deleted(): void { global $DB; $this->resetAfterTest(); $data = new stdClass(); $user = $this->getDataGenerator()->create_user(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); $this->getDataGenerator()->enrol_user($user->id, $this->course->id); $this->setUser($user); logger::log_meeting_joined_event(instance::get_from_instanceid($bbactivity->id), 0); $data->courseid = $this->get_course()->id; $data->reset_bigbluebuttonbn_logs = false; $data->course = $bbactivity->course; // Add and Join. $this->assertCount(2, $DB->get_records('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => $bbactivity->id])); bigbluebuttonbn_reset_userdata($data); $this->assertCount(2, $DB->get_records('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => $bbactivity->id])); } /** * Reset user data and make sure grades are reset. * * @covers ::bigbluebuttonbn_reset_userdata */ public function test_bigbluebuttonbn_reset_userdata_with_grades(): void { global $DB; $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance($this->course, ['grade' => 100]); $this->getDataGenerator()->enrol_user($user->id, $this->course->id); $this->setUser($user); // Pass a user grade. $grade = []; $gradeitem = $DB->get_record('grade_items', [ 'courseid' => $this->course->id, 'itemtype' => 'mod', 'itemmodule' => 'bigbluebuttonbn', 'iteminstance' => $bbactivity->id, ]); $grade[$user->id] = [ 'userid' => $user->id, 'rawgrade' => 75, ]; grade_update( source: 'mod/bigbluebuttonbn', courseid: $this->course->id, itemtype: 'mod', itemmodule: 'bigbluebuttonbn', iteminstance: $bbactivity->id, itemnumber: 0, grades: $grade ); $params = [ 'userid' => $user->id, 'itemid' => $gradeitem->id, 'rawgrade' => 75, ]; $gradegrades = $DB->get_records('grade_grades', $params); $this->assertCount(1, $gradegrades); // Reset user data. $data = new stdClass(); $data->courseid = $this->course->id; $data->course = $bbactivity->course; $data->reset_gradebook_grades = 1; bigbluebuttonbn_reset_userdata($data); // Check grades have been reset. $gradegrades = $DB->get_records('grade_grades', $params); $this->assertCount(0, $gradegrades); } /** * Check course module * * @covers ::bigbluebuttonbn_get_coursemodule_info */ public function test_bigbluebuttonbn_get_coursemodule_info(): void { $this->resetAfterTest(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); $info = bigbluebuttonbn_get_coursemodule_info($bbactivitycm); $this->assertEquals($info->name, $bbactivity->name); } /** * Check update since * * @covers ::bigbluebuttonbn_check_updates_since */ public function test_bigbluebuttonbn_check_updates_since(): void { $this->resetAfterTest(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); $result = bigbluebuttonbn_check_updates_since($bbactivitycm, 0); $expected = json_encode([ 'configuration' => [ 'updated' => false, ], 'contentfiles' => [ 'updated' => false, ], 'introfiles' => [ 'updated' => false, ], 'completion' => [ 'updated' => false, ], 'gradeitems' => [ 'updated' => false, ], 'outcomes' => [ 'updated' => false, ], ]); $this->assertEquals($expected, json_encode($result)); } /** * Check event action (calendar) * * @covers ::mod_bigbluebuttonbn_core_calendar_provide_event_action */ public function test_mod_bigbluebuttonbn_core_calendar_provide_event_action(): void { global $DB; $this->initialise_mock_server(); $this->resetAfterTest(); $this->setAdminUser(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); // Standard use case, the meeting start and we want add an action event to join the meeting. $event = $this->create_action_event($this->get_course(), $bbactivity, logger::EVENT_MEETING_START); $factory = new \core_calendar\action_factory(); $actionevent = mod_bigbluebuttonbn_core_calendar_provide_event_action($event, $factory); $this->assertEquals("Join session", $actionevent->get_name()); // User has already joined the meeting (there is log event EVENT_JOIN already for this user). $instance = instance::get_from_instanceid($bbactivity->id); logger::log_meeting_joined_event($instance, 0); $bbactivity->closingtime = time() - 1000; $bbactivity->openingtime = time() - 2000; $DB->update_record('bigbluebuttonbn', $bbactivity); $event = $this->create_action_event($this->get_course(), $bbactivity, logger::EVENT_MEETING_START); $actionevent = mod_bigbluebuttonbn_core_calendar_provide_event_action($event, $factory); $this->assertNull($actionevent); } /** * Creates an action event. * * @param stdClass $course The course the bigbluebutton activity is in * @param stdClass $bbbactivity The bigbluebutton activity to create an event for * @param string $eventtype The event type. eg. ASSIGN_EVENT_TYPE_DUE. * @return bool|calendar_event */ private function create_action_event(stdClass $course, stdClass $bbbactivity, string $eventtype) { $event = new stdClass(); $event->name = 'Calendar event'; $event->modulename = 'bigbluebuttonbn'; $event->courseid = $course->id; $event->instance = $bbbactivity->id; $event->type = CALENDAR_EVENT_TYPE_ACTION; $event->priority = CALENDAR_EVENT_USER_OVERRIDE_PRIORITY; $event->eventtype = $eventtype; $event->timestart = time(); return calendar_event::create($event); } /** * Test setting navigation admin menu * * @covers ::bigbluebuttonbn_extend_settings_navigation */ public function test_bigbluebuttonbn_extend_settings_navigation_admin(): void { global $PAGE, $CFG; $this->resetAfterTest(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); $CFG->bigbluebuttonbn_meetingevents_enabled = true; $PAGE->set_cm($bbactivitycm); $PAGE->set_context(context_module::instance($bbactivitycm->id)); $PAGE->set_url('/mod/bigbluebuttonbn/view.php', ['id' => $bbactivitycm->id]); $settingnav = $PAGE->settingsnav; $this->setAdminUser(); $node = navigation_node::create('testnavigationnode'); bigbluebuttonbn_extend_settings_navigation($settingnav, $node); $this->assertCount(1, $node->get_children_key_list()); } /** * Check additional setting menu * * @covers ::bigbluebuttonbn_extend_settings_navigation */ public function test_bigbluebuttonbn_extend_settings_navigation_user(): void { global $PAGE, $CFG; $this->resetAfterTest(); $generator = $this->getDataGenerator(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); $user = $generator->create_user(); $this->setUser($user); list($course, $bbactivitycmuser) = get_course_and_cm_from_instance($bbactivity->id, 'bigbluebuttonbn'); $CFG->bigbluebuttonbn_meetingevents_enabled = true; $PAGE->set_cm($bbactivitycmuser); $PAGE->set_context(context_module::instance($bbactivitycm->id)); $PAGE->set_url('/mod/bigbluebuttonbn/view.php', ['id' => $bbactivitycm->id]); $settingnav = $PAGE->settingsnav; $node = navigation_node::create('testnavigationnode'); bigbluebuttonbn_extend_settings_navigation($settingnav, $node); $this->assertCount(0, $node->get_children_key_list()); } /** * Check the visibility on calendar * @covers ::mod_bigbluebuttonbn_core_calendar_is_event_visible */ public function test_mod_bigbluebuttonbn_core_calendar_is_event_visible(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); $bbactivity->closingtime = time() - 1000; $bbactivity->openingtime = time() - 2000; $DB->update_record('bigbluebuttonbn', $bbactivity); $event = $this->create_action_event($this->get_course(), $bbactivity, logger::EVENT_MEETING_START); $this->assertFalse(mod_bigbluebuttonbn_core_calendar_is_event_visible($event)); $bbactivity->closingtime = time() + 1000; $DB->update_record('bigbluebuttonbn', $bbactivity); $event = $this->create_action_event($this->get_course(), $bbactivity, logger::EVENT_MEETING_START); $this->assertTrue(mod_bigbluebuttonbn_core_calendar_is_event_visible($event)); $event->instance = 0; $this->assertFalse(mod_bigbluebuttonbn_core_calendar_is_event_visible($event)); } } PK\ y5N5Ntests/meeting_test.phpnu[. /** * Meeting test. * * @package mod_bigbluebuttonbn * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) */ namespace mod_bigbluebuttonbn; use mod_bigbluebuttonbn\test\testcase_helper_trait; /** * Meeting tests class. * * @package mod_bigbluebuttonbn * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) * @covers \mod_bigbluebuttonbn\meeting * @coversDefaultClass \mod_bigbluebuttonbn\meeting */ final class meeting_test extends \advanced_testcase { use testcase_helper_trait; /** * Setup Test */ public function setUp(): void { parent::setUp(); $this->initialise_mock_server(); // We do not force the group mode so we can change the activity group mode during test. $this->course = $this->getDataGenerator()->create_course(['groupmode' => SEPARATEGROUPS]); $this->getDataGenerator()->create_group(['name' => 'G1', 'courseid' => $this->course->id]); $this->getDataGenerator()->create_group(['name' => 'G2', 'courseid' => $this->course->id]); } /** * Get a list of possible test (dataprovider) * * @return array[] */ public static function get_instance_types_meeting_info(): array { return [ 'Instance Type ALL - No Group' => [ 'type' => instance::TYPE_ALL, 'groupname' => null, 'groupmode' => NOGROUPS, 'canjoin' => ['useringroup' => true, 'usernotingroup' => true], ], 'Instance Type ALL - Group 1 - Visible groups' => [ 'type' => instance::TYPE_ALL, 'groupname' => 'G1', 'groupmode' => VISIBLEGROUPS, 'canjoin' => ['useringroup' => true, 'usernotingroup' => true], ], 'Instance Type ALL - Group 1 - Separate groups' => [ 'type' => instance::TYPE_ALL, 'groupname' => 'G1', 'groupmode' => SEPARATEGROUPS, 'canjoin' => ['useringroup' => true, 'usernotingroup' => false], ], 'Instance Type ROOM Only - No Group' => [ 'type' => instance::TYPE_ROOM_ONLY, 'groupname' => null, 'groupmode' => NOGROUPS, 'canjoin' => ['useringroup' => true, 'usernotingroup' => true], ], 'Instance Type ROOM Only - Group 1 - Visible groups' => [ 'type' => instance::TYPE_ROOM_ONLY, 'groupname' => 'G1', 'groupmode' => VISIBLEGROUPS, 'canjoin' => ['useringroup' => true, 'usernotingroup' => true], ], 'Instance Type ROOM Only - Group 1 - Separate groups' => [ 'type' => instance::TYPE_ROOM_ONLY, 'groupname' => 'G1', 'groupmode' => SEPARATEGROUPS, 'canjoin' => ['useringroup' => true, 'usernotingroup' => false], ], 'Instance Type Recording Only - No Group' => [ 'type' => instance::TYPE_RECORDING_ONLY, 'groupname' => null, 'groupmode' => NOGROUPS, 'canjoin' => ['useringroup' => false, 'usernotingroup' => false] ], 'Instance Type Recording Only - Group 1' => [ 'type' => instance::TYPE_RECORDING_ONLY, 'groupname' => 'G1', 'groupmode' => VISIBLEGROUPS, 'canjoin' => ['useringroup' => false, 'usernotingroup' => false] ] ]; } /** * Test that create meeing is working for all types. * * @dataProvider get_instance_types_meeting_info * @param int $type * @param string|null $groupname * @covers ::create_meeting * @covers ::create_meeting_data * @covers ::create_meeting_metadata */ public function test_create_meeting(int $type, ?string $groupname, $groupmode, $canjoin): void { $this->resetAfterTest(); [$meeting, $useringroup, $usernotingroup, $groupid, $activity] = $this->prepare_meeting($type, $groupname, SEPARATEGROUPS, false); $meeting->create_meeting(); $meetinginfo = $meeting->get_meeting_info(); $this->assertNotNull($meetinginfo); $this->assertEquals($activity->id, $meetinginfo->bigbluebuttonbnid); $this->assertFalse($meetinginfo->statusrunning); $this->assertStringContainsString("is ready", $meetinginfo->statusmessage); $this->assertEquals($groupid, $meetinginfo->groupid); } /** * Test for get meeting info for all types * * @param int $type * @param string|null $groupname * @dataProvider get_instance_types_meeting_info * @covers ::get_meeting_info * @covers ::do_get_meeting_info */ public function test_get_meeting_info(int $type, ?string $groupname, $groupmode, $canjoin): void { $this->resetAfterTest(); [$meeting, $useringroup, $usernotingroup, $groupid, $activity] = $this->prepare_meeting($type, $groupname); $meetinginfo = $meeting->get_meeting_info(); $this->assertNotNull($meetinginfo); $this->assertEquals($activity->id, $meetinginfo->bigbluebuttonbnid); $this->assertTrue($meetinginfo->statusrunning); $this->assertStringContainsString("in progress", $meetinginfo->statusmessage); $this->assertEquals($groupid, $meetinginfo->groupid); $meeting->end_meeting(); $meeting->update_cache(); $meetinginfo = $meeting->get_meeting_info(); $this->assertFalse($meetinginfo->statusrunning); if ($type == instance::TYPE_ALL) { $this->assertTrue($meetinginfo->features['showroom']); $this->assertTrue($meetinginfo->features['showrecordings']); } else if ($type == instance::TYPE_ROOM_ONLY) { $this->assertTrue($meetinginfo->features['showroom']); $this->assertFalse($meetinginfo->features['showrecordings']); } else if ($type == instance::TYPE_RECORDING_ONLY) { $this->assertFalse($meetinginfo->features['showroom']); $this->assertTrue($meetinginfo->features['showrecordings']); } } /** * Test can join is working for all types * * @param int $type * @param string|null $groupname * @param int $groupmode * @param array $canjoin * @dataProvider get_instance_types_meeting_info * @covers ::can_join */ public function test_can_join(int $type, ?string $groupname, int $groupmode, array $canjoin): void { $this->resetAfterTest(); [$meeting, $useringroup, $usernotingroup, $groupid, $activity] = $this->prepare_meeting($type, $groupname, $groupmode); $this->setUser($useringroup); $meeting->update_cache(); $this->assertEquals($canjoin['useringroup'], $meeting->can_join()); if ($meeting->can_join()) { $meetinginfo = $meeting->get_meeting_info(); $this->assertStringContainsString("The session is in progress.", $meetinginfo->statusmessage); } if ($groupname) { $this->setUser($usernotingroup); $meeting->update_cache(); $this->assertEquals($canjoin['usernotingroup'], $meeting->can_join()); } } /** * Test can join is working if opening/closing time are set * * @param int $type * @param string|null $groupname * @param int $groupmode * @param array $canjoin * @param array $dates * @dataProvider get_data_can_join_with_dates * @covers ::can_join */ public function test_can_join_with_dates(int $type, ?string $groupname, int $groupmode, array $canjoin, array $dates): void { // Apply the data provider relative values to now. array_walk($dates, function(&$val) { $val = time() + $val; }); $this->resetAfterTest(); [$meeting, $useringroup, $usernotingroup, $groupid, $activity] = $this->prepare_meeting($type, $groupname, $groupmode, true, $dates); $this->setUser($useringroup); $meeting->update_cache(); $this->assertEquals($canjoin['useringroup'], $meeting->can_join()); // We check that admin can not join outside opening/closing times either. $this->setAdminUser(); $this->assertEquals(false, $meeting->can_join()); if ($groupname) { $this->setUser($usernotingroup); $meeting->update_cache(); $this->assertEquals($canjoin['usernotingroup'], $meeting->can_join()); $this->setAdminUser(); $this->assertEquals(false, $meeting->can_join()); } } /** * Test can join is working if the "Wait for moderator to join" setting is set and a moderator has not yet joined. * * @covers ::join * @covers ::join_meeting */ public function test_join_wait_for_moderator_not_joined(): void { $this->resetAfterTest(); $this->setAdminUser(); $bbbgenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); $student = $this->getDataGenerator()->create_and_enrol($this->get_course()); $meetinginfo = [ 'course' => $this->get_course()->id, 'type' => instance::TYPE_ALL, 'wait' => 1, ]; $activity = $bbbgenerator->create_instance($meetinginfo, [ 'wait' => 1, ]); $instance = instance::get_from_instanceid($activity->id); $meeting = new meeting($instance); // The moderator has not joined. $this->setUser($student); $meeting->update_cache(); $this->expectException(\mod_bigbluebuttonbn\local\exceptions\meeting_join_exception::class); meeting::join_meeting($instance); } /** * Test can join is working if the "Wait for moderator to join" setting is set and a moderator has already joined. * * @covers ::join * @covers ::join_meeting */ public function test_join_wait_for_moderator_is_joined(): void { $this->resetAfterTest(); $this->setAdminUser(); $bbbgenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); $moderator = $this->getDataGenerator()->create_and_enrol($this->get_course(), 'editingteacher'); $student = $this->getDataGenerator()->create_and_enrol($this->get_course()); $meetinginfo = [ 'course' => $this->get_course()->id, 'type' => instance::TYPE_ALL, 'wait' => 1, 'moderators' => 'role:editingteacher', ]; $activity = $bbbgenerator->create_instance($meetinginfo, [ 'wait' => 1, ]); $instance = instance::get_from_instanceid($activity->id); $meeting = new meeting($instance); $bbbgenerator->create_meeting([ 'instanceid' => $instance->get_instance_id(), ]); $this->setUser($moderator); $meeting->update_cache(); $joinurl = $meeting->join(logger::ORIGIN_BASE); $this->assertIsString($joinurl); $this->join_meeting($joinurl); $meeting->update_cache(); $this->assertCount(1, $meeting->get_attendees()); // The student can now join the meeting as a moderator is present. $this->setUser($student); $joinurl = $meeting->join(logger::ORIGIN_BASE); $this->assertIsString($joinurl); } /** * Test can join is working if the "user limit" setting is set and reached. * * @covers ::join * @covers ::join_meeting */ public function test_join_user_limit_reached(): void { $this->resetAfterTest(); set_config('bigbluebuttonbn_userlimit_editable', true); $this->setAdminUser(); $bbbgenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); $moderator = $this->getDataGenerator()->create_and_enrol($this->get_course(), 'editingteacher'); $student1 = $this->getDataGenerator()->create_and_enrol($this->get_course()); $student2 = $this->getDataGenerator()->create_and_enrol($this->get_course()); $meetinginfo = [ 'course' => $this->get_course()->id, 'type' => instance::TYPE_ALL, 'userlimit' => 2, ]; $activity = $bbbgenerator->create_instance($meetinginfo, [ 'userlimit' => 2, ]); $instance = instance::get_from_instanceid($activity->id); $meeting = new meeting($instance); $bbbgenerator->create_meeting([ 'instanceid' => $instance->get_instance_id(), ]); // Moderator joins the meeting. $this->setUser($moderator); $this->join_meeting($meeting->join(logger::ORIGIN_BASE)); $meeting->update_cache(); $this->assertEquals(1, $meeting->get_participant_count()); // Student1 joins the meeting. $this->setUser($student1); $this->join_meeting($meeting->join(logger::ORIGIN_BASE)); $meeting->update_cache(); $this->assertEquals(2, $meeting->get_participant_count()); $this->assertTrue($instance->has_user_limit_been_reached($meeting->get_participant_count())); // Student2 tries to join but the limit has been reached. $this->setUser($student2); $meeting->update_cache(); $this->assertFalse($meeting->can_join()); $this->expectException(\mod_bigbluebuttonbn\local\exceptions\meeting_join_exception::class); meeting::join_meeting($instance); } /** * Test that attendees returns the right list of attendees * * @covers ::get_attendees */ public function test_get_attendees(): void { $this->resetAfterTest(); [$meeting, $useringroup, $usernotingroup, $groupid, $activity] = $this->prepare_meeting(instance::TYPE_ALL, null, NOGROUPS, true); $this->setUser($useringroup); $this->join_meeting($meeting->join(logger::ORIGIN_BASE)); $meeting->update_cache(); $this->assertCount(1, $meeting->get_attendees()); $otheruser = $this->getDataGenerator()->create_and_enrol($this->get_course()); $this->setUser($otheruser); $meeting->update_cache(); $this->join_meeting($meeting->join(logger::ORIGIN_BASE)); $meeting->update_cache(); $this->assertCount(2, $meeting->get_attendees()); } /** * Test that attendees returns the right list of attendees * * @covers ::get_attendees */ public function test_participant_count(): void { $this->resetAfterTest(); [$meeting, $useringroup, $usernotingroup, $groupid, $activity] = $this->prepare_meeting(instance::TYPE_ALL, null, NOGROUPS, true); $this->setUser($useringroup); $this->join_meeting($meeting->join(logger::ORIGIN_BASE)); $meeting->update_cache(); $meetinginfo = $meeting->get_meeting_info(); $this->assertEquals(1, $meetinginfo->participantcount); $this->assertEquals(1, $meetinginfo->totalusercount); $this->assertEquals(0, $meetinginfo->moderatorcount); $this->setUser($usernotingroup); $this->join_meeting($meeting->join(logger::ORIGIN_BASE)); $meeting->update_cache(); $meetinginfo = $meeting->get_meeting_info(); $this->assertEquals(2, $meetinginfo->participantcount); $this->assertEquals(2, $meetinginfo->totalusercount); $this->assertEquals(0, $meetinginfo->moderatorcount); $this->setAdminUser(); $this->join_meeting($meeting->join(logger::ORIGIN_BASE)); $meeting->update_cache(); $meetinginfo = $meeting->get_meeting_info(); $this->assertEquals(2, $meetinginfo->participantcount); $this->assertEquals(3, $meetinginfo->totalusercount); $this->assertEquals(1, $meetinginfo->moderatorcount); } /** * Send a join meeting API CALL * * @param string $url */ protected function join_meeting(string $url) { $curl = new \curl(); $url = new \moodle_url($url); $curl->get($url->out_omit_querystring(), $url->params()); } /** * Get a list of possible test (dataprovider) * * @return array[] */ public static function get_data_can_join_with_dates(): array { return [ 'Instance Type ALL - No Group - Closed in past' => [ 'type' => instance::TYPE_ALL, 'groupname' => null, 'groupmode' => NOGROUPS, 'canjoin' => ['useringroup' => false, 'usernotingroup' => false], 'dates' => ['openingtime' => -7200, 'closingtime' => -3600] ], 'Instance Type ALL - No Group - Open in future' => [ 'type' => instance::TYPE_ALL, 'groupname' => null, 'groupmode' => NOGROUPS, 'canjoin' => ['useringroup' => false, 'usernotingroup' => false], 'dates' => ['openingtime' => 3600, 'closingtime' => 7200] ], ]; } /** * Helper to prepare for a meeting * * @param int $type * @param string|null $groupname * @param int $groupmode * @param bool $createmeeting * @param array $dates * @return array */ protected function prepare_meeting(int $type, ?string $groupname, int $groupmode = SEPARATEGROUPS, bool $createmeeting = true, array $dates = []) { $this->setAdminUser(); $bbbgenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); $groupid = 0; $useringroup = $this->getDataGenerator()->create_and_enrol($this->get_course()); $usernotingroup = $this->getDataGenerator()->create_and_enrol($this->get_course()); if (!empty($groupname)) { $groupid = groups_get_group_by_name($this->get_course()->id, $groupname); $this->getDataGenerator()->create_group_member(['groupid' => $groupid, 'userid' => $useringroup->id]); } $meetinginfo = [ 'course' => $this->get_course()->id, 'type' => $type ]; if ($dates) { $meetinginfo = array_merge($meetinginfo, $dates); }; $activity = $bbbgenerator->create_instance($meetinginfo, ['groupmode' => $groupmode]); $instance = instance::get_from_instanceid($activity->id); if ($groupid) { $instance->set_group_id($groupid); } if ($createmeeting) { // Create the meetings on the mock server, so we can join it as a simple user. $bbbgenerator->create_meeting([ 'instanceid' => $instance->get_instance_id(), 'groupid' => $instance->get_group_id() ]); } $meeting = new meeting($instance); return [$meeting, $useringroup, $usernotingroup, $groupid, $activity]; } } PK\)Z tests/logger_test.phpnu[. /** * BBB Library tests class. * * @package mod_bigbluebuttonbn * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) */ namespace mod_bigbluebuttonbn; use mod_bigbluebuttonbn\test\testcase_helper_trait; /** * Tests for the logger class. * * @package mod_bigbluebuttonbn * @copyright 2018 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) * @covers \mod_bigbluebuttonbn\logger * @coversDefaultClass \mod_bigbluebuttonbn\logger */ final class logger_test extends \advanced_testcase { use testcase_helper_trait; /** * Test delete instance logs */ public function test_log_instance_deleted(): void { global $DB; $this->resetAfterTest(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); $instance = instance::get_from_instanceid($bbactivity->id); logger::log_instance_deleted($instance); $this->assertTrue($DB->record_exists('bigbluebuttonbn_logs', [ 'bigbluebuttonbnid' => $bbactivity->id, 'log' => logger::EVENT_DELETE, ])); } /** * Test log method */ public function test_log_recording_played_event(): void { global $DB; $this->resetAfterTest(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); $instance = instance::get_from_instanceid($bbactivity->id); logger::log_recording_played_event($instance, 1); $this->assertTrue($DB->record_exists('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => $instance->get_instance_id()])); } } PK\.>,tests/output/recording_row_playback_test.phpnu[. namespace mod_bigbluebuttonbn\output; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\recording; use mod_bigbluebuttonbn\test\testcase_helper_trait; /** * Recording row * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent.david [at] call-learning [dt] fr) */ final class recording_row_playback_test extends \advanced_testcase { use testcase_helper_trait; /** * Setup for test */ public function setUp(): void { parent::setUp(); $this->initialise_mock_server(); } /** * Recording sample data */ const RECORDING_DATA = [ [ 'status' => recording::RECORDING_STATUS_PROCESSED, 'playback' => [ 'format' => [ [ 'type' => 'podcast', 'url' => 'http://mypodcast', 'processingTime' => 0, 'length' => 0, ], [ 'type' => 'presentation', 'url' => 'http://mypresentation', 'processingTime' => 0, 'length' => 0, ], [ 'type' => 'video', 'url' => 'http://myvideo', 'processingTime' => 0, 'length' => 0, ], [ 'type' => 'settings', 'url' => 'http://mysettings', 'processingTime' => 0, 'length' => 0, ] ] ] ] ]; /** * Should this recording be included ? * * @param string $role * @param array $canview * @param object|null $globalsettings * @return void * @covers \recording_row_playback::should_be_included * @dataProvider should_be_included_data_provider */ public function test_should_be_included(string $role, array $canview, ?object $globalsettings = null): void { global $PAGE; $this->resetAfterTest(); ['recordings' => $recordingsdata, 'activity' => $activity] = $this->create_activity_with_recordings( $this->get_course(), instance::TYPE_ALL, self::RECORDING_DATA ); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $activity->course, $role); if (!empty($globalsettings)) { foreach ((array) $globalsettings as $key => $value) { set_config($key, $value); } } $this->setUser($user); $recording = new recording(0, $recordingsdata[0]); $rowplayback = new recording_row_playback($recording, instance::get_from_instanceid($activity->id)); $rowinfo = $rowplayback->export_for_template($PAGE->get_renderer('mod_bigbluebuttonbn')); $playbacktypes = array_map(function($playback) { foreach ($playback->attributes as $attributearray) { if (in_array('data-target', $attributearray)) { return $attributearray['value']; } } return ''; }, $rowinfo->playbacks); $this->assertEmpty(array_diff($canview, $playbacktypes)); } /** * Data provider for the should be included method * * @return array */ public static function should_be_included_data_provider(): array { return [ 'editingteacher user should see all' => [ 'role' => 'editingteacher', 'canview' => ['video', 'presentation', 'podcast', 'settings'], ], 'student can see only default' => [ 'role' => 'student', 'canview' => ['video', 'presentation'], ], 'student can see only default except when we add more format to all users' => [ 'role' => 'student', 'canview' => ['video', 'presentation', 'settings'], 'globalsettings' => (object) ['bigbluebuttonbn_recording_safe_formats' => 'video,presentation,settings'] ] ]; } /** * Test recording link is rendered for imported recordings. * * @return void * @covers \recording_row_playback::should_be_included */ public function test_show_recording_links(): void { global $PAGE; $this->resetAfterTest(); set_config('bigbluebuttonbn_importrecordings_enabled', 1); $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); ['recordings' => $recordingsdata, 'activity' => $activity] = $this->create_activity_with_recordings( $this->get_course(), instance::TYPE_ALL, self::RECORDING_DATA ); $recording = new recording(0, $recordingsdata[0]); $instance = instance::get_from_instanceid($activity->id); // Now create a new activity and import the recording. $newactivity = $plugingenerator->create_instance([ 'course' => $instance->get_course_id(), 'type' => instance::TYPE_ALL, 'name' => 'Example 2', ]); $plugingenerator->create_meeting([ 'instanceid' => $newactivity->id, ]); $newinstance = instance::get_from_instanceid($newactivity->id); // Import recording into new instance. $importedrecording = $recording->create_imported_recording($newinstance); $importedrowplayback = new recording_row_playback($importedrecording, $newinstance); $importedrowinfo = $importedrowplayback->export_for_template($PAGE->get_renderer('mod_bigbluebuttonbn')); $this->assertNotEmpty($importedrowinfo->playbacks); } } PK\·] tests/search_test.phpnu[. namespace mod_bigbluebuttonbn; use advanced_testcase; use context_course; use context_module; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\test\testcase_helper_trait; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php'); /** * Provides the unit tests for forum search. * * @package mod_bigbluebuttonbn * @copyright 2021 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) * @covers \mod_bigbluebuttonbn\search\tags * @covers \mod_bigbluebuttonbn\search\activity */ final class search_test extends advanced_testcase { use testcase_helper_trait; /** * @var string Area id */ protected $bbbactivtyarea = null; public function setUp(): void { parent::setUp(); set_config('enableglobalsearch', true); $this->bbbactivtyarea = \core_search\manager::get_search_area('mod_bigbluebuttonbn-activity'); } /** * Test for indexing * * @return void */ public function test_indexing(): void { $this->resetAfterTest(); // Setup test data. $generator = $this->getDataGenerator(); $course = $generator->create_course(); $bbactivity1 = $generator->create_module('bigbluebuttonbn', ['course' => $course->id, 'name' => 'BBB 1']); $bbactivity2 = $generator->create_module('bigbluebuttonbn', ['course' => $course->id, 'name' => 'BBB 2']); // Get all surveys for indexing - note that there are special entries in the table with // course zero which should not be returned. $rs = $this->bbbactivtyarea->get_document_recordset(); $this->assertEquals(2, iterator_count($rs)); $rs->close(); // Test specific context and course context. $rs = $this->bbbactivtyarea->get_document_recordset(0, context_module::instance($bbactivity1->cmid)); $this->assertEquals(1, iterator_count($rs)); $rs->close(); $rs = $this->bbbactivtyarea->get_document_recordset(0, context_course::instance($course->id)); $documents = iterator_to_array($rs); $this->assertEquals(2, count($documents)); $rs->close(); } } PK\Fߐ0tests/task/completion_update_state_task_test.phpnu[. namespace mod_bigbluebuttonbn\task; use advanced_testcase; use mod_bigbluebuttonbn\task\completion_update_state; /** * Completion_update_state task tests. * * @package mod_bigbluebuttonbn * @copyright 2024 Catalyst IT * @author Matthew Hilton * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \mod_bigbluebuttonbn\task\completion_update_state */ final class completion_update_state_task_test extends advanced_testcase { /** * Providers data to test_invalid_customdata test * @return array */ public static function invalid_customdata_provider(): array { return [ 'empty' => [ 'customdata' => [], 'expectoutput' => "", 'expectexceptionmessage' => "Task customdata was missing bigbluebuttonbn->id or userid", ], 'bbb id set but is invalid' => [ 'customdata' => [ 'bigbluebuttonbn' => -1, ], 'expectoutput' => "", 'expectexceptionmessage' => "Task customdata was missing bigbluebuttonbn->id or userid", ], 'bbb id is valid, but there is no user' => [ 'customdata' => [ 'bigbluebuttonbn' => ':bbb', ], 'expectoutput' => "", 'expectexceptionmessage' => "Task customdata was missing bigbluebuttonbn->id or userid", ], 'bbb id is valid, but the user is not given' => [ 'customdata' => [ 'bigbluebuttonbn' => ':bbb', ], 'expectoutput' => "", 'expectexceptionmessage' => "Task customdata was missing bigbluebuttonbn->id or userid", ], 'bbb id is valid, but the user given is invalid' => [ 'customdata' => [ 'bigbluebuttonbn' => ':bbb', 'userid' => -1, ], 'expectoutput' => "User does not exist, ignoring.\n", 'expectexceptionmessage' => "", ], 'bbb and userid is valid' => [ 'customdata' => [ 'bigbluebuttonbn' => ':bbb', 'userid' => ':userid', ], // Expects this output, since all the necessary data is there. 'expectoutput' => "Task completion_update_state running for user :userid\nCompletion not enabled\n", 'expectexceptionmessage' => "", ], ]; } /** * Tests the task handles an invalid cmid gracefully. * @param array $customdata customdata to set (with placeholders to replace with real data). * @param string $expectoutput any output expected from the test, or empty to not expect output. * @param string $expectexceptionmessage exception message expected from test, or empty to expect nothing. * @dataProvider invalid_customdata_provider */ public function test_invalid_customdata(array $customdata, string $expectoutput, string $expectexceptionmessage): void { $this->resetAfterTest(); $customdata = (object) $customdata; // Replace any placeholders in the customdata. if (!empty($customdata->bigbluebuttonbn) && $customdata->bigbluebuttonbn == ':bbb') { $course = $this->getDataGenerator()->create_course(); $module = $this->getDataGenerator()->create_module('bigbluebuttonbn', ['course' => $course->id]); $customdata->bigbluebuttonbn = $module; } $user = $this->getDataGenerator()->create_user(); // Replace userid placeholders. if (!empty($customdata->userid) && $customdata->userid == ':userid') { $customdata->userid = $user->id; } $task = new completion_update_state(); $task->set_custom_data($customdata); if (!empty($expectoutput)) { $expectoutput = str_replace(':userid', $user->id, $expectoutput); $this->expectOutputString($expectoutput); } if (!empty($expectexceptionmessage)) { $this->expectExceptionMessage($expectexceptionmessage); } $task->execute(); } } PK\M=uJJ)tests/task/check_recordings_task_test.phpnu[. namespace mod_bigbluebuttonbn\task; use advanced_testcase; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\recording; use mod_bigbluebuttonbn\test\testcase_helper_trait; /** * Test class for the check_pending_recordings and check_dismissed_recordings task * * @package mod_bigbluebuttonbn * @copyright 2022 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \mod_bigbluebuttonbn\task\check_dismissed_recordings * @covers \mod_bigbluebuttonbn\task\check_pending_recordings * @covers \mod_bigbluebuttonbn\recording::sync_pending_recordings_from_server */ final class check_recordings_task_test extends advanced_testcase { use testcase_helper_trait; /** * @var $RECORDINGS_DATA array fake recording data. */ const RECORDINGS_DATA = [ ['name' => 'Recording 1'], ['name' => 'Recording 2'], ['name' => 'Recording 3'], ['name' => 'Recording 4'], ]; /** * Setup for test */ public function setUp(): void { parent::setUp(); $this->initialise_mock_server(); $this->resetAfterTest(); } /** * Test that dismissed recordings are retrieved */ public function test_check_dismissed_recordings(): void { $this->create_meeting_and_recordings(recording::RECORDING_STATUS_DISMISSED); $this->assertEquals(4, recording::count_records()); $this->assertEquals(0, recording::count_records(['status' => recording::RECORDING_STATUS_PROCESSED])); $task = new check_dismissed_recordings(); ob_start(); $task->execute(); ob_end_clean(); $this->assertEquals(4, recording::count_records(['status' => recording::RECORDING_STATUS_PROCESSED])); } /** * Test that pending recordings are retrieved */ public function test_check_pending_recordings(): void { $this->create_meeting_and_recordings(); $this->assertEquals(4, recording::count_records()); $this->assertEquals(0, recording::count_records(['status' => recording::RECORDING_STATUS_PROCESSED])); $task = new check_pending_recordings(); ob_start(); $task->execute(); ob_end_clean(); $this->assertEquals(4, recording::count_records(['status' => recording::RECORDING_STATUS_PROCESSED])); } /** * Create sample meeting and recording. * * @param int $status status for the newly created recordings * @return array recording data (not the persistent class but plain object) */ private function create_meeting_and_recordings(int $status = recording::RECORDING_STATUS_AWAITING): array { $generator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); $course = $this->getDataGenerator()->create_course(); $activity = $generator->create_instance([ 'course' => $course->id, 'type' => instance::TYPE_ALL ]); $instance = instance::get_from_instanceid($activity->id); $generator->create_meeting([ 'instanceid' => $instance->get_instance_id(), 'groupid' => $instance->get_group_id() ]); foreach (self::RECORDINGS_DATA as $data) { $rdata = $generator->create_recording( array_merge([ 'bigbluebuttonbnid' => $instance->get_instance_id(), 'groupid' => $instance->get_group_id() ], $data) ); $recording = new recording($rdata->id); $recording->set('status', $status); $recording->save(); $recordings[] = $rdata; } return $recordings; } } PK\՜ %tests/task/send_guest_emails_test.phpnu[. namespace mod_bigbluebuttonbn\task; use advanced_testcase; /** * Send guest email tests * * @package mod_bigbluebuttonbn * @copyright 2019 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \mod_bigbluebuttonbn\task\send_guest_emails * @coversDefaultClass \mod_bigbluebuttonbn\task\send_guest_emails */ final class send_guest_emails_test extends advanced_testcase { /** * Check if set instance ID works correctly * */ public function test_send_emails(): void { $this->resetAfterTest(); $emailsink = $this->redirectEmails(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $instancedata = $generator->create_module('bigbluebuttonbn', [ 'course' => $course->id, ]); $moderatoruser = $generator->create_user(); $guestemail = new send_guest_emails(); $guestemail->set_custom_data( [ 'emails' => ['test1@email.com', 'test2@email.com'], 'useridfrom' => $moderatoruser->id ] ); $guestemail->set_instance_id($instancedata->id); \core\task\manager::queue_adhoc_task($guestemail); $this->runAdhocTasks(); // Check the events. $messages = $emailsink->get_messages(); $this->assertCount(2, $messages); $this->assertEquals('Invitation: BigBlueButton 1 session in Test course 1', $messages[0]->subject); $this->assertEquals('noreply@www.example.com', $messages[0]->from); $this->assertEquals('test1@email.com', $messages[0]->to); $this->assertEquals('test2@email.com', $messages[1]->to); } } PK\e *tests/task/base_send_notification_test.phpnu[. namespace mod_bigbluebuttonbn\task; use advanced_testcase; /** * Class containing the scheduled task for lti module. * * @package mod_bigbluebuttonbn * @copyright 2023 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \mod_bigbluebuttonbn\task\base_send_notification * @coversDefaultClass \mod_bigbluebuttonbn\task\base_send_notification */ final class base_send_notification_test extends advanced_testcase { /** * Returns mock base_send_notification class * * @return base_send_notification */ private function get_mock(): base_send_notification { return $this->getMockForAbstractClass( base_send_notification::class, [], '', true, true, true, [] ); } /** * Returns reflection method for base_send_notification->get_instance * * @return \ReflectionMethod */ private function get_instance_reflection(): \ReflectionMethod { $rc = new \ReflectionClass(base_send_notification::class); return $rc->getMethod('get_instance'); } /** * Check if set instance ID works correctly */ public function test_set_instance_id(): void { $this->resetAfterTest(); $stub = $this->get_mock(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $instancedata = $generator->create_module('bigbluebuttonbn', [ 'course' => $course->id, ]); $stub->set_instance_id($instancedata->id); $rcm = $this->get_instance_reflection(); $instance = $rcm->invoke($stub); $this->assertNotNull($instance); $this->assertEquals($instancedata->id, $instance->get_instance_id()); } /** * Check if instanceid missing is checked and handled. */ public function test_set_instanceid_missing(): void { $this->resetAfterTest(); $stub = $this->get_mock(); $rcm = $this->get_instance_reflection(); // This should throw a coding exception since there is no instanceid set. $this->expectException(\coding_exception::class); $this->expectExceptionMessage("Task custom data was missing instanceid"); $rcm->invoke($stub); } } PK\^-S?S?+tests/task/upgrade_recordings_task_test.phpnu[. namespace mod_bigbluebuttonbn\task; use advanced_testcase; use core\task\manager; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\logger; use mod_bigbluebuttonbn\recording; use mod_bigbluebuttonbn\test\testcase_helper_trait; /** * Class containing the scheduled task for lti module. * * @package mod_bigbluebuttonbn * @copyright 2019 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \mod_bigbluebuttonbn\task\upgrade_recordings_task */ final class upgrade_recordings_task_test extends advanced_testcase { use testcase_helper_trait; /** * @var object $instance */ protected $instance = null; /** * @var array $groups all groups */ protected $groups = []; /** * Setup for test */ public function setUp(): void { parent::setUp(); $this->initialise_mock_server(); $this->resetAfterTest(); } /** * Upgrade task test */ public function test_upgrade_recordings_basic(): void { global $DB; $this->setup_basic_data(); upgrade_recordings_task::schedule_upgrade_per_meeting(false);; // The first run will lead to all of them being processed, and none left over. // A new job is always queued on a successful run. $this->runAdhocTasks(upgrade_recordings_task::class); $this->assertEquals(0, $DB->count_records('bigbluebuttonbn_logs', ['log' => logger::EVENT_CREATE])); $this->assertEquals(75, recording::count_records(['imported' => '0'])); // Old logs are kept but renamed. $this->assertEquals(75, $DB->count_records('bigbluebuttonbn_logs', ['log' => logger::EVENT_CREATE_MIGRATED])); $this->assertEquals(15, recording::count_records(['groupid' => $this->groups[0]->id, 'imported' => '0', 'status' => recording::RECORDING_STATUS_PROCESSED])); $this->assertEquals(15, recording::count_records(['groupid' => $this->groups[1]->id, 'imported' => '0', 'status' => recording::RECORDING_STATUS_PROCESSED])); $this->assertEquals(45, recording::count_records(['groupid' => 0, 'imported' => '0', 'status' => recording::RECORDING_STATUS_PROCESSED])); // The second run will lead to no change in the number of logs, but no further jobs will be queued. upgrade_recordings_task::schedule_upgrade_per_meeting(); $this->runAdhocTasks(upgrade_recordings_task::class); $this->assertEquals(75, recording::count_records()); // The first run will lead to all of them being processed, and none left over. // A new job is always queued on a successful run. $this->assertEmpty($DB->get_records_select( 'bigbluebuttonbn_logs', 'log = :logmatch AND ' . $DB->sql_like('meta', ':match'), [ 'logmatch' => 'Create', 'match' => '%true%' ] )); $this->runAdhocTasks(upgrade_recordings_task::class); $this->assertEmpty($DB->get_records_select( 'bigbluebuttonbn_logs', 'log = :logmatch AND ' . $DB->sql_like('meta', ':match'), [ 'logmatch' => 'Create', 'match' => '%true%' ] )); $this->assertEquals(0, $DB->count_records('bigbluebuttonbn_logs', ['log' => 'Create'])); // Ensure that logs match. $matchesarray = [ [ 'Executing .*', 'Fetching logs for conversion', "Creating new recording records", 'Migrated 30 recordings', ], [ 'Executing .*', 'Fetching logs for conversion', "Creating new recording records", 'Migrated 15 recordings', ], [ 'Executing .*', 'Fetching logs for conversion', "Creating new recording records", "Unable to find an activity for .*. This recording is headless", 'Migrated 15 recordings' ] ]; foreach ($matchesarray as $matches) { $this->expectOutputRegex('/' . implode('.*', $matches) . '/s'); } $this->resetDebugging(); // We might have debugging message that are sent by get_from_meetingid and can ignore them. } /** * Upgrade task test */ public function test_upgrade_recordings_imported_basic(): void { global $DB; $this->setup_basic_data(true); upgrade_recordings_task::schedule_upgrade_per_meeting(true);; // The first run will lead to all of them being processed, and none left over. // A new job is always queued on a successful run. $this->runAdhocTasks(upgrade_recordings_task::class); $this->assertEquals(0, $DB->count_records('bigbluebuttonbn_logs', ['log' => logger::EVENT_IMPORT])); $this->assertEquals(75, $DB->count_records('bigbluebuttonbn_logs', ['log' => logger::EVENT_IMPORT_MIGRATED])); $this->assertEquals(75, recording::count_records(['imported' => '1'])); $this->assertEquals(15, recording::count_records(['groupid' => $this->groups[0]->id, 'imported' => '1', 'status' => recording::RECORDING_STATUS_PROCESSED])); $this->assertEquals(15, recording::count_records(['groupid' => $this->groups[1]->id, 'imported' => '1', 'status' => recording::RECORDING_STATUS_PROCESSED])); $this->assertEquals(45, recording::count_records(['groupid' => 0, 'imported' => '1', 'status' => recording::RECORDING_STATUS_PROCESSED])); // The second run will lead to no change in the number of logs, but no further jobs will be queued. upgrade_recordings_task::schedule_upgrade_per_meeting(); $this->runAdhocTasks(upgrade_recordings_task::class); $this->assertEquals(75, recording::count_records(['imported' => '1'])); // The first run will lead to all of them being processed, and none left over. // A new job is always queued on a successful run. $this->assertEmpty($DB->get_records_select( 'bigbluebuttonbn_logs', 'log = :logmatch', [ 'logmatch' => 'Import' ] )); // Ensure that logs match. $matchesarray = [ [ 'Executing .*', 'Fetching logs for conversion', "Creating new recording records", 'Migrated 30 recordings', ], [ 'Executing .*', 'Fetching logs for conversion', "Creating new recording records", 'Migrated 15 recordings', ], [ 'Executing .*', 'Fetching logs for conversion', "Creating new recording records", "Unable to find an activity for .*. This recording is headless", 'Migrated 15 recordings' ] ]; foreach ($matchesarray as $matches) { $this->expectOutputRegex('/' . implode('.*', $matches) . '/s'); } $this->resetDebugging(); // We might have debugging message that are sent by get_from_meetingid and can ignore them. } /** * Upgrade recordings when we have missing recordings on the server * Basically, the recordings are imported and then we cannot the other because logs have been marked as migrated. */ public function test_upgrade_recordings_with_missing_recording_on_bbb_server(): void { global $DB; $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); [$teacher, $groups, $instance, $groupedinstance, $deletedinstance] = $this->setup_basic_course_and_meeting(); $this->create_log_entries($instance, $teacher->id, 5); $this->create_log_entries($instance, $teacher->id, 5, false, false); $this->assertEquals(10, $DB->count_records('bigbluebuttonbn_logs', ['log' => 'Create'])); // Schedule the run. upgrade_recordings_task::schedule_upgrade_per_meeting(); $this->runAdhocTasks(upgrade_recordings_task::class); // At this point only 5 are created, the rest is still in the queue. $this->assertEquals(0, $DB->count_records('bigbluebuttonbn_logs', ['log' => 'Create'])); $this->assertEquals(5, recording::count_records()); // Now create 5 recordings on the server. // Schedule the run. upgrade_recordings_task::schedule_upgrade_per_meeting(); for ($index = 0; $index < 5; $index++) { $plugingenerator->create_recording([ 'bigbluebuttonbnid' => $instance->get_instance_id(), 'groupid' => $instance->get_group_id(), 'starttime' => time(), 'endtime' => time() + HOURSECS, ], true); // Create another recording on the server. } $this->assertEquals(0, $DB->count_records('task_adhoc', ['classname' => '\\' . upgrade_recordings_task::class])); $this->runAdhocTasks(upgrade_recordings_task::class); // Ensure that logs match. // Ensure that logs match. $matchesarray = [ [ 'Executing .*', 'Fetching logs for conversion', "Creating new recording records", 'Migrated 5 recordings', ], ]; foreach ($matchesarray as $matches) { $this->expectOutputRegex('/' . implode('.*', $matches) . '/s'); } } /** * Upgrade task test with more recordings on the server than in the log : we add all recording and should have * no more logs. */ public function test_upgrade_recordings_with_more_recordings_on_bbb_server(): void { global $DB; $generator = $this->getDataGenerator(); // Create a course with student and teacher, and two groups. $this->course = $generator->create_course(); $user = $this->getDataGenerator()->create_and_enrol($this->course); // Create an ungrouped activity. $activity = $generator->create_module('bigbluebuttonbn', [ 'course' => $this->course->id, ]); $this->instance = instance::get_from_instanceid($activity->id); // We create 5 recordings in the log but no recording instance on the server. $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); $plugingenerator->create_meeting([ 'instanceid' => $this->instance->get_instance_id(), 'groupid' => $this->instance->get_group_id(), ]); $this->create_log_entries($this->instance, $user->id, 5); $plugingenerator->create_recording([ 'bigbluebuttonbnid' => $this->instance->get_instance_id(), 'groupid' => $this->instance->get_group_id(), 'starttime' => time(), 'endtime' => time() + HOURSECS, ], true); // Create another recording on the server. $this->assertEquals(5, $DB->count_records('bigbluebuttonbn_logs', ['log' => 'Create'])); upgrade_recordings_task::schedule_upgrade_per_meeting(); $this->runAdhocTasks(upgrade_recordings_task::class); $this->assertEquals(6, recording::count_records(['status' => recording::RECORDING_STATUS_PROCESSED])); $this->assertEquals(0, $DB->count_records('bigbluebuttonbn_logs', ['log' => 'Create'])); // Ensure that logs match. $matches = [ 'Executing .*', 'Fetching logs for conversion', "Creating new recording records", 'Migrated 6 recordings', ]; $this->expectOutputRegex('/' . implode('.*', $matches) . '/s'); } /** * Setup basic data for tests * * @param bool $importedrecording * @return void * @throws \coding_exception * @throws \moodle_exception */ protected function setup_basic_data($importedrecording = false) { global $DB; [$teacher, $groups, $instance, $groupedinstance, $deletedinstance] = $this->setup_basic_course_and_meeting(); $this->create_log_entries($instance, $teacher->id, 30, $importedrecording); foreach ($groups as $group) { $groupinstance = instance::get_group_instance_from_instance($groupedinstance, $group->id); $this->create_log_entries($groupinstance, $teacher->id, 15, $importedrecording); } $this->create_log_entries($deletedinstance, $teacher->id, 15, $importedrecording); course_delete_module($deletedinstance->get_cm_id()); // Truncate the recordings table to reflect what it would have looked like before this version. $DB->delete_records('bigbluebuttonbn_recordings'); $this->groups = $groups; $this->instance = $instance; } /** * Setup basic data for tests * * @return array * @throws \coding_exception * @throws \moodle_exception */ protected function setup_basic_course_and_meeting() { $generator = $this->getDataGenerator(); // Create a course with student and teacher, and two groups. $this->course = $generator->create_course(); $groups = []; $groups[] = $generator->create_group(['courseid' => $this->course->id]); $groups[] = $generator->create_group(['courseid' => $this->course->id]); $teacher = $generator->create_and_enrol($this->course, 'editingteacher'); $generator->create_and_enrol($this->course, 'student'); // Create a "normal" meeting. $instance = $this->create_meeting_for_logs(); // Create an grouped activity. $groupedinstance = $this->create_meeting_for_logs($groups); // Create an instance that will then be deleted. $deletedinstance = $this->create_meeting_for_logs(); // Create logs for an activity which no longer exists (because we deleted it). return [$teacher, $groups, $instance, $groupedinstance, $deletedinstance]; } /** * Create a meeting and return its instance * * @param array|null $groups * @return instance */ protected function create_meeting_for_logs(?array $groups = null) { $generator = $this->getDataGenerator(); $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); $params = [ 'course' => $this->course->id, ]; if (!empty($groups)) { $params['groupmode'] = SEPARATEGROUPS; } $activity = $generator->create_module('bigbluebuttonbn', $params); $instance = instance::get_from_instanceid($activity->id); if (!empty($groups)) { foreach ($groups as $group) { $groupinstance = instance::get_group_instance_from_instance($instance, $group->id); $plugingenerator->create_meeting([ 'instanceid' => $groupinstance->get_instance_id(), 'groupid' => $groupinstance->get_group_id(), ]); } } else { $plugingenerator->create_meeting([ 'instanceid' => $instance->get_instance_id(), 'groupid' => $instance->get_group_id(), ]); } return $instance; } } PK\C]]5tests/task/send_recording_ready_notification_test.phpnu[. namespace mod_bigbluebuttonbn\task; use advanced_testcase; /** * Class containing the scheduled task for lti module. * * @package mod_bigbluebuttonbn * @copyright 2019 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \mod_bigbluebuttonbn\task\base_send_notification * @covers \mod_bigbluebuttonbn\task\base_send_notification * @covers \mod_bigbluebuttonbn\task\send_recording_ready_notification */ final class send_recording_ready_notification_test extends advanced_testcase { /** * Test the sending of messages. */ public function test_recipients(): void { $this->resetAfterTest(); $this->preventResetByRollback(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $instancedata = $generator->create_module('bigbluebuttonbn', [ 'course' => $course->id, ]); // Create some users in the course, and some not. $editingteacher = $generator->create_and_enrol($course, 'editingteacher'); $teacher = $generator->create_and_enrol($course, 'teacher'); $students = [ $generator->create_and_enrol($course, 'student'), $generator->create_and_enrol($course, 'student'), $generator->create_and_enrol($course, 'student'), $generator->create_and_enrol($course, 'student'), $generator->create_and_enrol($course, 'student'), ]; $recipients = array_map(function($user) { return $user->id; }, $students); $recipients[] = $editingteacher->id; $recipients[] = $teacher->id; $unrelateduser = $generator->create_user(); $instancedata = $generator->create_module('bigbluebuttonbn', [ 'course' => $course->id, ]); $stub = $this->getMockBuilder(send_recording_ready_notification::class) ->onlyMethods([]) ->getMock(); $stub->set_instance_id($instancedata->id); // Capture events. $sink = $this->redirectMessages(); // Now execute. $stub->execute(); // Check the events. $messages = $sink->get_messages(); $this->assertCount(7, $messages); foreach ($messages as $message) { $this->assertNotFalse(array_search($message->useridto, $recipients)); $this->assertNotEquals($unrelateduser->id, $message->useridto); $this->assertEquals($editingteacher->id, $message->useridfrom); } } /** * Test the sending of messages. */ public function test_recipients_no_teacher(): void { $this->resetAfterTest(); $this->preventResetByRollback(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $instancedata = $generator->create_module('bigbluebuttonbn', [ 'course' => $course->id, ]); // Create some users in the course, and some not. $students = [ $generator->create_and_enrol($course, 'student'), $generator->create_and_enrol($course, 'student'), $generator->create_and_enrol($course, 'student'), $generator->create_and_enrol($course, 'student'), $generator->create_and_enrol($course, 'student'), ]; $recipients = array_map(function($user) { return $user->id; }, $students); $unrelateduser = $generator->create_user(); $instancedata = $generator->create_module('bigbluebuttonbn', [ 'course' => $course->id, ]); $stub = $this->getMockBuilder(send_recording_ready_notification::class) ->onlyMethods([]) ->getMock(); $stub->set_instance_id($instancedata->id); // Capture events. $sink = $this->redirectMessages(); // Now execute. $stub->execute(); // Check the events. $messages = $sink->get_messages(); $this->assertCount(5, $messages); $noreplyuser = \core_user::get_noreply_user(); foreach ($messages as $message) { $this->assertNotFalse(array_search($message->useridto, $recipients)); $this->assertNotEquals($unrelateduser->id, $message->useridto); $this->assertEquals($noreplyuser->id, $message->useridfrom); } } /** * Test that messages are not sent to a suspended user. */ public function test_messages_sent_suspended_user(): void { global $DB; $this->resetAfterTest(); $this->preventResetByRollback(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $instancedata = $generator->create_module('bigbluebuttonbn', [ 'course' => $course->id, ]); // Create some users in the course, and some not. $student = $generator->create_and_enrol($course, 'student'); $suspendedstudent = $generator->create_and_enrol($course, 'student'); $DB->set_field('user', 'suspended', 1, ['id' => $suspendedstudent->id]); $instancedata = $generator->create_module('bigbluebuttonbn', [ 'course' => $course->id, ]); $stub = $this->getMockBuilder(send_recording_ready_notification::class) ->onlyMethods([]) ->getMock(); $stub->set_instance_id($instancedata->id); // Capture events. $sink = $this->redirectMessages(); // Now execute. $stub->execute(); // Check the events. $messages = $sink->get_messages(); $this->assertCount(1, $messages); $noreplyuser = \core_user::get_noreply_user(); foreach ($messages as $message) { $this->assertEquals($student->id, $message->useridto); $this->assertNotEquals($suspendedstudent->id, $message->useridto); $this->assertEquals($noreplyuser->id, $message->useridfrom); } } } PK\v*O[[tests/plugin_test.phpnu[. namespace mod_bigbluebuttonbn; use advanced_testcase; use moodle_exception; /** * Tests for the Big Blue Button Plugin class. * * @package mod_bigbluebuttonbn * @copyright 2021 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \mod_bigbluebuttonbn\plugin */ final class plugin_test extends advanced_testcase { /** * Test html2text * * @covers ::html2text */ public function test_html2text(): void { $this->assertEquals('My text is in HTML', plugin::html2text('

My text is in HTML

', 100)); $this->assertEquals('My...', plugin::html2text('

My text is in HTML

', 2)); } /** * Test random_password * * @covers ::random_password */ public function test_random_password(): void { $password = plugin::random_password(10); $this->assertEquals(10, strlen($password)); $this->assertMatchesRegularExpression( '/[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789]+/', $password); } /** * Test generate_guest_meeting_credentials * * @covers ::generate_guest_meeting_credentials */ public function test_generate_guest_meeting_credentials(): void { [$guestlinkuid, $password] = plugin::generate_guest_meeting_credentials(); $this->assertEquals(40, strlen($guestlinkuid)); } } PK\ |7474$tests/completion/completion_test.phpnu[. namespace mod_bigbluebuttonbn\completion; use completion_info; use context_module; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\logger; use mod_bigbluebuttonbn\meeting; use mod_bigbluebuttonbn\test\testcase_helper_trait; /** * Tests for Big Blue Button Completion. * * @package mod_bigbluebuttonbn * @copyright 2021 - present, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent@call-learning.fr) * @covers \mod_bigbluebuttonbn\completion\custom_completion */ final class completion_test extends \advanced_testcase { use testcase_helper_trait; /** * Setup basic */ public function setUp(): void { parent::setUp(); $this->initialise_mock_server(); set_config('enablecompletion', true); // Enable completion for all tests. } /** * Completion with no rules: the completion is completed as soons as we view the course. */ public function test_get_completion_state_no_rules(): void { $this->resetAfterTest(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $completion = new custom_completion($bbactivitycm, $user->id); $result = $completion->get_overall_completion_state(); // No custom rules so complete by default. $this->assertEquals(COMPLETION_COMPLETE, $result); } /** * Completion with no rules and join meeting */ public function test_get_completion_state_no_rules_and_join_meeting(): void { $this->resetAfterTest(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Now create a couple of logs. $instance = instance::get_from_instanceid($bbactivity->id); logger::log_meeting_joined_event($instance, 0); // No custom rules and we joined once so complete. $completion = new custom_completion($bbactivitycm, $user->id); $result = $completion->get_overall_completion_state(); $this->assertEquals(COMPLETION_COMPLETE, $result); } /** * With state incomplete */ public function test_get_completion_state_incomplete(): void { $this->resetAfterTest(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); $bbactivitycm->override_customdata('customcompletionrules', [ 'completionengagementchats' => '1', 'completionattendance' => '1' ]); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $completion = new custom_completion($bbactivitycm, $user->id); $result = $completion->get_overall_completion_state(); $this->assertEquals(COMPLETION_INCOMPLETE, $result); } /** * With state complete */ public function test_get_completion_state_complete(): void { $this->resetAfterTest(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance( $this->get_course(), [ 'completion' => '2', 'completionengagementtalks' => 2, 'completionengagementchats' => 2, 'completionattendance' => 15 ] ); $instance = instance::get_from_instanceid($bbactivity->id); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Add a couple of fake logs. $overrides = ['meetingid' => $bbactivity->meetingid]; $meta = [ 'origin' => 0, 'data' => [ 'duration' => 300, // 300 seconds, i.e 5 mins. 'engagement' => [ 'chats' => 2, 'talks' => 2, ], ], ]; // We setup a couple of logs as per engagement and duration. logger::log_event_summary($instance, $overrides, $meta); logger::log_event_summary($instance, $overrides, $meta); $completion = new custom_completion($bbactivitycm, $user->id); $result = $completion->get_overall_completion_state(); $this->assertEquals(COMPLETION_INCOMPLETE, $result); // Now we have 15 mins. logger::log_event_summary($instance, $overrides, $meta); // Now that the meeting was joined, it should be complete. $completion = new custom_completion($bbactivitycm, $user->id); $result = $completion->get_overall_completion_state(); $this->assertEquals(COMPLETION_COMPLETE, $result); } /** * No rule description but active */ public function test_mod_bigbluebuttonbn_get_completion_active_rule_descriptions(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Two activities, both with automatic completion. One has the 'completionsubmit' rule, one doesn't. // Inspired from the same test in forum. list($bbactivitycontext, $cm1, $bbactivity) = $this->create_instance($this->get_course(), ['completion' => '2']); $cm1->override_customdata('customcompletionrules', [ 'completionattendance' => '1' ]); list($bbactivitycontext, $cm2, $bbactivity) = $this->create_instance($this->get_course(), ['completion' => '2']); $cm2->override_customdata('customcompletionrules', [ 'completionattendance' => '0' ]); $completioncm1 = new custom_completion($cm1, $user->id); // TODO: check the return value here as there might be an issue with the function compared to the forum for example. $this->assertEquals( [ 'completionengagementchats' => get_string('completionengagementchats_desc', 'mod_bigbluebuttonbn', 1), 'completionengagementtalks' => get_string('completionengagementtalks_desc', 'mod_bigbluebuttonbn', 1), 'completionattendance' => get_string('completionattendance_desc', 'mod_bigbluebuttonbn', 1), 'completionengagementraisehand' => get_string('completionengagementraisehand_desc', 'mod_bigbluebuttonbn', 1), 'completionengagementpollvotes' => get_string('completionengagementpollvotes_desc', 'mod_bigbluebuttonbn', 1), 'completionengagementemojis' => get_string('completionengagementemojis_desc', 'mod_bigbluebuttonbn', 1) ], $completioncm1->get_custom_rule_descriptions()); $completioncm2 = new custom_completion($cm2, $user->id); $this->assertEquals( [ 'completionengagementchats' => get_string('completionengagementchats_desc', 'mod_bigbluebuttonbn', 1), 'completionengagementtalks' => get_string('completionengagementtalks_desc', 'mod_bigbluebuttonbn', 1), 'completionattendance' => get_string('completionattendance_desc', 'mod_bigbluebuttonbn', 0), 'completionengagementraisehand' => get_string('completionengagementraisehand_desc', 'mod_bigbluebuttonbn', 1), 'completionengagementpollvotes' => get_string('completionengagementpollvotes_desc', 'mod_bigbluebuttonbn', 1), 'completionengagementemojis' => get_string('completionengagementemojis_desc', 'mod_bigbluebuttonbn', 1) ], $completioncm2->get_custom_rule_descriptions()); } /** * Completion View */ public function test_view(): void { $this->resetAfterTest(); $this->setAdminUser(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance( null, ['completion' => 2, 'completionview' => 1] ); // Trigger and capture the event. $sink = $this->redirectEvents(); // Check completion before viewing. $completion = new completion_info($this->get_course()); $completiondata = $completion->get_data($bbactivitycm); $this->assertEquals(0, $completiondata->viewed); $this->assertEquals(COMPLETION_NOT_VIEWED, $completiondata->completionstate); bigbluebuttonbn_view($bbactivity, $this->get_course(), $bbactivitycm, context_module::instance($bbactivitycm->id)); $events = $sink->get_events(); $this->assertTrue(count($events) > 1); // TODO : Here we have the module completion event triggered twice. // this might be a bug from 4.0 core and will need some further investigation. $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_bigbluebuttonbn\event\course_module_viewed', $event); $this->assertEquals($bbactivitycontext, $event->get_context()); $url = new \moodle_url('/mod/bigbluebuttonbn/view.php', ['id' => $bbactivitycontext->instanceid]); $this->assertEquals($url, $event->get_url()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); // Check completion status. $completion = new completion_info($this->get_course()); $completiondata = $completion->get_data($bbactivitycm); $this->assertEquals(1, $completiondata->viewed); $this->assertEquals(COMPLETION_COMPLETE, $completiondata->completionstate); } /** * Completion with no rules and join meeting * * @param array $customcompletionrules * @param array $events * @param int $expectedstate * @dataProvider custom_completion_data_provider */ public function test_get_completion_with_events(array $customcompletionrules, array $events, int $expectedstate): void { $this->resetAfterTest(); list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance( $this->get_course(), [ 'completion' => '2', ] ); $bbactivitycm->override_customdata('customcompletionrules', $customcompletionrules); $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Now create a couple of events. $instance = instance::get_from_instanceid($bbactivity->id); set_config('bigbluebuttonbn_meetingevents_enabled', true); $meeting = $plugingenerator->create_meeting([ 'instanceid' => $instance->get_instance_id(), 'groupid' => $instance->get_group_id(), 'participants' => json_encode([$user->id]) ]); foreach ($events as $edesc) { $plugingenerator->add_meeting_event($user, $instance, $edesc->name, $edesc->data ?? ''); } $result = $plugingenerator->send_all_events($instance); $this->assertNotEmpty($result->data); $data = json_decode(json_encode($result->data)); meeting::meeting_events($instance, $data); $completion = new custom_completion($bbactivitycm, $user->id); $result = $completion->get_overall_completion_state(); $this->assertEquals($expectedstate, $result); } /** * Data generator * * @return array[] */ public static function custom_completion_data_provider(): array { return [ 'simple' => [ 'customcompletionrules' => [ 'completionengagementtalks' => 1, 'completionengagementchats' => 1, ], 'events' => [ (object) ['name' => 'talks'], (object) ['name' => 'chats'] ], 'expectedstate' => COMPLETION_COMPLETE ], 'not right events' => [ 'customcompletionrules' => [ 'completionengagementchats' => 1, ], 'events' => [ (object) ['name' => 'talks'] ], 'expectedstate' => COMPLETION_INCOMPLETE ], 'attendance' => [ 'customcompletionrules' => [ 'completionattendance' => 1, ], 'events' => [ (object) ['name' => 'talks'], (object) ['name' => 'attendance', 'data' => '70'] ], 'expectedstate' => COMPLETION_COMPLETE ] ]; } } PK\VWWtests/instance_test.phpnu[. /** * Tests for the Big Blue Button Instance. * * @package mod_bigbluebuttonbn * @copyright 2021 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_bigbluebuttonbn; use advanced_testcase; use moodle_exception; /** * Tests for the Big Blue Button Instance. * * @package mod_bigbluebuttonbn * @copyright 2021 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \mod_bigbluebuttonbn\instance */ final class instance_test extends advanced_testcase { /** * Test get from * * @param string $function * @param string $field * @dataProvider get_from_location_provider * @covers ::get_from_instanceid * @covers ::get_from_cmid */ public function test_get_from(string $function, string $field): void { $this->resetAfterTest(); [ 'record' => $record, ] = $this->get_test_instance(); $instance = call_user_func("mod_bigbluebuttonbn\instance::{$function}", $record->{$field}); $this->assertInstanceOf(instance::class, $instance); $this->assertEquals($record->id, $instance->get_instance_id()); $this->assertEquals($record->cmid, $instance->get_cm_id()); $this->assertEquals($record->cmid, $instance->get_cm()->id); } /** * Get from location provider * * @return string[][] */ public static function get_from_location_provider(): array { return [ ['get_from_instanceid', 'id'], ['get_from_cmid', 'cmid'], ]; } /** * Get an instance from a cmid. * @covers ::get_from_cmid */ public function test_get_from_cmid(): void { $this->resetAfterTest(); [ 'record' => $record, 'cm' => $cm, ] = $this->get_test_instance(); $instance = instance::get_from_cmid($cm->id); $this->assertInstanceOf(instance::class, $instance); $this->assertEquals($record->id, $instance->get_instance_id()); $this->assertEquals($cm->id, $instance->get_cm()->id); } /** * If the instance was not found, and exception should be thrown. * @covers ::get_from_cmid */ public function test_get_from_cmid_not_found(): void { $this->assertNull(instance::get_from_cmid(100)); } /** * If the instance was not found, and exception should be thrown. * @covers ::get_from_instanceid */ public function test_get_from_instance_not_found(): void { $this->assertNull(instance::get_from_instanceid(100)); } /** * Get from meeting id * * @covers ::get_from_meetingid */ public function test_get_from_meetingid(): void { $this->resetAfterTest(); [ 'record' => $record, ] = $this->get_test_instance(); // The meetingid is confusingly made up of a meetingid field, courseid, instanceid, and groupid. $instance = instance::get_from_meetingid(sprintf( "%s-%s-%s", $record->meetingid, $record->course, $record->id )); $this->assertInstanceOf(instance::class, $instance); $this->assertEquals($record->id, $instance->get_instance_id()); $this->assertEquals($record->cmid, $instance->get_cm_id()); $this->assertEquals($record->cmid, $instance->get_cm()->id); } /** * Get the get_from_meetingid() function where the meetingid includes a groupid. * * @covers ::get_from_meetingid */ public function test_get_from_meetingid_group(): void { $this->resetAfterTest(); [ 'record' => $record, 'course' => $course, 'cm' => $cm, ] = $this->get_test_instance(); $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]); $instance = instance::get_from_meetingid( sprintf("%s-%s-%s[0]", $record->meetingid, $record->course, $record->id) ); $this->assertEquals($cm->instance, $instance->get_instance_id()); $this->assertEquals($cm->id, $instance->get_cm_id()); } /** * Test getting Meeting ID from log as Log field (meetingid) is the full meeting id (with courseid, and bigbluebuttonid). * * @covers ::get_from_meetingid */ public function test_get_from_meetingid_from_log(): void { global $DB; $this->resetAfterTest(); [ 'record' => $record, 'course' => $course, 'cm' => $cm, ] = $this->get_test_instance(); $instance = instance::get_from_cmid($cm->id); $instance->set_group_id(1); logger::log_meeting_joined_event($instance, 1); // Get the meeting ID from the logged "join" event. $meetingid = $DB->get_field('bigbluebuttonbn_logs', 'meetingid', [ 'courseid' => $course->id, 'bigbluebuttonbnid' => $instance->get_instance_id(), 'log' => 'Join', ], MUST_EXIST); $retrievedinstance = instance::get_from_meetingid($meetingid); $this->assertEquals($cm->instance, $retrievedinstance->get_instance_id()); $this->assertEquals($cm->id, $retrievedinstance->get_cm_id()); } /** * Ensure that invalid meetingids throw an appropriate exception. * * @dataProvider invalid_meetingid_provider * @param string $meetingid * @covers ::get_from_meetingid */ public function test_get_from_meetingid_invalid(string $meetingid): void { $this->expectException(moodle_exception::class); instance::get_from_meetingid($meetingid); } /** * Provide invalid meeting examples * * @return \string[][] */ public static function invalid_meetingid_provider(): array { // Meeting IDs are in the formats: // - -- // - --[] // Note: deducing the group from meeting id will soon be deprecated. return [ 'Non-numeric instanceid' => ['aaa-123-aaa'], ]; } /** * Test the get_all_instances_in_course function. * * @covers ::get_all_instances_in_course */ public function test_get_all_instances_in_course(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $records = []; for ($i = 0; $i < 5; $i++) { $this->getDataGenerator()->create_module('bigbluebuttonbn', [ 'course' => $course->id, ]); } $instances = instance::get_all_instances_in_course($course->id); $this->assertCount(5, $instances); foreach ($instances as $instance) { $this->assertInstanceOf(instance::class, $instance); } } /** * Get test instance from data * * @param array $data * @return array */ protected function get_test_instance(array $data = []): array { $course = $this->getDataGenerator()->create_course(); $record = $this->getDataGenerator()->create_module('bigbluebuttonbn', array_merge([ 'course' => $course->id, ], $data)); $cm = get_fast_modinfo($course)->instances['bigbluebuttonbn'][$record->id]; return [ 'course' => $course, 'record' => $record, 'cm' => $cm, ]; } /** * Test the get_meeting_id function for a meeting configured for a group. * * @covers ::get_meeting_id */ public function test_get_meeting_id_with_groups(): void { $this->resetAfterTest(); [ 'record' => $record, 'course' => $course, ] = $this->get_test_instance(); $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]); $instance = instance::get_from_instanceid($record->id); // No group. $this->assertEquals( sprintf("%s-%s-%s[0]", $record->meetingid, $record->course, $record->id), $instance->get_meeting_id(0) ); // Specified group. $this->assertEquals( sprintf("%s-%s-%s[%d]", $record->meetingid, $record->course, $record->id, $group->id), $instance->get_meeting_id($group->id) ); } /** * Test the get_meeting_id function for a meeting configured for a group. * * @covers ::get_meeting_id */ public function test_get_meeting_id_without_groups(): void { $this->resetAfterTest(); [ 'record' => $record, 'course' => $course, ] = $this->get_test_instance(); $instance = instance::get_from_instanceid($record->id); // No group. $this->assertEquals( sprintf("%s-%s-%s[0]", $record->meetingid, $record->course, $record->id), $instance->get_meeting_id(null) ); } /** * Data provider to check the various room_available scenarios' * * @return array */ public static function is_currently_open_provider(): array { return [ 'No opening or closing time set: Is open' => [null, null, true], 'Opening time set in the past, no closing: Is open' => [-DAYSECS, null, true], 'Opening time set in the future, no closing: Is closed' => [+DAYSECS, null, false], 'Closing time set in the past, no opening: Is closed' => [null, -DAYSECS, false], 'Closing time set in the future, no opening: Is open' => [null, +DAYSECS, true], 'Opening and closing in the past: Is closed' => [-WEEKSECS, -DAYSECS, false], 'Opening and closing in the future: Is closed' => [+DAYSECS, +WEEKSECS, false], 'Opening in the past, Closing in the future: Is open' => [-DAYSECS, +DAYSECS, true], ]; } /** * Check instance currently open * * @dataProvider is_currently_open_provider * @param null|int $openingtime * @param null|int $closingtime * @param bool $expected * @covers ::is_currently_open */ public function test_is_currently_open(?int $openingtime, ?int $closingtime, bool $expected): void { $stub = $this->getMockBuilder(instance::class) ->onlyMethods(['get_instance_var']) ->disableOriginalConstructor() ->getMock(); if ($openingtime) { $openingtime = $openingtime + time(); } if ($closingtime) { $closingtime = $closingtime + time(); } $stub->method('get_instance_var') ->willReturnCallback(function($var) use ($openingtime, $closingtime) { if ($var === 'openingtime') { return $openingtime; } return $closingtime; }); $this->assertEquals($expected, $stub->is_currently_open()); } /** * Ensure that the user_must_wait_to_join function works as expectd. * * @dataProvider user_must_wait_to_join_provider * @param bool $isadmin * @param bool $ismoderator * @param bool $haswaitingroom * @param bool $expected * @covers ::user_must_wait_to_join */ public function test_user_must_wait_to_join(bool $isadmin, bool $ismoderator, bool $haswaitingroom, bool $expected): void { $stub = $this->getMockBuilder(instance::class) ->onlyMethods([ 'get_instance_var', 'is_admin', 'is_moderator', ]) ->disableOriginalConstructor() ->getMock(); $stub->method('is_admin')->willReturn($isadmin); $stub->method('is_moderator')->willReturn($ismoderator); $stub->method('get_instance_var')->willReturn($haswaitingroom); $this->assertEquals($expected, $stub->user_must_wait_to_join()); } /** * Data provider for the user_must_wait_to_join function. * * @return array */ public static function user_must_wait_to_join_provider(): array { return [ 'Admins must never wait to join (waiting disabled)' => [true, false, false, false], 'Admins must never wait to join (waiting enabled)' => [true, false, true, false], 'Moderators must never wait to join (waiting disabled)' => [false, true, false, false], 'Moderators must never wait to join (waiting enabled)' => [false, true, true, false], 'Other users must wait to join if waiting enabled' => [false, false, true, true], 'Other users cannot wait to join if waiting disabled' => [false, false, false, false], ]; } /** * Ensure that the does_current_user_count_towards_user_limit function works as expectd. * * @dataProvider does_current_user_count_towards_user_limit_provider * @param bool $isadmin * @param bool $ismoderator * @param bool $expected * @covers ::does_current_user_count_towards_user_limit */ public function test_does_current_user_count_towards_user_limit( bool $isadmin, bool $ismoderator, bool $expected ): void { $stub = $this->getMockBuilder(instance::class) ->onlyMethods([ 'is_admin', 'is_moderator', ]) ->disableOriginalConstructor() ->getMock(); $stub->method('is_admin')->willReturn($isadmin); $stub->method('is_moderator')->willReturn($ismoderator); $this->assertEquals($expected, $stub->does_current_user_count_towards_user_limit()); } /** * Data provider for the does_current_user_count_towards_user_limit function. * * @return array */ public static function does_current_user_count_towards_user_limit_provider(): array { return [ 'Admin does not count' => [true, false, false], 'Moderator does not count' => [false, true, false], 'Other users do count' => [false, false, true], ]; } /** * Ensure that the does_current_user_count_towards_user_limit function works as expectd. * * @dataProvider get_current_user_password_provider * @param bool $isadmin * @param bool $ismoderator * @param bool $expectedmodpassword * @covers ::get_current_user_password */ public function test_get_current_user_password(bool $isadmin, bool $ismoderator, bool $expectedmodpassword): void { $stub = $this->getMockBuilder(instance::class) ->onlyMethods([ 'is_admin', 'is_moderator', 'get_moderator_password', 'get_viewer_password', ]) ->disableOriginalConstructor() ->getMock(); $stub->method('is_admin')->willReturn($isadmin); $stub->method('is_moderator')->willReturn($ismoderator); $stub->method('get_moderator_password')->willReturn('Moderator Password'); $stub->method('get_viewer_password')->willReturn('Viewer Password'); if ($expectedmodpassword) { $this->assertEquals('Moderator Password', $stub->get_current_user_password()); } else { $this->assertEquals('Viewer Password', $stub->get_current_user_password()); } } /** * Data provider for the get_current_user_password function. * * @return array */ public static function get_current_user_password_provider(): array { return [ 'Admin is a moderator' => [true, false, true], 'Moderator is a moderator' => [false, true, true], 'Others are a viewer' => [false, false, false], ]; } /** * Ensure that the get_current_user_role function works as expected. * * @dataProvider get_current_user_role_provider * @param bool $isadmin * @param bool $ismoderator * @param bool $expectedmodrole * @covers ::get_current_user_role */ public function test_get_current_user_role(bool $isadmin, bool $ismoderator, bool $expectedmodrole): void { $stub = $this->getMockBuilder(instance::class) ->onlyMethods([ 'is_admin', 'is_moderator', ]) ->disableOriginalConstructor() ->getMock(); $stub->method('is_admin')->willReturn($isadmin); $stub->method('is_moderator')->willReturn($ismoderator); if ($expectedmodrole) { $this->assertEquals('MODERATOR', $stub->get_current_user_role()); } else { $this->assertEquals('VIEWER', $stub->get_current_user_role()); } } /** * Data provider for the get_current_user_role function. * * @return array */ public static function get_current_user_role_provider(): array { return [ 'Admin is a moderator' => [true, false, true], 'Moderator is a moderator' => [false, true, true], 'Others are a viewer' => [false, false, false], ]; } /** * Tests for the allow_recording_start_stop function. * * @dataProvider allow_recording_start_stop_provider * @param bool $isrecorded * @param bool $showbuttons * @param bool $expected * @covers ::allow_recording_start_stop */ public function test_allow_recording_start_stop( bool $isrecorded, bool $showbuttons, bool $expected ): void { $stub = $this->getMockBuilder(instance::class) ->onlyMethods([ 'is_recorded', 'should_show_recording_button', ]) ->disableOriginalConstructor() ->getMock(); $stub->method('is_recorded')->willReturn($isrecorded); $stub->method('should_show_recording_button')->willReturn($showbuttons); $this->assertEquals($expected, $stub->allow_recording_start_stop()); } /** * Data provider for the allow_recording_start_stop function. * * @return array */ public static function allow_recording_start_stop_provider(): array { return [ 'Meeting is not recorded: No start/stop' => [false, false, false], 'Meeting recorded, Buttons shown: Allow' => [true, true, true], 'Meeting recorded, Buttons not shown: Deny' => [true, false, false], ]; } /** * Test get user id (guest or current user) * @covers \mod_bigbluebuttonbn\instance::get_user_id */ public function test_get_user_id(): void { $this->resetAfterTest(); $this->setUser(null); ['record' => $record ] = $this->get_test_instance(); $instance = instance::get_from_instanceid($record->id); $this->assertEquals(0, $instance->get_user_id()); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $this->assertEquals($user->id, $instance->get_user_id()); } /** * Test guest access URL * * @covers ::get_guest_access_url */ public function test_get_guest_access_url(): void { global $CFG; $this->resetAfterTest(); ['record' => $record ] = $this->get_test_instance(['guestallowed' => true]); $CFG->bigbluebuttonbn['guestaccess_enabled'] = 1; $instance = instance::get_from_instanceid($record->id); $this->assertNotEmpty($instance->get_guest_access_url()); } /** * Test guest allowed flag * * @covers ::is_guest_allowed */ public function test_is_guest_allowed(): void { global $CFG; $this->resetAfterTest(); ['record' => $record ] = $this->get_test_instance(['guestallowed' => true]); $CFG->bigbluebuttonbn['guestaccess_enabled'] = 1; $instance = instance::get_from_instanceid($record->id); $this->assertTrue($instance->is_guest_allowed()); $CFG->bigbluebuttonbn['guestaccess_enabled'] = 0; $this->assertFalse($instance->is_guest_allowed()); } /** * Test private method get_instance_info_retriever * * @covers ::get_instance_info_retriever */ public function test_get_instance_info_retriever(): void { $this->resetAfterTest(); [ 'record' => $record, 'cm' => $cm, ] = $this->get_test_instance(); $instance = instance::get_from_instanceid($record->id); $instancereflection = new \ReflectionClass($instance); $getinstanceinforetriever = $instancereflection->getMethod('get_instance_info_retriever'); $this->assertInstanceOf('\mod_bigbluebuttonbn\instance', $getinstanceinforetriever->invoke($instance, $record->id, instance::IDTYPE_INSTANCEID)); $this->assertEquals($cm->id, $instance->get_cm_id()); } /** * Test guest access password * * @covers ::get_guest_access_password */ public function get_guest_access_password() { global $CFG; $this->resetAfterTest(); ['record' => $record ] = $this->get_test_instance(['guestallowed' => true]); $CFG->bigbluebuttonbn['guestaccess_enabled'] = 1; $instance = instance::get_from_instanceid($record->id); $this->assertNotEmpty($instance->get_guest_access_password()); } } PK\>:1tests/behat/fixtures/show_simpleplugin_values.phpnu[. /** * Test page for simple subplugins. * * @package mod_bigbluebuttonbn * @copyright 2024 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Shamiso Jaravaza (shamiso [dt] jaravaza [at] blindsidenetworks [dt] com) */ require_once(__DIR__ . '/../../../../../config.php'); defined('BEHAT_SITE_RUNNING') || die(); global $PAGE, $OUTPUT; require_login(); $PAGE->set_context(context_system::instance()); $PAGE->set_url('/mod/bigbluebuttonbn/tests/behat/fixtures/show_simpleplugin_values.php'); echo $OUTPUT->header(); $bnid = $DB->get_field('bigbluebuttonbn', 'id', ['name' => 'BBB Instance name']); // Check that the subplugin has the correct meeting events data. $meetingevent = $DB->get_field('bbbext_simple', 'meetingevents', ['bigbluebuttonbnid' => $bnid]); $meetingevent = json_decode($meetingevent, true); $chats = $meetingevent['data']['attendees'][0]['engagement']['chats']; echo "

(BBB Instance name): meetingevents: {$chats}

"; echo $OUTPUT->footer(); PK\3!!!tests/behat/meeting_roles.featurenu[@mod @mod_bigbluebuttonbn Feature: Test that meeting roles are sent to the server In order to ensure that start meeting capabilities are respected As a teacher I need meeting roles to be sent to the meeting Background: Given a BigBlueButton mock server is configured And I enable "bigbluebuttonbn" "mod" plugin @javascript Scenario Outline: Users should receive the appropriate role when joining the meeting Given the following course exists: | name | Test course | | shortname | C1 | And the following "users" exist: | username | firstname | lastname | email | | traverst | Terry | Travers | t.travers@example.com | And the following "course enrolments" exist: | user | course | role | | traverst | C1 | | And the following "activity" exists: | course | C1 | | activity | bigbluebuttonbn | | name | Room recordings | | idnumber | Room recordings | | moderators | | | viewers | | When I am on the "Room recordings" Activity page logged in as traverst And I click on "Join session" "link" And I switch to "bigbluebutton_conference" window Then I should see "" in the "attendeeRole" "mod_bigbluebuttonbn > Meeting field" Examples: | moderators | viewers | moodlerole | meetingrole | | | | student | VIEWER | | role:editingteacher | | editingteacher | MODERATOR | | role:student | | editingteacher | VIEWER | | role:student | | student | MODERATOR | | user:traverst | | student | MODERATOR | PK\1Ht t /tests/behat/add_instance_in_empty_state.featurenu[@mod @mod_bigbluebuttonbn @javascript Feature: I can create a bigbluebuttonbn instance with default server In case the BigBlueButton server has not been configured As a user I want to see a notification message Background: Make sure that a course is created Given I enable "bigbluebuttonbn" "mod" plugin And the following "courses" exist: | fullname | shortname | category | | Test course | Test course | 0 | And the following "users" exist: | username | firstname | lastname | email | | user1 | User1G1 | 1 | user1@example.com | | teacher1 | TeacherG1 | 1 | teacher1@example.com | And the following "course enrolments" exist: | user | course | role | | user1 | Test course | student | | teacher1 | Test course | editingteacher | And the following "activities" exist: | activity | course | name | type | | bigbluebuttonbn | Test course | BBB Instance name | 0 | | bigbluebuttonbn | Test course | BBB Instance name 2 | 1 | | bigbluebuttonbn | Test course | BBB Instance name 3 | 2 | And I am on the "Test course" "course" page logged in as "admin" Scenario Outline: Add an activity using default server for the three types of instance types When I change window size to "large" And I add a bigbluebuttonbn activity to course "Test course" section "1" And I select "" from the "Instance type" singleselect Then I should see "Restrict access" Examples: | type | | Room with recordings | | Room only | | Recordings only | Scenario Outline: Users should see a notification message when accessing activities if the default server is used When I am on the "BBB Instance name" Activity page logged in as Then "Join session" "link" should exist And I "" Examples: | user | shouldseemessage | messageexist | | user1 | The BigBlueButton server credentials will soon expire. | should not see | | teacher1 | The BigBlueButton server credentials will soon expire. | should see | | admin | The BigBlueButton server credentials will soon expire. | should see | PK\4 tests/behat/completion.featurenu[@mod @mod_bigbluebuttonbn Feature: As a user I can complete a BigblueButtonBN activity by usual or custom criteria Background: Given I enable "bigbluebuttonbn" "mod" plugin And the following "courses" exist: | fullname | shortname | category | enablecompletion | | Test course | C1 | 0 | 1 | And the following "activities" exist: | activity | name | intro | course | idnumber | type | recordings_imported | | bigbluebuttonbn | RoomRecordings | Test Room Recording description | C1 | bigbluebuttonbn1 | 0 | 0 | And the following "users" exist: | username | firstname | lastname | email | | traverst | Terry | Travers | t.travers@example.com | And the following "course enrolments" exist: | user | course | role | | traverst | C1 | student | Scenario: I set the completion to standard type of completion. Given I am on the "RoomRecordings" "bigbluebuttonbn activity" page logged in as admin When I click on "Settings" "link" And I expand all fieldsets And I set the following fields to these values: | Add requirements | 1 | | View the activity | 1 | And I press "Save and display" And I log out And I am on the "RoomRecordings" "bigbluebuttonbn activity" page logged in as traverst Then I should see "Done: View" @javascript Scenario: I set the completion type to custom completion Given a BigBlueButton mock server is configured And the following config values are set as admin: | bigbluebuttonbn_meetingevents_enabled | 1 | And I am on the "RoomRecordings" "bigbluebuttonbn activity" page logged in as admin And I click on "Settings" "link" And I expand all fieldsets And I set the following fields to these values: | Add requirements | 1 | | Chats | 1 | And I press "Save and display" # We start the meeting here so to make sure that meta_analytics-callback-url is set. And the following "mod_bigbluebuttonbn > meeting" exists: | activity | RoomRecordings | And I log out And I am on the "RoomRecordings" "bigbluebuttonbn activity" page logged in as traverst When I click on "Join session" "link" And I switch to "bigbluebutton_conference" window And I wait until the page is ready And I follow "End Meeting" And the BigBlueButtonBN server has received the following events from user "traverst": | instancename | eventtype | eventdata | | RoomRecordings | chats | 1 | # Selenium driver does not like the click action to be done before we # automatically close the window so we need to make sure that the window # is closed before. And I close all opened windows And I switch to the main window And the BigBlueButtonBN activity "RoomRecordings" has sent recording all its events And I run all adhoc tasks And I reload the page Then I should see "Done: Participate in 1 chat(s)" @javascript Scenario: Validate completion when registering live sessions Given the following config values are set as admin: | bigbluebuttonbn_meetingevents_enabled | 1 | When I am on the "RoomRecordings" "bigbluebuttonbn activity" page logged in as admin And I click on "Validate completion" "link" Then I should see "Validate completion has been triggered." PK\#eetests/behat/recordings.featurenu[@mod @mod_bigbluebuttonbn Feature: The recording can be managed through the room page As a user I am able to see the relevant recording for a given bigbluebutton activity and modify its parameters Background: Make sure that import recording is enabled and course, activities and recording exists Given a BigBlueButton mock server is configured And I enable "bigbluebuttonbn" "mod" plugin And the following "courses" exist: | fullname | shortname | category | | Test Course 1 | C1 | 0 | | Test Course 2 | C2 | 0 | And the following "users" exist: | username | firstname | lastname | email | | user1 | User | 1 | user1@example.com | And the following "activities" exist: | activity | name | intro | course | idnumber | type | recordings_imported | | bigbluebuttonbn | RoomRecordings | Test Room Recording description | C1 | bigbluebuttonbn1 | 0 | 0 | And the following "mod_bigbluebuttonbn > meeting" exists: | activity | RoomRecordings | And the following "mod_bigbluebuttonbn > recordings" exist: | bigbluebuttonbn | name | description | status | | RoomRecordings | Recording 1 | Description 1 | 2 | | RoomRecordings | Recording 2 | Description 2 | 3 | | RoomRecordings | Recording 3 | Description 3 | 0 | | RoomRecordings | Recording 4 | Description 4 | 1 | @javascript Scenario: I can see the recordings related to an activity Given I am on the "RoomRecordings" "bigbluebuttonbn activity" page logged in as admin And "Recording 1" "table_row" should exist And "Recording 2" "table_row" should exist And "Recording 3" "table_row" should not exist And "Recording 4" "table_row" should not exist # Recording 3 will be fetched and metadata will be present so, we will see it. When the BigBlueButtonBN server has sent recording ready notifications And I run the scheduled task "mod_bigbluebuttonbn\task\check_pending_recordings" And I reload the page Then "Recording 1" "table_row" should exist And "Recording 2" "table_row" should exist And "Recording 3" "table_row" should exist And "Recording 4" "table_row" should not exist @javascript Scenario: I can rename the recording Given I am on the "RoomRecordings" "bigbluebuttonbn activity" page logged in as admin When I set the field "Edit name" in the "Recording 1" "table_row" to "Recording with an updated name 1" Then I should see "Recording with an updated name 1" And I should see "Recording 2" And I reload the page And I should see "Recording with an updated name 1" And I should see "Recording 2" @javascript Scenario: I can set a new description for this recording Given I am on the "RoomRecordings" "bigbluebuttonbn activity" page logged in as admin When I set the field "Edit description" in the "Recording 1" "table_row" to "This is a new recording description 1" Then I should see "This is a new recording description 1" And I should see "Description 2" in the "Recording 2" "table_row" And I reload the page And I should see "This is a new recording description 1" in the "Recording 1" "table_row" And I should see "Description 2" in the "Recording 2" "table_row" @javascript Scenario: I can delete a recording Given I am on the "RoomRecordings" "bigbluebuttonbn activity" page logged in as admin And I change window size to "large" When I click on "a[data-action='delete']" "css_element" in the "Recording 1" "table_row" And I click on "OK" "button" in the "Confirm" "dialogue" Then I should not see "Recording 1" And I should see "Recording 2" And I reload the page And I should not see "Recording 1" And I should see "Recording 2" PK\J@ )tests/behat/join_meeting_as_guest.featurenu[@mod @mod_bigbluebuttonbn @javascript Feature: Test the ability to run the full meeting lifecycle (start to end) for guest users Background: Given a BigBlueButton mock server is configured And I enable "bigbluebuttonbn" "mod" plugin And the following config values are set as admin: | bigbluebuttonbn_guestaccess_enabled | 1 | And the following course exists: | name | Test course | | shortname | C1 | And the following "users" exist: | username | firstname | lastname | email | | traverst | Terry | Travers | t.travers@example.com | | teacher | Teacher | Teacher | t.eacher@example.com | And the following "course enrolments" exist: | user | course | role | | traverst | C1 | student | | teacher | C1 | editingteacher | And the following "activity" exists: | course | C1 | | activity | bigbluebuttonbn | | name | Room recordings | | idnumber | Room recordings | | moderators | role:editingteacher | | wait | 0 | | guestallowed | 1 | Scenario: Student users should be able to see the guest user information When I am on the "Room recordings" Activity page logged in as traverst Then I should not see "Add guests" Scenario: Teacher users should be able to see the guest user information When I am on the "Room recordings" Activity page logged in as teacher And I should see "Add guests" Then I click on "Add guests" "button" And I should see "Add guests to this meeting" in the ".modal-dialog" "css_element" Scenario: Guest users should be able to join a meeting as guest when the meeting is running. When I am on the "Room recordings" Activity page logged in as traverst And "Join session" "link" should exist And I click on "Join session" "link" And I switch to the main window And I log out And I close all opened windows And I am on the "Room recordings" "mod_bigbluebuttonbn > BigblueButtonBN Guest" page Then I should see "Guest username" And I should see "Password" And I set the field "username" to "Test Guest User" And I click on "Join meeting" "button" And I should see "Test Guest User" And I click on "Leave Meeting" "link" And I should see "C1: Room recordings" PK\/tests/behat/end_meeting.featurenu[@mod @mod_bigbluebuttonbn Feature: Test the ability to end a meeting In order to ensure that end meeting capabilities are respected As a teacher I need to control who can end a meeting Background: Given a BigBlueButton mock server is configured And I enable "bigbluebuttonbn" "mod" plugin Scenario Outline: Only a BigBlueButton moderator can end a session Given the following course exists: | name | Test course | | shortname | C1 | And the following "users" exist: | username | firstname | lastname | email | | traverst | Terry | Travers | t.travers@example.com | And the following "course enrolments" exist: | user | course | role | | traverst | C1 | | And the following "activity" exists: | course | C1 | | activity | bigbluebuttonbn | | name | Room recordings | | idnumber | Room recordings | | moderators | | And the following "mod_bigbluebuttonbn > meeting" exists: | activity | Room recordings | When I am on the "Room recordings" Activity page logged in as traverst Then "End session" "link" exist Examples: # Note: If the teacher is not listed as a moderator in the activity roles, then will not have permission to end the # session. | moderators | role | existence | | | editingteacher | should not | | | teacher | should not | | | student | should not | | role:teacher | student | should not | | role:teacher | teacher | should | | role:student | student | should | | user:traverst | student | should | Scenario: An administrator can always end a meeting Given the following course exists: | name | Test course | | shortname | C1 | And the following "activity" exists: | course | C1 | | activity | bigbluebuttonbn | | name | Room recordings | | idnumber | Room recordings | And the following "mod_bigbluebuttonbn > meeting" exists: | activity | Room recordings | When I am on the "Room recordings" Activity page logged in as admin Then "End session" "link" should exist Scenario Outline: Only a BigBlueButton moderator can end a session from the index page Given the following course exists: | name | Test course | | shortname | C1 | And the following "users" exist: | username | firstname | lastname | email | | traverst | Terry | Travers | t.travers@example.com | And the following "course enrolments" exist: | user | course | role | | traverst | C1 | | And the following "activity" exists: | course | C1 | | activity | bigbluebuttonbn | | name | Room recordings | | idnumber | Room recordings | | moderators | | And the following "mod_bigbluebuttonbn > meeting" exists: | activity | Room recordings | When I am on the C1 "mod_bigbluebuttonbn > Index" page logged in as traverst Then "End session" "link" exist Examples: # Note: If the teacher is not listed as a moderator in the activity roles, then will not have permission to end the # session. | moderators | role | existence | | | editingteacher | should not | | | teacher | should not | | | student | should not | | role:teacher | student | should not | | role:teacher | teacher | should | | role:student | student | should | | user:traverst | student | should | Scenario: An administrator can always end a meeting on the index page Given the following course exists: | name | Test course | | shortname | C1 | And the following "activity" exists: | course | C1 | | activity | bigbluebuttonbn | | name | Room recordings | | idnumber | Room recordings | And the following "mod_bigbluebuttonbn > meeting" exists: | activity | Room recordings | When I am on the C1 "mod_bigbluebuttonbn > Index" page logged in as admin Then "End session" "link" should exist PK\7`tests/behat/group_mode.featurenu[@mod @mod_bigbluebuttonbn @course Feature: Test the module in group mode. Background: # groupmode 1 = separate groups, we force the group # groupmode 2 = visible group Given a BigBlueButton mock server is configured And I enable "bigbluebuttonbn" "mod" plugin And the following "courses" exist: | fullname | shortname | category | groupmode | groupmodeforce | | Test Course 1 | C1 | 0 | 1 | 1 | | Test Course 2 | C2 | 0 | 2 | 1 | And the following "groups" exist: | name | course | idnumber | participation | | Group 1 | C1 | G1 | 1 | | Group 2 | C1 | G2 | 1 | | Group 3 | C1 | G3 | 0 | | Group 1 | C2 | G1b | 1 | | Group 2 | C2 | G2b | 1 | | Group 3 | C2 | G3b | 0 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | TeacherG1 | 1 | teacher1@example.com | | user1 | User1G1 | 1 | user1@example.com | | user2 | User2G2 | 2 | user2@example.com | | user3 | User3NoGgroup | 3 | user3@example.com | | user4 | User4NPGgroup | 4 | user4@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | user1 | C1 | student | | user2 | C1 | student | | user3 | C1 | student | | user4 | C1 | student | | teacher1 | C2 | editingteacher | | user1 | C2 | student | | user2 | C2 | student | | user3 | C2 | student | | user4 | C2 | student | And the following "group members" exist: | user | group | | teacher1 | G1 | | user1 | G1 | | user2 | G2 | | user4 | G3 | | teacher1 | G1b | | user1 | G1b | | user2 | G2b | | user4 | G3b | And the following "activities" exist: | activity | name | intro | course | idnumber | type | recordings_imported | | bigbluebuttonbn | RoomRecordings | Test Room Recording description | C1 | bigbluebuttonbn1 | 0 | 0 | | bigbluebuttonbn | RoomRecordingsVisibleGroups | Test Room Recording with visible groups | C2 | bigbluebuttonbn2 | 0 | 0 | And the following "mod_bigbluebuttonbn > meeting" exists: | activity | RoomRecordings | | activity | RoomRecordingsVisibleGroups | And the following "mod_bigbluebuttonbn > meetings" exist: | activity | group | | RoomRecordings | G1 | | RoomRecordings | G2 | | RoomRecordings | G3 | | RoomRecordingsVisibleGroups | G1b | | RoomRecordingsVisibleGroups | G2b | | RoomRecordings | G3b | And the following "mod_bigbluebuttonbn > recordings" exist: | bigbluebuttonbn | name | group | | RoomRecordings | Recording G1 | G1 | | RoomRecordings | Recording G2 | G2 | | RoomRecordings | Recording G3 | G3 | | RoomRecordingsVisibleGroups | Recording G1b | G1b | | RoomRecordingsVisibleGroups | Recording G2b | G2b | | RoomRecordings | Recording G3b | G3b | And the following "mod_bigbluebuttonbn > recordings" exist: | bigbluebuttonbn | name | | RoomRecordings | Recording No group | | RoomRecordingsVisibleGroups | Recording No group C2 | @javascript Scenario: When I create a BBB activity as a teacher who cannot access all groups, I should only be able to select the group I belong on the main bigblue button page. Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/site:accessallgroups | Prevent | editingteacher | Course | C1 | When I am on the "RoomRecordings" "bigbluebuttonbn activity" page logged in as "teacher1" Then I should see "Separate groups: Group 1" @javascript Scenario: When I create a BBB activity as a teacher, I should only be able to specify individual "User" participants with whom I share a group with (or can view on the course participants screen). When I am on the "RoomRecordings" "bigbluebuttonbn activity" page logged in as "teacher1" Then I should see "Group 1" in the "select[name='group']" "css_element" And I should see "Group 2" in the "select[name='group']" "css_element" And I should not see "Group 3" in the "select[name='group']" "css_element" @javascript Scenario Outline: When I view a BBB activity as a student in a course with separate groups, I should only be able to see Recordings from my group When I am on the "RoomRecordings" "bigbluebuttonbn activity" page logged in as "" Then I "Recording G1" And I "Recording G2" And I should not see "Recording G3" And I "Recording No group" Examples: | user | G1 | G2 | NO | | user1 | should see | should not see | should not see | | user2 | should not see | should see | should not see | | user3 | should not see | should not see | should not see | | user4 | should not see | should not see | should not see | @javascript Scenario Outline: When I view a BBB activity as a student in a course with visible group set, I should be able to see Recordings from my group or the default meeting if I am not in a group. When I am on the "RoomRecordingsVisibleGroups" "bigbluebuttonbn activity" page logged in as "user3" And I select "" from the "group" singleselect Then I "Recording G1b" And I "Recording G2b" And I should not see "Recording G3b" And I "Recording No group C2" Examples: | groupname | G1 | G2 | NO | | All participants | should see | should see | should see | | Group 1 | should see | should not see | should not see | | Group 2 | should not see | should see | should not see | @javascript Scenario Outline: When I view a BBB activity as a student in a course with visible group set, I should be able to join meeting if not I should not see the activity When I am on the "" "bigbluebuttonbn activity" page logged in as "" Then I should see "" Examples: | user | Activity | Message | | user1 | RoomRecordings | Join session | | user2 | RoomRecordings | Join session | | user3 | RoomRecordings | You do not have a role that is allowed to join this session | | user4 | RoomRecordings | You do not have a role that is allowed to join this session | | user1 | RoomRecordingsVisibleGroups | Join session | | user2 | RoomRecordingsVisibleGroups | Join session | | user3 | RoomRecordingsVisibleGroups | Join session | | user3 | RoomRecordingsVisibleGroups | Join session | | user4 | RoomRecordings | You do not have a role that is allowed to join this session | PK\` | And the following "activity" exists: | course | C1 | | activity | bigbluebuttonbn | | name | Room recordings | | idnumber | Room recordings | | moderators | role:editingteacher | | wait | | When I am on the "Room recordings" Activity page logged in as traverst Then "Join session" "link" exist Examples: | wait | role | existence | | 0 | student | should | | 0 | teacher | should | | 0 | editingteacher | should | | 1 | student | should not | | 1 | teacher | should not | | 1 | editingteacher | should | Scenario Outline: Users should be able to join a session if a moderator has already joined Given the following course exists: | name | Test course | | shortname | C1 | And the following "users" exist: | username | firstname | lastname | email | | traverst | Terry | Travers | t.travers@example.com | And the following "course enrolments" exist: | user | course | role | | traverst | C1 | | And the following "activity" exists: | course | C1 | | activity | bigbluebuttonbn | | name | Room recordings | | idnumber | Room recordings | | moderators | role:editingteacher | | wait | | And the following "mod_bigbluebuttonbn > meeting" exists: | activity | Room recordings | | moderatorCount | 1 | When I am on the "Room recordings" Activity page logged in as traverst Then "Join session" "link" should exist Examples: | wait | role | | 0 | student | | 0 | editingteacher | | 1 | student | | 1 | editingteacher | Scenario Outline: An administrator should always be allowed to join a meeting Given the following course exists: | name | Test course | | shortname | C1 | And the following "activity" exists: | course | C1 | | activity | bigbluebuttonbn | | name | Room recordings | | idnumber | Room recordings | | moderators | role:editingteacher | | wait | | When I am on the "Room recordings" Activity page logged in as admin Then "Join session" "link" should exist Examples: | wait | | 0 | | 1 | Scenario Outline: A student moderator should be able to start a meeting Given the following course exists: | name | Test course | | shortname | C1 | And the following "users" exist: | username | firstname | lastname | email | | traverst | Terry | Travers | t.travers@example.com | And the following "course enrolments" exist: | user | course | role | | traverst | C1 | student | And the following "activity" exists: | course | C1 | | activity | bigbluebuttonbn | | name | Room recordings | | idnumber | Room recordings | | moderators | | | wait | | When I am on the "Room recordings" Activity page logged in as traverst Then "Join session" "link" should exist Examples: | wait | moderators | | 0 | user:traverst | | 1 | user:traverst | | 0 | role:student | | 1 | role:student | PK\=? ? !tests/behat/edit_instance.featurenu[@mod @mod_bigbluebuttonbn @javascript Feature: I can edit a bigbluebutton instance As a user I can edit a BigbluebuttonBN instance Background: Make sure that a course is created Given a BigBlueButton mock server is configured And I enable "bigbluebuttonbn" "mod" plugin And the following config values are set as admin: | bigbluebuttonbn_voicebridge_editable | 1 | And the following "courses" exist: | fullname | shortname | category | enablecompletion | | Test course | Test course | 0 | 1 | Scenario Outline: Add a mod_bigbluebuttonbn instance with Room with recordings Given the following "activities" exist: | activity | course | name | type | | bigbluebuttonbn | Test course | | | When I am on the "" "bigbluebuttonbn activity" page logged in as admin And I click on "Settings" "link" Then the field "Instance type" matches value "" And I "Role assigned during live session" And I expand all fieldsets And I "The session may be recorded." Examples: | activityname | instancetype | shouldseerole | shouldseesession | | Activity with recording | 0 | should see | should see | | Activity only | 1 | should see | should see | | Recordings only | 2 | should not see | should not see | Scenario: When the activity completion are locked, all the completion settings, including the one specific to BigBlueButtonBN are disabled Given the following "activities" exist: | activity | course | name | type | | bigbluebuttonbn | Test course | RoomRecordings | 0 | And the following config values are set as admin: | bigbluebuttonbn_config_experimental_features | 1 | | bigbluebuttonbn_meetingevents_enabled | 1 | And the following "users" exist: | username | firstname | lastname | | student1 | Student1 | 1 | And the following "course enrolments" exist: | user | course | role | | student1 | Test course | student | And I am on the "RoomRecordings" "bigbluebuttonbn activity editing" page logged in as "admin" And I expand all fieldsets And I set the following fields to these values: | Add requirements | 1 | | View the activity | 1 | And I press "Save and return to course" And I log out # Then I visit the page first to make sure that completion settings are locked. And I am on the "RoomRecordings" "bigbluebuttonbn activity" page logged in as "student1" And I log out And I am on the "RoomRecordings" "bigbluebuttonbn activity editing" page logged in as "admin" When I expand all fieldsets Then I should see "Completion options locked" And the "View the activity" "field" should be disabled And the "Require attendance (minutes)" "field" should be disabled And the "Chats" "field" should be disabled PK\\A %tests/behat/view_presentation.featurenu[@mod @mod_bigbluebuttonbn @javascript @_file_upload Feature: Test visibility of presentation on activity page In order to ensure that presentation files are not visible to students when they shouldn't be As a teacher I set the visibility of presentation files in the BigBlueButtonBN activity Background: Given I enable "bigbluebuttonbn" "mod" plugin And the following course exists: | name | Test course | | shortname | C1 | And the following "users" exist: | username | firstname | lastname | email | | traverst | Terry | Travers | t.travers@example.com | | uraverst | Uerry | Uravers | u.uravers@example.com | And the following "course enrolments" exist: | user | course | role | | traverst | C1 | student | | uraverst | C1 | teacher | And the following config values are set as admin: | bigbluebuttonbn_preuploadpresentation_editable | 1 | Scenario Outline: Check that presentation file can only be viewed when teachers allow it Given the following "activity" exists: | course | C1 | | activity | bigbluebuttonbn | | name | Room recordings | | moderators | role:teacher | | showpresentation | | And the following config values are set as admin: | config | value | plugin | | showpresentation_default | | mod_bigbluebuttonbn | | showpresentation_editable | | mod_bigbluebuttonbn | And I am on the "Room recordings" "bigbluebuttonbn activity editing" page logged in as "admin" And I expand all fieldsets And I upload "mod/bigbluebuttonbn/tests/fixtures/bbpresentation.pptx" file to "Select files" filemanager And I press "Save and display" When I am on the "Room recordings" Activity page logged in as Then I "Presentation file" And I "bbpresentation.pptx" Examples: | user | value | showfile_editable | showfile_default | existence | | traverst | 1 | 1 | 1 | should see | | uraverst | 1 | 1 | 1 | should see | | traverst | 1 | 1 | 0 | should see | | uraverst | 1 | 1 | 0 | should see | | traverst | 0 | 0 | 1 | should see | | uraverst | 0 | 0 | 1 | should see | | traverst | 0 | 0 | 0 | should not see | | uraverst | 0 | 0 | 0 | should see | | traverst | 0 | 1 | 1 | should not see | | uraverst | 0 | 1 | 1 | should see | PK\ tests/behat/join_meeting.featurenu[@mod @mod_bigbluebuttonbn @javascript Feature: Test the ability to run the full meeting lifecycle (start to end) I can start a meeting then end it Background: Given a BigBlueButton mock server is configured And I enable "bigbluebuttonbn" "mod" plugin And the following config values are set as admin: | bigbluebuttonbn_userlimit_editable | 1 | And the following course exists: | name | Test course | | shortname | C1 | And the following "users" exist: | username | firstname | lastname | email | | traverst | Terry | Travers | t.travers@example.com | | uraverst | Uerry | Uravers | u.uravers@example.com | | vraverst | Verry | Vravers | v.vravers@example.com | And the following "course enrolments" exist: | user | course | role | | traverst | C1 | student | | uraverst | C1 | student | | vraverst | C1 | student | And the following "activity" exists: | course | C1 | | activity | bigbluebuttonbn | | name | Room recordings | | idnumber | Room recordings | | moderators | role:editingteacher | | wait | 0 | | userlimit | 2 | Scenario: Users should be able to join a meeting then end the meeting for themselves and return to the meeting page to join again. When I am on the "Room recordings" Activity page logged in as traverst And "Join session" "link" should exist And I click on "Join session" "link" And I switch to "bigbluebutton_conference" window And I click on "End Meeting" "link" # Selenium driver does not like the click action to be done before we # automatically close the window so we need to make sure that the window # is closed before. And I close all opened windows And I switch to the main window And I reload the page Then I should see "Room recordings" And I should see "This room is ready. You can join the session now." Scenario: Users can join the meeting until the maximum number of users has been reached When I am on the "Room recordings" Activity page logged in as traverst Then "Join session" "link" should exist And I click on "Join session" "link" And I switch to the main window And I log out And I am on the "Room recordings" Activity page logged in as uraverst And "Join session" "link" should exist And I click on "Join session" "link" And I switch to the main window And I log out And I am on the "Room recordings" Activity page logged in as vraverst And "Join session" "link" should not exist And I should see "The limit of how many users allowed in a session has been reached" And I log out And I am on the "Room recordings" Activity page logged in as admin And "Join session" "link" should not exist And I should see "The limit of how many users allowed in a session has been reached" PK\2   tests/behat/guest_access.featurenu[@mod @mod_bigbluebuttonbn Feature: Guest access allows external users to connect to a meeting Background: Given a BigBlueButton mock server is configured And I enable "bigbluebuttonbn" "mod" plugin And the following "courses" exist: | fullname | shortname | category | | Test Course 1 | C1 | 0 | And the following "activities" exist: | activity | name | intro | course | idnumber | type | recordings_imported | guestallowed | | bigbluebuttonbn | RoomRecordings | Test Room Recording description | C1 | bigbluebuttonbn1 | 0 | 0 | 0 | | bigbluebuttonbn | RoomRecordingsWithGuest | Test Room with guest | C1 | bigbluebuttonbn1 | 0 | 0 | 1 | @javascript Scenario Outline: I need to enable guest access to see the instance parameters Given the following config values are set as admin: | bigbluebuttonbn_guestaccess_enabled | | When I am on the "RoomRecordings" "bigbluebuttonbn activity editing" page logged in as "admin" Then I "Guest access" And I log out Examples: | guestaccess | result | | 1 | should see | | 0 | should not see | @javascript Scenario Outline: I should see Guest settings on the module form Given the following config values are set as admin: | bigbluebuttonbn_guestaccess_enabled | 1 | When I am on the "RoomRecordings" "bigbluebuttonbn activity editing" page logged in as "admin" And I should see "Guest access" And I click on "Expand all" "link" in the "region-main" "region" And I should see "Allow guest access" And I set the field "Allow guest access" to Then I "Guests joining must be admitted by a moderator" And I "Meeting link" And I "Meeting password" And I "Copy link" And I "Copy password" And I log out Examples: | guestaccess | result | seelink | seepassword | seecopylink | seecopypw | | "1" | should see | should see | should see | should see | should see | | "0" | should not see | should not see | should not see | should not see | should not see | @javascript Scenario: I should be able to use the guest link and see the popup dialog Given the following config values are set as admin: | bigbluebuttonbn_guestaccess_enabled | 1 | When I am on the "RoomRecordingsWithGuest" "bigbluebuttonbn activity" page logged in as "admin" Then I click on "Add guests" "button" And I should see "Meeting link" And I should see "Meeting password" And I should see "Copy link" And I should see "Copy password" @javascript Scenario: I should see errors when submitting the guest access form with erroneous values Given the following config values are set as admin: | bigbluebuttonbn_guestaccess_enabled | 1 | And I am on the "RoomRecordingsWithGuest" "bigbluebuttonbn activity" page logged in as "admin" And I click on "Add guests" "button" And I set the field "Guest emails" to "123" When I click on "OK" "button" in the "Add guests to this meeting" "dialogue" Then I should see "Invalid email: 123" @javascript Scenario: I should be able to invite guest to the meeting Given the following config values are set as admin: | bigbluebuttonbn_guestaccess_enabled | 1 | When I am on the "RoomRecordingsWithGuest" "bigbluebuttonbn activity" page logged in as "admin" And I click on "Add guests" "button" And I set the field "Guest emails" to "testuser@email.com" And I click on "OK" "button" in the "Add guests to this meeting" "dialogue" Then I should see "An invitation will be sent to testuser@email.com." And I log out Scenario: I should be able to invite guest to the meeting even if forcelogin is set Given the following config values are set as admin: | bigbluebuttonbn_guestaccess_enabled | 1 | | forcelogin | 1 | When I am on the "RoomRecordingsWithGuest" "mod_bigbluebuttonbn > BigblueButtonBN Guest" page Then I should see "C1: RoomRecordingsWithGuest" PK\JɃ< < 9tests/behat/bigbluebuttonbn_activity_availability.featurenu[@mod @mod_bigbluebuttonbn Feature: Manage BigBlueButton session timings As a teacher I want to set and manage the open and close times for BigBlueButton sessions So that I can control when students can join the sessions Background: Given a BigBlueButton mock server is configured And I enable "bigbluebuttonbn" "mod" plugin And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | One | teacher1@example.com | | student1 | Student | One | student1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | calendar_upcoming | System | 1 | my-index | side-post | Scenario Outline: Setting and verifying BBB activity open and close times Given the following "activities" exist: | course | activity | name | openingtime | closingtime | | C1 | bigbluebuttonbn | BBB 1 | | | When I am on the "BBB 1" "bigbluebuttonbn activity" page logged in as student1 And "Join session" "link" exist And I should see "Open:" And I should see "%A, %d %B %Y##" And I should see "Close:" And I should see "%A, %d %B %Y##" And I am viewing calendar in "month" view And I see "BBB 1" And I am on site homepage And I follow "Dashboard" And I see "BBB 1" in the "Upcoming events" "block" Examples: | openingtime | closingtime | calendarvisibility | buttonvisibility | upcomingeventvisibility | | ##now +1 minute## | ##now +5 minutes## | should | should not | should | | ##1 hour ago## | ##+2 hours## | should | should | should not | | ##yesterday## | ##yesterday +3 hours## | should not | should not | should not | PK\,!G%tests/behat/recordings_import.featurenu[@mod @mod_bigbluebuttonbn @core_form @course Feature: Manage and list recordings As a user I am able to import existing recording into another bigbluebutton activity Background: Make sure that import recording is enabled and course, activities and recording exists Given a BigBlueButton mock server is configured And the following config values are set as admin: | bigbluebuttonbn_importrecordings_enabled | 1 | | bigbluebuttonbn_importrecordings_from_deleted_enabled | 1 | And I enable "bigbluebuttonbn" "mod" plugin And the following "courses" exist: | fullname | shortname | category | | Test Course 1 | C1 | 0 | | Test Course 2 | C2 | 0 | And the following "users" exist: | username | firstname | lastname | email | | user1 | User | 1 | user1@example.com | And the following "activities" exist: | activity | name | intro | course | idnumber | type | recordings_imported | | bigbluebuttonbn | RoomRecordings | Test Room Recording description | C1 | bigbluebuttonbn1 | 0 | 0 | | bigbluebuttonbn | RoomRecordings1 | Test Recordings description 1 | C2 | bigbluebuttonbn2 | 0 | 1 | | bigbluebuttonbn | RecordingOnly | Test Recordings only description 1 | C2 | bigbluebuttonbn3 | 2 | 1 | And the following "mod_bigbluebuttonbn > meeting" exists: | activity | RoomRecordings | And the following "mod_bigbluebuttonbn > recordings" exist: | bigbluebuttonbn | name | status | | RoomRecordings | Recording 1 | 3 | | RoomRecordings | Recording 2 | 3 | @javascript Scenario: I check we display the right information (Recording Name as name and Description) When I am on the "RoomRecordings1" "bigbluebuttonbn activity" page logged in as "admin" # We check column names regarding changes made in CONTRIB-7703. Then I should not see "Recording" in the ".mod_bigbluebuttonbn_recordings_table thead" "css_element" And I should not see "Meeting" in the ".mod_bigbluebuttonbn_recordings_table thead" "css_element" And I should see "Name" in the ".mod_bigbluebuttonbn_recordings_table thead" "css_element" @javascript Scenario Outline: I check that I can import recordings into the Recording activity from other activities When I am on the "" "bigbluebuttonbn activity" page logged in as "admin" And I click on "Import recording links" "button" And I select "Test Course 1" from the "sourcecourseid" singleselect And I select "RoomRecordings" from the "sourcebn" singleselect # add the first recording And I click on "a.action-icon" "css_element" in the "Recording 1" "table_row" # add the second recording And I click on "a.action-icon" "css_element" in the "Recording 2" "table_row" And I click on "Go back" "button" Then "Recording 1" "table_row" And "Recording 2" "table_row" Examples: | instancename | existence | | RecordingOnly | should exist | | RoomRecordings1 | should exist | @javascript Scenario: I check that I can import recordings into the Recording activity and then if I delete them they are back into the pool to be imported again When I am on the "RoomRecordings1" "bigbluebuttonbn activity" page logged in as "admin" And I change window size to "large" And I click on "Import recording links" "button" And I select "Test Course 1" from the "sourcecourseid" singleselect And I select "RoomRecordings" from the "sourcebn" singleselect # add the first recording And I click on "a.action-icon" "css_element" in the "Recording 1" "table_row" # add the second recording And I click on "a.action-icon" "css_element" in the "Recording 2" "table_row" And I wait until the page is ready And I click on "Go back" "button" # This should be refactored with the right classes for the table element # We use javascript here to create the table so we don't get the same structure. Then "Recording 1" "table_row" should exist And I click on "a[data-action='delete']" "css_element" in the "Recording 1" "table_row" And I click on "OK" "button" in the "Confirm" "dialogue" # There is no confirmation dialog when deleting an imported record. And I wait until the page is ready But I should not see "Recording 1" And I click on "Import recording links" "button" And I select "Test Course 1" from the "sourcecourseid" singleselect And I select "RoomRecordings" from the "sourcebn" singleselect And I should see "Recording 1" @javascript @runonly Scenario: I check that I can import recordings from a deleted instance into the Recording activity and then if I delete them they are back into the pool to be imported again Given I log in as "admin" When I am on "Test Course 1" course homepage with editing mode on And I delete "RoomRecordings" activity # The activity is deleted asynchroneously. And I run all adhoc tasks And I am on the "RoomRecordings1" "bigbluebuttonbn activity" page logged in as "admin" And I click on "Import recording links" "button" And I select "Recordings from deleted activities" from the "sourcecourseid" singleselect Then I should see "Recording 1" And I should see "Recording 2" # add the first recording And I click on "a.action-icon" "css_element" in the "Recording 1" "table_row" # add the second recording And I click on "a.action-icon" "css_element" in the "Recording 2" "table_row" And I wait until the page is ready And I click on "Go back" "button" # This should be refactored with the right classes for the table element # We use javascript here to create the table so we don't get the same structure. And "Recording 1" "table_row" should exist And I click on "a[data-action='delete']" "css_element" in the "Recording 1" "table_row" And I click on "OK" "button" in the "Confirm" "dialogue" # There is no confirmation dialog when deleting an imported record. And I wait until the page is ready But I should not see "Recording 1" # Change window size to large to avoid the "Import recording links" button being hidden (random failure). And I change window size to "large" And I click on "Import recording links" "button" And I select "Recordings from deleted activities" from the "sourcecourseid" singleselect And I should see "Recording 1" But I should not see "Recording 2" Scenario: I check that when I disable Import recording feature the import recording link button should not be shown Given I log in as "admin" And the following config values are set as admin: | bigbluebuttonbn_importrecordings_enabled | 0 | When I am on the "RoomRecordings1" "bigbluebuttonbn activity" page logged in as "admin" Then I should not see "Import recording links" PK\}14Stests/behat/subplugins.featurenu[@mod @mod_bigbluebuttonbn @with_bbbext_simple Feature: BigBlueButtonBN Subplugins test As a BigBlueButtonBN user I can list the subplugins the admin settings pages I can see the additional settings coming from the subplugins in the edit form Background: Make sure that the BigBlueButtonBN plugin is enabled Given I enable "bigbluebuttonbn" "mod" plugin And the following "courses" exist: | fullname | shortname | category | enablecompletion | | Test course | Test course | 0 | 1 | And the following "activities" exist: | activity | course | name | type | | bigbluebuttonbn | Test course | BBB Instance name | 0 | And the following config values are set as admin: | enableasyncbackup | 0 | Scenario: Add a subplugin and check that the settings are available Given I log in as "admin" When I navigate to "Plugins > Activity modules > BigBlueButton > Manage BigBlueButton extension plugins" in site administration Then I should see "Simple" Scenario: I check that new fields are available and editable in the instance edit form Given I am on the "BBB Instance name" "bigbluebuttonbn activity editing" page logged in as "admin" When I expand all fieldsets Then I should see "New field" @javascript Scenario: I check that new fields are available and when I edit the field the value is saved Given I am on the "BBB Instance name" "bigbluebuttonbn activity editing" page logged in as "admin" And I expand all fieldsets And I press "Save and display" And I expand all fieldsets And I should see "New field cannot be empty" And I set the field "New field" to "50" And I press "Save and display" And I am on the "BBB Instance name" "bigbluebuttonbn activity editing" page When I expand all fieldsets Then the following fields match these values: | New field | 50 | Scenario: I check that new fields are not available when subplugin is disabled Given I log in as "admin" And I navigate to "Plugins > Activity modules > BigBlueButton > Manage BigBlueButton extension plugins" in site administration And I click on "Disable" "link" And I am on the "BBB Instance name" "bigbluebuttonbn activity editing" page When I expand all fieldsets Then I should not see "New field" @javascript Scenario: I check that custom completion with subplugin works Given a BigBlueButton mock server is configured And the following config values are set as admin: | bigbluebuttonbn_meetingevents_enabled | 1 | And the following "users" exist: | username | firstname | lastname | email | | traverst | Terry | Travers | t.travers@example.com | And the following "course enrolments" exist: | user | course | role | | traverst | Test course | student | And I am on the "BBB Instance name" "bigbluebuttonbn activity editing" page logged in as "admin" And I expand all fieldsets And I set the following fields to these values: | Add requirements | 1 | | Raise hand twice | 1 | And I set the field "New field" to "50" And I press "Save and display" # We start the meeting here so to make sure that meta_analytics-callback-url is set. And the following "mod_bigbluebuttonbn > meeting" exists: | activity | BBB Instance name | And I log out And I am on the "BBB Instance name" "bigbluebuttonbn activity" page logged in as "traverst" And I click on "Join session" "link" And I switch to "bigbluebutton_conference" window And I wait until the page is ready And I follow "End Meeting" And the BigBlueButtonBN server has received the following events from user "traverst": | instancename | eventtype | eventdata | | BBB Instance name | raisehand | 1 | | BBB Instance name | raisehand | 1 | # Selenium driver does not like the click action to be done before we # automatically close the window so we need to make sure that the window # is closed before. And I close all opened windows And I switch to the main window And the BigBlueButtonBN activity "BBB Instance name" has sent recording all its events And I run all adhoc tasks When I reload the page Then I should see "Done: Raise hand twice in a meeting." @javascript Scenario: I check that custom events are triggered and sent to subplugin when enabled Given a BigBlueButton mock server is configured And the following config values are set as admin: | bigbluebuttonbn_meetingevents_enabled | 1 | And the following "users" exist: | username | firstname | lastname | email | | traverst | Terry | Travers | t.travers@example.com | And the following "course enrolments" exist: | user | course | role | | traverst | Test course | student | And the following "mod_bigbluebuttonbn > meeting" exists: | activity | BBB Instance name | And I log out And I am on the "BBB Instance name" "bigbluebuttonbn activity" page logged in as "traverst" And I click on "Join session" "link" And I switch to "bigbluebutton_conference" window And I wait until the page is ready And I follow "End Meeting" And the BigBlueButtonBN server has received the following events from user "traverst": | instancename | eventtype | eventdata | | BBB Instance name | chats | 1 | # Selenium driver does not like the click action to be done before we # automatically close the window so we need to make sure that the window # is closed before. And I close all opened windows And I switch to the main window And the BigBlueButtonBN activity "BBB Instance name" has sent recording all its events And I log out And I log in as "admin" And I run all adhoc tasks When I am on fixture page "/mod/bigbluebuttonbn/tests/behat/fixtures/show_simpleplugin_values.php" Then I should see "(BBB Instance name): meetingevents: 1" PK\F3')))tests/behat/behat_mod_bigbluebuttonbn.phpnu[. /** * Behat custom steps and configuration for mod_bigbluebuttonbn. * * @package mod_bigbluebuttonbn * @category test * @copyright 2021 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); use Behat\Behat\Hook\Scope\BeforeScenarioScope; use Behat\Gherkin\Node\TableNode; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\local\config; use mod_bigbluebuttonbn\test\subplugins_test_helper_trait; use Moodle\BehatExtension\Exception\SkippedException; require_once(__DIR__ . '../../../classes/test/subplugins_test_helper_trait.php'); /** * Behat custom steps and configuration for mod_bigbluebuttonbn. * * @package mod_bigbluebuttonbn * @copyright 2021 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_mod_bigbluebuttonbn extends behat_base { use subplugins_test_helper_trait; /** * @var array List of installed subplugins. */ protected $installedsubplugins = []; /** * BeforeScenario hook to reset the remote testpoint. * * @BeforeScenario @mod_bigbluebuttonbn * * @param BeforeScenarioScope $scope */ public function before_scenario(BeforeScenarioScope $scope) { if (defined('TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER')) { $this->send_mock_request('backoffice/reset'); } // Fields are empty by default which causes tests to fail. set_config('bigbluebuttonbn_server_url', config::DEFAULT_SERVER_URL); set_config('bigbluebuttonbn_shared_secret', config::DEFAULT_SHARED_SECRET); } /** * Check that the TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER is defined, so we can connect to the mock server. * * @Given /^a BigBlueButton mock server is configured$/ */ public function mock_is_configured(): void { if (!defined('TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER')) { throw new SkippedException( 'The TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER constant must be defined to run mod_bigbluebuttonbn tests' ); } set_config('bigbluebuttonbn_server_url', TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER); } /** * Return the list of exact named selectors. * * @return array */ public static function get_exact_named_selectors(): array { return [ new behat_component_named_selector('Meeting field', [ <<get_mocked_server_url($endpoint, $params); $curl = new \curl(); $curl->get($url->out_omit_querystring(), $url->params()); } /** * Convert page names to URLs for steps like 'When I am on the "[page name]" page'. * * Recognised page names are: * | None so far! | | * * @param string $page name of the page, with the component name removed e.g. 'Admin notification'. * @return moodle_url the corresponding URL. * @throws Exception with a meaningful error message if the specified page cannot be found. */ protected function resolve_page_url(string $page): moodle_url { throw new Exception("Unrecognised page type '{$page}'."); } /** * Convert page names to URLs for steps like 'When I am on the "[identifier]" "[page type]" page'. * * Recognised page names are: * | pagetype | name meaning | description | * | Index | BBB Course Index | The bbb index page (index.php) | * * @param string $type identifies which type of page this is, e.g. 'Indez'. * @param string $identifier identifies the particular page, e.g. 'Mathematics 101'. * @return moodle_url the corresponding URL. * @throws Exception with a meaningful error message if the specified page cannot be found. */ protected function resolve_page_instance_url(string $type, string $identifier): moodle_url { switch ($type) { case 'Index': $this->get_course_id($identifier); return new moodle_url('/mod/bigbluebuttonbn/index.php', [ 'id' => $this->get_course_id($identifier), ]); case 'BigblueButtonBN Guest': $cm = $this->get_cm_by_activity_name('bigbluebuttonbn', $identifier); $instance = instance::get_from_cmid($cm->id); $url = $instance->get_guest_access_url(); // We have to make sure we set the password. It makes it then easy to submit the form with the right password. $url->param('password', $instance->get_guest_access_password()); return $url; default: throw new Exception("Unrecognised page type '{$type}'."); } } /** * Get course id from its identifier (shortname or fullname or idnumber) * * @param string $identifier * @return int */ protected function get_course_id(string $identifier): int { global $DB; return $DB->get_field_select( 'course', 'id', "shortname = :shortname OR fullname = :fullname OR idnumber = :idnumber", [ 'shortname' => $identifier, 'fullname' => $identifier, 'idnumber' => $identifier, ], MUST_EXIST ); } /** * Trigger a recording ready notification on BBB side * * @Given the BigBlueButtonBN server has sent recording ready notifications */ public function trigger_recording_ready_notification(): void { $this->send_mock_request('backoffice/sendRecordingReadyNotifications', [ 'secret' => \mod_bigbluebuttonbn\local\config::DEFAULT_SHARED_SECRET, ] ); } /** * Trigger a meeting event on BBB side * * @Given /^the BigBlueButtonBN server has received the following events from user "(?P(?:[^"]|\\")*)":$/ * @param string $username * @param TableNode $data */ public function trigger_meeting_event(string $username, TableNode $data): void { global $DB; $user = core_user::get_user_by_username($username); $rows = $data->getHash(); foreach ($rows as $elementdata) { $instanceid = $DB->get_field('bigbluebuttonbn', 'id', [ 'name' => $elementdata['instancename'], ]); $instance = \mod_bigbluebuttonbn\instance::get_from_instanceid($instanceid); $this->send_mock_request('backoffice/addMeetingEvent', [ 'secret' => \mod_bigbluebuttonbn\local\config::DEFAULT_SHARED_SECRET, 'meetingID' => $instance->get_meeting_id(), 'attendeeID' => $user->id, 'attendeeName' => fullname($user), 'eventType' => $elementdata['eventtype'], 'eventData' => $elementdata['eventdata'] ?? '', ] ); } } /** * Send all events received for this meeting back to moodle * * @Given /^the BigBlueButtonBN activity "(?P(?:[^"]|\\")*)" has sent recording all its events$/ * @param string $instancename */ public function trigger_all_events(string $instancename): void { global $DB; $instanceid = $DB->get_field('bigbluebuttonbn', 'id', [ 'name' => $instancename, ]); $instance = \mod_bigbluebuttonbn\instance::get_from_instanceid($instanceid); $this->send_mock_request('backoffice/sendAllEvents', [ 'meetingID' => $instance->get_meeting_id(), 'sendQuery' => true ] ); } /** * Install the simple subplugin * * Important note here. Originally we had a step that was installing the plugin, however * because of race condition (mainly javascript calls), the hack to the core_component was * randomly lost due to the component cache being cleared. So we have to install the plugin before * any interaction with the site. * @BeforeScenario @with_bbbext_simple */ public function install_simple_subplugin() { $this->setup_fake_plugin("simple"); $mockedcomponent = new ReflectionClass(core_component::class); $mockedplugintypes = $mockedcomponent->getProperty('plugintypes'); $mockedplugintypes->setValue(null, null); $init = $mockedcomponent->getMethod('init'); $init->invoke(null); // I enable the plugin. $manager = core_plugin_manager::resolve_plugininfo_class(\mod_bigbluebuttonbn\extension::BBB_EXTENSION_PLUGIN_NAME); $manager::enable_plugin("simple", true); } /** * Uninstall the simple subplugin * * @AfterScenario @with_bbbext_simple */ public function uninstall_simple_subplugin() { $this->uninstall_fake_plugin("simple"); } } PK\7E E tests/behat/add_instance.featurenu[@mod @mod_bigbluebuttonbn @javascript Feature: bigbluebuttonbn instance In order to create a room activity with recordings As a user I need to add three room activities to an existent course Background: Make sure that a course is created Given a BigBlueButton mock server is configured And I enable "bigbluebuttonbn" "mod" plugin And the following "courses" exist: | fullname | shortname | category | | Test course | Test course | 0 | And the following "activities" exist: | activity | course | name | type | | bigbluebuttonbn | Test course | BBB Instance name | 0 | | bigbluebuttonbn | Test course | BBB Instance name 2 | 1 | | bigbluebuttonbn | Test course | BBB Instance name 3 | 2 | And I am on the "Test course" "course" page logged in as "admin" Scenario: Add a mod_bigbluebuttonbn instance with Room with recordings When I am on the "BBB Instance name" "bigbluebuttonbn activity" page Then I should see "This room is ready. You can join the session now." And I should see "Join session" And I should see "Recordings" Scenario: Add a mod_bigbluebuttonbn instance with Room only When I am on the "BBB Instance name 2" "bigbluebuttonbn activity" page Then I should see "This room is ready. You can join the session now." And I should see "Join session" And I should not see "Recordings" Scenario: Add a mod_bigbluebuttonbn instance with Recordings only When I am on the "BBB Instance name 3" "bigbluebuttonbn activity" page Then I should not see "This room is ready. You can join the session now." And I should not see "Join session" And I should see "Recordings" Scenario: Add a Recording Only activity and check that no live session settings are available for this instance type When I change window size to "large" And I add a bigbluebuttonbn activity to course "Test course" section "1" And I select "Recordings only" from the "Instance type" singleselect Then I should not see "Lock settings" Scenario Outline: Add an activity and check that required settings are available for the three types of instance types When I change window size to "large" And I add a bigbluebuttonbn activity to course "Test course" section "1" And I select "" from the "Instance type" singleselect Then I should see "Restrict access" Examples: | type | | Room with recordings | | Room only | | Recordings only | PK\ !tests/behat/lock_settings.featurenu[@mod @mod_bigbluebuttonbn Feature: Test that the meeting has the right lock setting. In order to ensure that a meeting creator can lock settings in a meeting As a teacher I set up locked feature for a given meeting and this should be reflected in the meeting. Background: Given a BigBlueButton mock server is configured And I enable "bigbluebuttonbn" "mod" plugin @javascript Scenario Outline: Teacher should be able to set the right lock feature in a given meeting Given the following course exists: | name | Test course | | shortname | C1 | And the following "users" exist: | username | firstname | lastname | email | | traverst | Terry | Travers | t.travers@example.com | And the following "course enrolments" exist: | user | course | role | | traverst | C1 | editingteacher | And the following "activity" exists: | course | C1 | | activity | bigbluebuttonbn | | name | Room recordings | | idnumber | Room recordings | | | | When I am on the "Room recordings" Activity page logged in as traverst And I click on "Join session" "link" And I switch to "bigbluebutton_conference" window Then I should see "" in the "lockSettings" "mod_bigbluebuttonbn > Meeting field" Examples: | locksettingname | value | bbbsettingvalue | | disablecam | 1 | disableCam : enabled | | disablemic | 1 | disableMic : enabled | | disableprivatechat | 1 | disablePrivateChat : enabled | | disablepublicchat | 1 | disablePublicChat : enabled | | disablenote | 1 | disableNotes : enabled | | hideuserlist | 1 | hideUserList : enabled | @javascript Scenario: If any lock Setting is enabled, then the LockOnJoin should be enabled Given the following course exists: | name | Test course | | shortname | C1 | And the following "users" exist: | username | firstname | lastname | email | | traverst | Terry | Travers | t.travers@example.com | And the following "course enrolments" exist: | user | course | role | | traverst | C1 | editingteacher | And the following "activity" exists: | course | C1 | | activity | bigbluebuttonbn | | name | Room recordings | | idnumber | Room recordings | | disablecam | 1 | When I am on the "Room recordings" Activity page logged in as traverst And I click on "Join session" "link" And I switch to "bigbluebutton_conference" window Then I should see "disableCam : enabled" in the "lockSettings" "mod_bigbluebuttonbn > Meeting field" And I should see "lockOnJoin : enabled" in the "lockSettings" "mod_bigbluebuttonbn > Meeting field" PK\ԐInntests/behat/roles.featurenu[@mod @mod_bigbluebuttonbn @javascript Feature: Set role as Bigbluebuttonbn moderator In order to set a room moderator As admin I need to see the list of roles Background: Given I enable "bigbluebuttonbn" "mod" plugin And the following "course" exist: | fullname | shortname | | Course 1 | C1 | And the following "activity" exists: | course | C1 | | activity | bigbluebuttonbn | | name | RoomRecordings | Scenario: Set the manager role as room moderator Given I am on the "RoomRecordings" "bigbluebuttonbn activity editing" page logged in as "admin" And I set the field "bigbluebuttonbn_participant_selection_type" to "Role" And I set the field "bigbluebuttonbn_participant_selection" to "Manager" And I click on "bigbluebuttonbn_participant_selection_add" "button" And I set the field "select-for-role-1" to "Moderator" And I press "Save and display" When I am on the "RoomRecordings" "bigbluebuttonbn activity editing" page Then "[name=select-for-role-1] option[value=moderator][selected]" "css_element" should exist PK\&a,tests/behat/room.featurenu[@mod @mod_bigbluebuttonbn @core_form @course Feature: The recording can be managed through the room page and as a user I can interact with the table Background: Make sure that import recording is enabled and course, activities and recording exists Given a BigBlueButton mock server is configured And I enable "bigbluebuttonbn" "mod" plugin And the following "courses" exist: | fullname | shortname | category | | Test Course 1 | C1 | 0 | And the following "users" exist: | username | firstname | lastname | email | | user1 | User | 1 | user1@example.com | And the following "activities" exist: | activity | name | intro | course | idnumber | type | recordings_imported | | bigbluebuttonbn | RoomRecordings | Test Room Recording description | C1 | bigbluebuttonbn1 | 0 | 0 | And the following "mod_bigbluebuttonbn > meeting" exists: | activity | RoomRecordings | And the following "mod_bigbluebuttonbn > recordings" exist: | bigbluebuttonbn | name | description | status | starttime | | RoomRecordings | Recording 1 | Description 1 | 2 | 1619666194 | | RoomRecordings | Recording 2 | Description 2 | 2 | 1639668194 | | RoomRecordings | Recording 3 | Description 3 | 2 | 1629666194 | | RoomRecordings | Recording 4 | Description 4 | 2 | 1649666194 | @javascript Scenario: Recording should be sortable by date Given I am on the "RoomRecordings" "bigbluebuttonbn activity" page logged in as admin When I click on "th[data-yui3-col-id='date'] .yui3-datatable-sort-indicator" "css_element" And "Recording 1" "text" should appear before "Recording 3" "text" And "Recording 3" "text" should appear before "Recording 2" "text" And "Recording 2" "text" should appear before "Recording 4" "text" And I click on "th[data-yui3-col-id='date'] .yui3-datatable-sort-indicator" "css_element" Then "Recording 1" "text" should appear after "Recording 3" "text" And "Recording 3" "text" should appear after "Recording 2" "text" And "Recording 2" "text" should appear after "Recording 4" "text" PK\Y3dd bbb_view.phpnu[. /** * View for BigBlueButton interaction. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) */ use core\notification; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\local\exceptions\server_not_available_exception; use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; use mod_bigbluebuttonbn\logger; use mod_bigbluebuttonbn\meeting; use mod_bigbluebuttonbn\plugin; use mod_bigbluebuttonbn\recording; require_once(dirname(dirname(dirname(__FILE__))) . '/config.php'); global $SESSION, $PAGE, $CFG, $DB, $USER, $OUTPUT; $action = required_param('action', PARAM_TEXT); $id = optional_param('id', 0, PARAM_INT); $bn = optional_param('bn', 0, PARAM_INT); $rid = optional_param('rid', '', PARAM_TEXT); $rtype = optional_param('rtype', 'presentation', PARAM_TEXT); $errors = optional_param('errors', '', PARAM_TEXT); $timeline = optional_param('timeline', 0, PARAM_INT); $index = optional_param('index', 0, PARAM_INT); $group = optional_param('group', -1, PARAM_INT); // Get the bbb instance from either the cmid (id), or the instanceid (bn). if ($id) { $instance = instance::get_from_cmid($id); } else { if ($bn) { $instance = instance::get_from_instanceid($bn); } } if (!$instance) { $courseid = optional_param('courseid', 1, PARAM_INT); \core\notification::error(get_string('general_error_not_found', 'mod_bigbluebuttonbn', $id)); redirect(new moodle_url('/course/view.php', ['id' => $courseid])); } $cm = $instance->get_cm(); $course = $instance->get_course(); $bigbluebuttonbn = $instance->get_instance_data(); $context = $instance->get_context(); require_login($course, true, $cm); // Note : this uses the group optional_param as a value to decide which groupid. $groupid = groups_get_activity_group($cm, true) ?: null; if ($groupid) { $instance->set_group_id($groupid); } // Print the page header. $PAGE->set_context($context); $PAGE->set_url('/mod/bigbluebuttonbn/bbb_view.php', ['id' => $cm->id, 'bigbluebuttonbn' => $bigbluebuttonbn->id]); $PAGE->set_title(format_string($bigbluebuttonbn->name)); $PAGE->set_cacheable(false); $PAGE->set_heading($course->fullname); $PAGE->blocks->show_only_fake_blocks(); switch (strtolower($action)) { case 'logout': if (isset($errors) && $errors != '') { $errors = (array) json_decode(urldecode($errors)); $msgerrors = ''; foreach ($errors as $error) { $msgerrors .= html_writer::tag('p', $error->{'message'}, ['class' => 'alert alert-danger']) . "\n"; } throw new moodle_exception('view_error_bigbluebutton', 'bigbluebuttonbn', $CFG->wwwroot . '/mod/bigbluebuttonbn/view.php?id=' . $id, $msgerrors, $errors); } if (empty($bigbluebuttonbn)) { echo get_string('view_message_tab_close', 'bigbluebuttonbn'); die(); } // Moodle event logger: Create an event for meeting left. logger::log_meeting_left_event($instance); // Update the cache. $meeting = new meeting($instance); $meeting->update_cache(); // Check the origin page. $select = "userid = ? AND log = ?"; $params = [ 'userid' => $USER->id, 'log' => logger::EVENT_JOIN, ]; $accesses = $DB->get_records_select('bigbluebuttonbn_logs', $select, $params, 'id ASC', 'id, meta', 1); $lastaccess = end($accesses); if (!empty($lastaccess->meta)) { $lastaccess = json_decode($lastaccess->meta); // If the user acceded from Timeline it should be redirected to the Dashboard. if (isset($lastaccess->origin) && $lastaccess->origin == logger::ORIGIN_TIMELINE) { redirect($CFG->wwwroot . '/my/'); } } break; case 'join': if (empty($bigbluebuttonbn)) { throw new moodle_exception('view_error_unable_join', 'bigbluebuttonbn'); break; } // Check the origin page. $origin = logger::ORIGIN_BASE; if ($timeline) { $origin = logger::ORIGIN_TIMELINE; } else if ($index) { $origin = logger::ORIGIN_INDEX; } try { $url = meeting::join_meeting($instance, $origin); redirect($url); } catch (server_not_available_exception $e) { bigbluebutton_proxy::handle_server_not_available($instance); } // We should never reach this point. break; case 'play': $recordings = $instance->get_recordings(); if (!isset($recordings[$rid])) { notification::add(get_string('recordingnotfound', 'mod_bigbluebuttonbn'), notification::ERROR); redirect($instance->get_view_url()); } $href = $recordings[$rid]->get_remote_playback_url($rtype); if (!$href) { notification::add(get_string('recordingurlnotfound', 'mod_bigbluebuttonbn'), notification::ERROR); redirect($instance->get_view_url()); } logger::log_recording_played_event($instance, $rid); redirect(urldecode($href)); // We should never reach this point. break; } // When we reach this point, we can close the tab or window where BBB was opened. echo $OUTPUT->header(); // Behat does not like when we close the Windows as it is expecting to locate // on click part of the pages (bug with selenium raising an exception). So this is a workaround. if (!defined('BEHAT_SITE_RUNNING')) { $PAGE->requires->js_call_amd('mod_bigbluebuttonbn/rooms', 'setupWindowAutoClose'); } echo $OUTPUT->footer(); PK\ { guest.phpnu[. /** * Guest access implementation * * @package mod_bigbluebuttonbn * @copyright 2022 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Laurent David (laurent [at] call-learning [dt] fr) */ use mod_bigbluebuttonbn\form\guest_login; use mod_bigbluebuttonbn\local\exceptions\server_not_available_exception; use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; use mod_bigbluebuttonbn\meeting; use mod_bigbluebuttonbn\plugin; require(__DIR__.'/../../config.php'); global $PAGE, $OUTPUT, $DB, $SITE; // Note here that we do not use require_login as the $CFG->forcelogin would prevent guest user from accessing this page. $PAGE->set_course($SITE); // Intialise the page and run through the setup. $uid = required_param('uid', PARAM_ALPHANUMEXT); $bbid = $DB->get_field('bigbluebuttonbn', 'id', ['guestlinkuid' => trim($uid)]); if (empty($bbid)) { throw new moodle_exception('guestaccess_activitynotfound', 'mod_bigbluebuttonbn'); } $instance = \mod_bigbluebuttonbn\instance::get_from_instanceid($bbid); // Prevent access to this page if the guest access has been disabled on this instance. if (!$instance->is_guest_allowed()) { throw new moodle_exception('guestaccess_feature_disabled', 'mod_bigbluebuttonbn'); } // Get the guest matching guest access link. $PAGE->set_url('/mod/bigbluebuttonbn/guest.php', ['uid' => $uid]); $title = $instance->get_course()->shortname . ': ' . format_string($instance->get_meeting_name()); $PAGE->set_title($title); $PAGE->set_heading($title); $PAGE->set_pagelayout('standard'); $form = new guest_login(null, ['uid' => $uid, 'instance' => $instance]); // Specific for the tests: we allow to set the password in the form here. if (defined('BEHAT_SITE_RUNNING')) { $form->set_data(['password' => optional_param('password', '', PARAM_RAW)]); } if ($data = $form->get_data()) { $username = $data->username; try { $meeting = new meeting($instance); // As the meeting doesn't exist, we raise an exception. if (!empty($meeting->get_meeting_info()->createtime)) { $url = $meeting->get_guest_join_url($username); redirect($url); } else { \core\notification::add( get_string('guestaccess_meeting_not_started', 'mod_bigbluebuttonbn'), \core\output\notification::NOTIFY_ERROR ); } } catch (server_not_available_exception $e) { bigbluebutton_proxy::handle_server_not_available($instance); } } echo $OUTPUT->header(); echo $form->render(); echo $OUTPUT->footer(); PK\ˠextension/README.mdnu[## BigBlueButtonBN extension subplugins The plugins are made to extend existing BigBlueButtonBN behaviour. For now, we have two extensions points using as base classes that should be implemented. * action_url_addons: inherit this class and redefine the execute method so add new parameter when we send an action url to the BigBlueButton server. * mod_instance_helper : inherit this class so all methods will be called when we either add/delete/or update a module instance. The extension classes should be placed in your plugin with exactly the same name but in a different namespace for example * **\\bbbext_\\bigbluebuttonbn\\mod_instance_helper** to extend hooks from the mod_instance_helper class. Some examples are provided in the tests/fixtures/simple folder. PK\cextension/.gitignorenu[*/PK\ settings.phpnu[. /** * Settings for BigBlueButtonBN. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) */ defined('MOODLE_INTERNAL') || die; $bbbsettings = new mod_bigbluebuttonbn\settings($ADMIN, $module, $section, $hassiteconfig); $bbbsettings->add_all_settings(); $settings = null; PK\= mod_form.phpnu[. /** * Config all BigBlueButtonBN instances in this course. * * @package mod_bigbluebuttonbn * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) */ use mod_bigbluebuttonbn\extension; use mod_bigbluebuttonbn\instance; use mod_bigbluebuttonbn\local\helpers\roles; use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/course/moodleform_mod.php'); /** * Moodle class for mod_form. * * @copyright 2010 onwards, Blindside Networks Inc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_bigbluebuttonbn_mod_form extends moodleform_mod { /** * @var array $formextensions extensions to this form */ protected $formextensions = []; /** * Define (add) particular settings this activity can have. * * @return void * @throws moodle_exception */ public function definition(): void { global $CFG, $DB, $PAGE; $mform = &$this->_form; // Validates if the BigBlueButton server is running. $serverversion = bigbluebutton_proxy::get_server_version(); if (is_null($serverversion)) { throw new moodle_exception('general_error_unable_connect', 'bigbluebuttonbn', $CFG->wwwroot . '/admin/settings.php?section=modsettingbigbluebuttonbn', \mod_bigbluebuttonbn\local\config::get('server_url') ); } // UI configuration options. $cfg = \mod_bigbluebuttonbn\local\config::get_options(); // Get only those that are allowed. $course = $this->_course; $context = context_course::instance($course->id); $bigbluebuttonbn = empty($this->get_current()->id) ? null : $this->get_current(); $this->formextensions = extension::mod_form_addons_instances($mform, $bigbluebuttonbn, $this->get_suffix()); $instancetyperofiles = $this->get_instance_type_profiles(); $this->bigbluebuttonbn_mform_add_block_profiles($mform, $instancetyperofiles); // Data for participant selection. $participantlist = roles::get_participant_list($bigbluebuttonbn, $context); // Add block 'General'. $this->bigbluebuttonbn_mform_add_block_general($mform, $cfg); // Add block 'Room'. $this->bigbluebuttonbn_mform_add_block_room($mform, $cfg); // Add block 'Lock'. $this->bigbluebuttonbn_mform_add_block_locksettings($mform, $cfg); // Add block 'Preuploads'. $this->bigbluebuttonbn_mform_add_block_preuploads($mform, $cfg); // Add block 'Participant List'. $this->bigbluebuttonbn_mform_add_block_user_role_mapping($mform, $participantlist); // Add block 'Guest Access'. $this->bigbluebuttonbn_mform_add_block_guest_access($mform, $cfg, $this->current); // Add block 'Schedule'. $this->bigbluebuttonbn_mform_add_block_schedule($mform, $this->current); // Now add subplugins form elements. $this->add_subplugins_elements(); // Add standard grading elements. $this->standard_grading_coursemodule_elements(); // Add standard elements, common to all modules. $this->standard_coursemodule_elements(); // Add standard buttons, common to all modules. $this->add_action_buttons(); $jsvars = [ 'instanceTypeDefault' => array_keys($instancetyperofiles)[0], ]; // Now add the instance type profiles to the form as a html hidden field. $mform->addElement('html', html_writer::div('', 'd-none', [ 'data-profile-types' => json_encode($instancetyperofiles), 'data-participant-data' => json_encode(roles::get_participant_data($context, $bigbluebuttonbn)), ])); $PAGE->requires->js_call_amd('mod_bigbluebuttonbn/modform', 'init', [$jsvars]); } /** * Get instance type profile. * * @return array|array[] * @throws moodle_exception */ protected function get_instance_type_profiles() { // Add profile data here instead of passing it by parameters. $context = context_course::instance($this->_course->id); $instancetyperofiles = bigbluebutton_proxy::get_instance_type_profiles_create_allowed( has_capability('mod/bigbluebuttonbn:addinstancewithmeeting', $context), has_capability('mod/bigbluebuttonbn:addinstancewithrecording', $context) ); // If none is allowed, fail and return. if (empty($instancetyperofiles)) { global $CFG; // Also check module context for those that are allowed. $contextm = context_module::instance($this->_cm->id); $instancetyperofiles = bigbluebutton_proxy::get_instance_type_profiles_create_allowed( has_capability('mod/bigbluebuttonbn:addinstancewithmeeting', $contextm), has_capability('mod/bigbluebuttonbn:addinstancewithrecording', $contextm) ); // If still none is allowed, fail and return. if (empty($instancetyperofiles)) { throw new moodle_exception('general_error_not_allowed_to_create_instances', 'bigbluebuttonbn', $CFG->wwwroot . '/admin/settings.php?section=modsettingbigbluebuttonbn'); } } return $instancetyperofiles; } /** * Prepare the attachment for being stored. * * @param array|null $defaultvalues * @return void */ public function data_preprocessing(&$defaultvalues) { parent::data_preprocessing($defaultvalues); $suffix = $this->get_suffix(); $completionattendanceenabledel = 'completionattendanceenabled' . $suffix; $completionattendanceel = 'completionattendance' . $suffix; // Completion: tick by default if completion attendance settings is set to 1 or more. $defaultvalues[$completionattendanceenabledel] = 0; if (!empty($defaultvalues[$completionattendanceel])) { $defaultvalues[$completionattendanceenabledel] = 1; } // Check if we are Editing an existing instance. if ($this->current->instance) { // Pre-uploaded presentation: copy existing files into draft area. try { $draftitemid = file_get_submitted_draft_itemid('presentation'); file_prepare_draft_area($draftitemid, $this->context->id, 'mod_bigbluebuttonbn', 'presentation', 0, ['subdirs' => 0, 'maxbytes' => 0, 'maxfiles' => 1, 'mainfile' => true] ); $defaultvalues['presentation'] = $draftitemid; } catch (Exception $e) { debugging('Presentation could not be loaded: ' . $e->getMessage(), DEBUG_DEVELOPER); return; } // Completion: tick if completion attendance settings is set to 1 or more. $defaultvalues[$completionattendanceenabledel] = 0; if (!empty($this->current->{$completionattendanceel})) { $defaultvalues[$completionattendanceenabledel] = 1; } } foreach ($this->formextensions as $formextension) { $formextension->data_preprocessing($defaultvalues); } } /** * Validates the data processed by the form. * * @param mixed $data * @param array $files * @return array */ public function validation($data, $files) { $errors = parent::validation($data, $files); if (isset($data['openingtime']) && isset($data['closingtime'])) { if ($data['openingtime'] != 0 && $data['closingtime'] != 0 && $data['closingtime'] < $data['openingtime']) { $errors['closingtime'] = get_string('bbbduetimeoverstartingtime', 'bigbluebuttonbn'); } } if (isset($data['voicebridge'])) { if (!bigbluebutton_proxy::is_voicebridge_number_unique($data['instance'], $data['voicebridge'])) { $errors['voicebridge'] = get_string('mod_form_field_voicebridge_notunique_error', 'bigbluebuttonbn'); } } $additionalsubpluginerrors = []; foreach ($this->formextensions as $formextension) { $additionalsubpluginerrors = array_merge($additionalsubpluginerrors, $formextension->validation($data, $files)); } return array_merge($errors, $additionalsubpluginerrors); } /** * Add elements for setting the custom completion rules. * * @return array List of added element names, or names of wrapping group elements. * @category completion */ public function add_completion_rules(): array { $mform = $this->_form; if (!(boolean) \mod_bigbluebuttonbn\local\config::get('meetingevents_enabled')) { return []; } $suffix = $this->get_suffix(); // Elements for completion by Attendance. $attendance['rulelabel'] = get_string('completionattendance', 'bigbluebuttonbn'); $completionattendanceenabledel = 'completionattendanceenabled' . $suffix; $completionattendanceel = 'completionattendance' . $suffix; $completionattendanceunitel = 'completionattendanceunit' . $suffix; $attendance['group'] = [ $mform->createElement('advcheckbox', $completionattendanceenabledel, '', $attendance['rulelabel'] . ' '), $mform->createElement('text', $completionattendanceel, 'minutes', ['size' => 3]), ]; $mform->setType($completionattendanceel, PARAM_INT); $completionattendancegroupel = 'completionattendancegroup' . $suffix; $mform->addGroup($attendance['group'], $completionattendancegroupel, '', ' ', false); $completionel = 'completion' . $suffix; $mform->hideIf($completionattendancegroupel, $completionel, 'neq', COMPLETION_AGGREGATION_ANY); $mform->hideIf($completionattendanceel, $completionattendanceenabledel, 'notchecked'); // Elements for completion by Engagement. $engagement['chatlabel'] = get_string('completionengagementchats', 'bigbluebuttonbn'); $engagement['talklabel'] = get_string('completionengagementtalks', 'bigbluebuttonbn'); $engagement['raisehand'] = get_string('completionengagementraisehand', 'bigbluebuttonbn'); $engagement['pollvotes'] = get_string('completionengagementpollvotes', 'bigbluebuttonbn'); $engagement['emojis'] = get_string('completionengagementemojis', 'bigbluebuttonbn'); $completionengagementchatsel = 'completionengagementchats' . $suffix; $completionengagementtalksel = 'completionengagementtalks' . $suffix; $completionengagementraisehandel = 'completionengagementraisehand' . $suffix; $completionengagementpollvotesel = 'completionengagementpollvotes' . $suffix; $completionengagementemojisel = 'completionengagementemojis' . $suffix; $engagement['group'] = [ $mform->createElement('advcheckbox', $completionengagementchatsel, '', $engagement['chatlabel'] . '  '), $mform->createElement('advcheckbox', $completionengagementtalksel, '', $engagement['talklabel'] . '  '), $mform->createElement('advcheckbox', $completionengagementraisehandel, '', $engagement['raisehand'] . '  '), $mform->createElement('advcheckbox', $completionengagementpollvotesel, '', $engagement['pollvotes'] . '  '), $mform->createElement('advcheckbox', $completionengagementemojisel, '', $engagement['emojis'] . '  '), ]; $completionengagementgroupel = 'completionengagementgroup' . $suffix; $mform->addGroup($engagement['group'], $completionengagementgroupel, '', ' ', false); $mform->addGroupRule($completionattendancegroupel, [ $completionattendanceel => [ [null, 'numeric', null, 'client'] ] ]); $mform->hideIf($completionengagementgroupel, $completionel, 'neq', COMPLETION_AGGREGATION_ANY); $completionrules = [$completionattendancegroupel, $completionengagementgroupel]; foreach ($this->formextensions as $formextension) { $completionrules = array_merge($completionrules, $formextension->add_completion_rules()); } return $completionrules; } /** * Called during validation to see whether some module-specific completion rules are selected. * * @param array $data Input data not yet validated. * @return bool True if one or more rules is enabled, false if none are. */ public function completion_rule_enabled($data) { $suffix = $this->get_suffix(); $rules = [ 'completionattendanceenabled', 'completionattendance', 'completionengagementchats', 'completionengagementtalks', 'completionengagementraisehand', 'completionengagementpollvotes', 'completionengagementemojis' ]; $enabled = array_reduce($rules, function($carry, $rule) use ($data, $suffix) { return $carry || !empty($data[$rule . $suffix]); }, false); foreach ($this->formextensions as $formextension) { $enabled = $enabled || $formextension->completion_rule_enabled($data); } return $enabled; } /** * Allows module to modify the data returned by form get_data(). * This method is also called in the bulk activity completion form. * * Only available on moodleform_mod. * * @param stdClass $data the form data to be modified. */ public function data_postprocessing($data) { parent::data_postprocessing($data); // Turn off completion settings if the checkboxes aren't ticked. if (!empty($data->completionunlocked)) { $suffix = $this->get_suffix(); $completion = $data->{'completion' . $suffix}; $autocompletion = !empty($completion) && $completion == COMPLETION_TRACKING_AUTOMATIC; if (empty($data->{'completionattendanceenabled' . $suffix}) || !$autocompletion) { $data->{'completionattendance' . $suffix} = 0; } } foreach ($this->formextensions as $formextension) { $formextension->data_postprocessing($data); } } /** * Function for showing the block for selecting profiles. * * @param MoodleQuickForm $mform * @param array $profiles * @return void */ private function bigbluebuttonbn_mform_add_block_profiles(MoodleQuickForm &$mform, array $profiles): void { if ((boolean) \mod_bigbluebuttonbn\local\config::recordings_enabled()) { $mform->addElement('select', 'type', get_string('mod_form_field_instanceprofiles', 'bigbluebuttonbn'), bigbluebutton_proxy::get_instance_profiles_array($profiles)); $mform->addHelpButton('type', 'mod_form_field_instanceprofiles', 'bigbluebuttonbn'); } } /** * Function for showing the block for general settings. * * @param MoodleQuickForm $mform * @param array $cfg * @return void */ private function bigbluebuttonbn_mform_add_block_general(MoodleQuickForm &$mform, array $cfg): void { global $CFG; $mform->addElement('header', 'general', get_string('mod_form_block_general', 'bigbluebuttonbn')); $mform->addElement('text', 'name', get_string('mod_form_field_name', 'bigbluebuttonbn'), 'maxlength="64" size="32"'); $mform->setType('name', empty($CFG->formatstringstriptags) ? PARAM_CLEANHTML : PARAM_TEXT); $mform->addRule('name', null, 'required', null, 'client'); $this->standard_intro_elements(get_string('mod_form_field_intro', 'bigbluebuttonbn')); $mform->setAdvanced('introeditor'); $mform->setAdvanced('showdescription'); } /** * Function for showing details of the room settings for the room. * * @param MoodleQuickForm $mform * @param array $cfg * @return void */ private function bigbluebuttonbn_mform_add_block_room_room(MoodleQuickForm &$mform, array $cfg): void { $field = ['type' => 'hidden', 'name' => 'welcome', 'data_type' => PARAM_INT, 'description_key' => null]; if ($cfg['welcome_editable']) { $field['type'] = 'textarea'; $field['data_type'] = PARAM_CLEANHTML; $field['description_key'] = 'mod_form_field_welcome'; $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], $field['description_key'], $cfg['welcome_default'], ['wrap' => 'virtual', 'rows' => 5, 'cols' => '60']); } $field = ['type' => 'hidden', 'name' => 'voicebridge', 'data_type' => PARAM_INT, 'description_key' => null]; if ($cfg['voicebridge_editable']) { $field['type'] = 'text'; $field['data_type'] = PARAM_TEXT; $field['description_key'] = 'mod_form_field_voicebridge'; $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], $field['description_key'], 0, ['maxlength' => 4, 'size' => 6], ['message' => get_string('mod_form_field_voicebridge_format_error', 'bigbluebuttonbn'), 'type' => 'numeric', 'rule' => '####', 'validator' => 'server'] ); } else { $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], $field['description_key'], 0, ['maxlength' => 4, 'size' => 6]); } $field = ['type' => 'hidden', 'name' => 'wait', 'data_type' => PARAM_INT, 'description_key' => null]; if ($cfg['waitformoderator_editable']) { $field['type'] = 'checkbox'; $field['description_key'] = 'mod_form_field_wait'; } $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], $field['description_key'], $cfg['waitformoderator_default']); $field = ['type' => 'hidden', 'name' => 'userlimit', 'data_type' => PARAM_INT, 'description_key' => null]; if ($cfg['userlimit_editable']) { $field['type'] = 'text'; $field['data_type'] = PARAM_TEXT; $field['description_key'] = 'mod_form_field_userlimit'; } $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], $field['description_key'], $cfg['userlimit_default']); $field = ['type' => 'hidden', 'name' => 'record', 'data_type' => PARAM_INT, 'description_key' => null]; if ($cfg['recording_editable']) { $field['type'] = 'checkbox'; $field['description_key'] = 'mod_form_field_record'; } $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], $field['description_key'], $cfg['recording_default']); // Record all from start and hide button. $field = ['type' => 'hidden', 'name' => 'recordallfromstart', 'data_type' => PARAM_INT, 'description_key' => null]; if ($cfg['recording_all_from_start_editable']) { $field['type'] = 'checkbox'; $field['description_key'] = 'mod_form_field_recordallfromstart'; } $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], $field['description_key'], $cfg['recording_all_from_start_default']); $field = ['type' => 'hidden', 'name' => 'recordhidebutton', 'data_type' => PARAM_INT, 'description_key' => null]; if ($cfg['recording_hide_button_editable']) { $field['type'] = 'checkbox'; $field['description_key'] = 'mod_form_field_recordhidebutton'; } $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], $field['description_key'], $cfg['recording_hide_button_default']); $mform->disabledIf('recordallfromstart', 'record'); $mform->disabledIf('recordhidebutton', 'record'); $mform->hideIf('recordhidebutton', 'recordallfromstart', 'checked'); // End Record all from start and hide button. $field = ['type' => 'hidden', 'name' => 'muteonstart', 'data_type' => PARAM_INT, 'description_key' => null]; if ($cfg['muteonstart_editable']) { $field['type'] = 'checkbox'; $field['description_key'] = 'mod_form_field_muteonstart'; } $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], $field['description_key'], $cfg['muteonstart_default']); } /** * Function for showing details of the lock settings for the room. * * @param MoodleQuickForm $mform * @param array $cfg * @return void */ private function bigbluebuttonbn_mform_add_block_locksettings(MoodleQuickForm &$mform, array $cfg): void { $mform->addElement('header', 'lock', get_string('mod_form_locksettings', 'bigbluebuttonbn')); $locksettings = false; $field = ['type' => 'hidden', 'name' => 'disablecam', 'data_type' => PARAM_INT, 'description_key' => null]; if ($cfg['disablecam_editable']) { $field['type'] = 'checkbox'; $field['description_key'] = 'mod_form_field_disablecam'; $locksettings = true; } $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], $field['description_key'], $cfg['disablecam_default']); $field = ['type' => 'hidden', 'name' => 'disablemic', 'data_type' => PARAM_INT, 'description_key' => null]; if ($cfg['disablemic_editable']) { $field['type'] = 'checkbox'; $field['description_key'] = 'mod_form_field_disablemic'; $locksettings = true; } $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], $field['description_key'], $cfg['disablemic_default']); $field = ['type' => 'hidden', 'name' => 'disableprivatechat', 'data_type' => PARAM_INT, 'description_key' => null]; if ($cfg['disableprivatechat_editable']) { $field['type'] = 'checkbox'; $field['description_key'] = 'mod_form_field_disableprivatechat'; $locksettings = true; } $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], $field['description_key'], $cfg['disableprivatechat_default']); $field = ['type' => 'hidden', 'name' => 'disablepublicchat', 'data_type' => PARAM_INT, 'description_key' => null]; if ($cfg['disablepublicchat_editable']) { $field['type'] = 'checkbox'; $field['description_key'] = 'mod_form_field_disablepublicchat'; $locksettings = true; } $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], $field['description_key'], $cfg['disablepublicchat_default']); $field = ['type' => 'hidden', 'name' => 'disablenote', 'data_type' => PARAM_INT, 'description_key' => null]; if ($cfg['disablenote_editable']) { $field['type'] = 'checkbox'; $field['description_key'] = 'mod_form_field_disablenote'; $locksettings = true; } $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], $field['description_key'], $cfg['disablenote_default']); $field = ['type' => 'hidden', 'name' => 'hideuserlist', 'data_type' => PARAM_INT, 'description_key' => null]; if ($cfg['hideuserlist_editable']) { $field['type'] = 'checkbox'; $field['description_key'] = 'mod_form_field_hideuserlist'; $locksettings = true; } $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], $field['description_key'], $cfg['hideuserlist_default']); // Output message if no settings. if (!$locksettings) { $field = ['type' => 'static', 'name' => 'no_locksettings', 'defaultvalue' => get_string('mod_form_field_nosettings', 'bigbluebuttonbn')]; $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], null, null, $field['defaultvalue']); } } /** * Function for showing details of the recording settings for the room. * * @param MoodleQuickForm $mform * @param array $cfg * @return void */ private function bigbluebuttonbn_mform_add_block_room_recordings(MoodleQuickForm &$mform, array $cfg): void { $recordingsettings = false; $field = ['type' => 'hidden', 'name' => 'recordings_deleted', 'data_type' => PARAM_INT, 'description_key' => null]; if ($cfg['recordings_deleted_editable']) { $field['type'] = 'checkbox'; $field['description_key'] = 'mod_form_field_recordings_deleted'; $recordingsettings = true; } $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], $field['description_key'], $cfg['recordings_deleted_default']); $field = ['type' => 'hidden', 'name' => 'recordings_imported', 'data_type' => PARAM_INT, 'description_key' => null]; if ($cfg['importrecordings_enabled'] && $cfg['recordings_imported_editable']) { $field['type'] = 'checkbox'; $field['description_key'] = 'mod_form_field_recordings_imported'; $recordingsettings = true; } $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], $field['description_key'], $cfg['recordings_imported_default']); $field = ['type' => 'hidden', 'name' => 'recordings_preview', 'data_type' => PARAM_INT, 'description_key' => null]; if ($cfg['recordings_preview_editable']) { $field['type'] = 'checkbox'; $field['description_key'] = 'mod_form_field_recordings_preview'; $recordingsettings = true; } $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], $field['description_key'], $cfg['recordings_preview_default']); if (!$recordingsettings) { $field = ['type' => 'static', 'name' => 'no_recordings', 'defaultvalue' => get_string('mod_form_field_nosettings', 'bigbluebuttonbn')]; $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], null, null, $field['defaultvalue']); } } /** * Function for showing the block for room settings. * * @param MoodleQuickForm $mform * @param array $cfg * @return void */ private function bigbluebuttonbn_mform_add_block_room(MoodleQuickForm &$mform, array $cfg) { if ($cfg['voicebridge_editable'] || $cfg['waitformoderator_editable'] || $cfg['userlimit_editable'] || $cfg['recording_editable'] || $cfg['muteonstart_editable']) { $mform->addElement('header', 'room', get_string('mod_form_block_room', 'bigbluebuttonbn')); $this->bigbluebuttonbn_mform_add_block_room_room($mform, $cfg); } if ($cfg['recordings_deleted_editable'] || $cfg['recordings_imported_editable'] || $cfg['recordings_preview_editable']) { $mform->addElement('header', 'recordings', get_string('mod_form_block_recordings', 'bigbluebuttonbn')); $this->bigbluebuttonbn_mform_add_block_room_recordings($mform, $cfg); } } /** * Function for showing the block for preuploaded presentation. * * @param MoodleQuickForm $mform * @param array $cfg * @return void */ private function bigbluebuttonbn_mform_add_block_preuploads(MoodleQuickForm &$mform, array $cfg): void { $bigbluebuttonbn = get_config('mod_bigbluebuttonbn'); if ($cfg['preuploadpresentation_editable'] || $bigbluebuttonbn->showpresentation_editable) { $mform->addElement('header', 'preuploadpresentation', get_string('mod_form_block_presentation', 'bigbluebuttonbn')); $mform->setExpanded('preuploadpresentation'); if ($cfg['preuploadpresentation_editable']) { $filemanageroptions = []; $filemanageroptions['accepted_types'] = '*'; $filemanageroptions['maxbytes'] = 0; $filemanageroptions['subdirs'] = 0; $filemanageroptions['maxfiles'] = 1; $filemanageroptions['mainfile'] = true; $mform->addElement('filemanager', 'presentation', get_string('selectfiles'), null, $filemanageroptions); } if ($bigbluebuttonbn->showpresentation_editable) { $mform->addElement('advcheckbox', 'showpresentation', get_string('mod_form_field_showpresentation', 'bigbluebuttonbn')); $mform->setDefault('showpresentation', $bigbluebuttonbn->showpresentation_default); } else { $mform->addElement('hidden', 'showpresentation', 0); } } $mform->setType('showpresentation', PARAM_BOOL); } /** * Function for showing the block for setting participant roles. * * @param MoodleQuickForm $mform * @param array $participantlist * @return void */ private function bigbluebuttonbn_mform_add_block_user_role_mapping(MoodleQuickForm &$mform, array $participantlist): void { global $OUTPUT; $participantselection = roles::get_participant_selection_data(); $mform->addElement('header', 'permissions', get_string('mod_form_block_participants', 'bigbluebuttonbn')); $mform->setExpanded('permissions'); $mform->addElement('hidden', 'participants', json_encode($participantlist)); $mform->setType('participants', PARAM_TEXT); $selectiontype = new single_select(new moodle_url(qualified_me()), 'bigbluebuttonbn_participant_selection_type', $participantselection['type_options'], $participantselection['type_selected']); $selectionparticipants = new single_select(new moodle_url(qualified_me()), 'bigbluebuttonbn_participant_selection', $participantselection['options'], $participantselection['selected']); $action = new single_button(new moodle_url(qualified_me()), get_string('mod_form_field_participant_list_action_add', 'bigbluebuttonbn'), 'post', single_button::BUTTON_SECONDARY, ['name' => 'bigbluebuttonbn_participant_selection_add'] ); $pformcontext = [ 'selectionType' => $selectiontype->export_for_template($OUTPUT), 'selectionParticipant' => $selectionparticipants->export_for_template($OUTPUT), 'action' => $action->export_for_template($OUTPUT), ]; $html = $OUTPUT->render_from_template('mod_bigbluebuttonbn/participant_form', $pformcontext); $mform->addElement('static', 'static_participant_list', get_string('mod_form_field_participant_list', 'bigbluebuttonbn'), $html); } /** * Function to add guest acces settings to the instance * * @param MoodleQuickForm $mform * @param array $cfg * @param stdClass $current * @return void * @throws coding_exception */ private function bigbluebuttonbn_mform_add_block_guest_access(MoodleQuickForm $mform, array $cfg, stdClass $current) { if (!empty($cfg['guestaccess_enabled'])) { $mform->addElement('header', 'guestaccess', get_string('mod_form_block_guestaccess', 'bigbluebuttonbn')); $mform->setExpanded('guestaccess'); $mform->addElement('advcheckbox', 'guestallowed', get_string('mod_form_field_guestallowed', 'bigbluebuttonbn')); $mform->addElement('advcheckbox', 'mustapproveuser', get_string('mod_form_field_mustapproveuser', 'bigbluebuttonbn')); $mform->hideIf('mustapproveuser', 'guestallowed'); if (!empty($this->_cm)) { $instance = instance::get_from_cmid($this->_cm->id); \mod_bigbluebuttonbn\form\guest_add::add_meeting_links_elements($mform); $mform->setDefault('guestjoinurl', $instance->get_guest_access_url()); $mform->setDefault('guestpassword', $instance->get_guest_access_password()); $mform->hideIf('guestjoinurl', 'guestallowed'); $mform->hideIf('guestpassword', 'guestallowed'); } } else { $mform->addElement('hidden', 'guestallowed', 0); $mform->addElement('hidden', 'mustapproveuser', 0); } $mform->setType('guestallowed', PARAM_BOOL); $mform->setType('mustapproveuser', PARAM_BOOL); $mform->setDefault('guestallowed', 0); $mform->setDefault('mustapproveuser', 1); } /** * Function for showing the block for integration with the calendar. * * @param MoodleQuickForm $mform * @param stdClass $activity * @return void */ private function bigbluebuttonbn_mform_add_block_schedule(MoodleQuickForm &$mform, stdClass &$activity) { $mform->addElement('header', 'schedule', get_string('mod_form_block_schedule', 'bigbluebuttonbn')); if (!empty($activity->openingtime) || !empty($activity->closingtime)) { $mform->setExpanded('schedule'); } $mform->addElement('date_time_selector', 'openingtime', get_string('mod_form_field_openingtime', 'bigbluebuttonbn'), ['optional' => true]); $mform->setDefault('openingtime', 0); $mform->addElement('date_time_selector', 'closingtime', get_string('mod_form_field_closingtime', 'bigbluebuttonbn'), ['optional' => true]); $mform->setDefault('closingtime', 0); } /** * Function for showing an element. * * @param MoodleQuickForm $mform * @param string $type * @param string $name * @param string|null $datatype * @param string|null $descriptionkey * @param mixed|null $defaultvalue * @param array|null $options * @param array|null $rule * @return void */ private function bigbluebuttonbn_mform_add_element(MoodleQuickForm &$mform, string $type, string $name, ?string $datatype, ?string $descriptionkey = "", $defaultvalue = null, ?array $options = null, ?array $rule = null): void { $datatype = $datatype ?? 'hidden'; if ($type === 'hidden' || $type === 'static') { $mform->addElement($type, $name, $defaultvalue); $mform->setType($name, $datatype); return; } if ($descriptionkey) { $mform->addElement($type, $name, get_string($descriptionkey, 'bigbluebuttonbn'), $options); if (get_string_manager()->string_exists($descriptionkey . '_help', 'bigbluebuttonbn')) { $mform->addHelpButton($name, $descriptionkey, 'bigbluebuttonbn'); } } if (!empty($rule)) { $mform->addRule($name, $rule['message'], $rule['type'], $rule['rule'], $rule['validator']); } $mform->setDefault($name, $defaultvalue); $mform->setType($name, $datatype); } /** * Definition after data * * Here just to tweak form group in completion that should not be frozen. This avoid * unwanted warnings. */ public function definition_after_data() { parent::definition_after_data(); foreach ($this->formextensions as $formextension) { $formextension->definition_after_data(); } } /** * Add subplugins form elements * @return void */ private function add_subplugins_elements() { foreach ($this->formextensions as $formextension) { $formextension->add_fields(); } } } PK\Qr! styles.cssnu[PK\wZZ 11Xdb/services.phpnu[PK\~~db/subplugins.jsonnu[PK\Ahzdb/install.phpnu[PK\.} Ydb/log.phpnu[PK\)Һ }(db/tasks.phpnu[PK\Eqq s.db/access.phpnu[PK\!Edb/upgrade.phpnu[PK\[k#k# Zdb/install.xmlnu[PK\Sb}db/messages.phpnu[PK\ X db/caches.phpnu[PK\Zlang/en/bigbluebuttonbn.phpnu[PK\Vlang/en/deprecated.txtnu[PK\+YY2Wbackup/moodle2/backup_bigbluebuttonbn_stepslib.phpnu[PK\iS>Ohbackup/moodle2/restore_bigbluebuttonbn_activity_task.class.phpnu[PK\o  =gybackup/moodle2/backup_bigbluebuttonbn_activity_task.class.phpnu[PK\Y? KK3backup/moodle2/restore_bigbluebuttonbn_stepslib.phpnu[PK\JJJview.phpnu[PK\Rj pix/monologo.svgnu[PK\XFnxnxlib.phpnu[PK\{( +version.phpnu[PK\B204 4 |0import_view.phpnu[PK\=Bs9adminmanageplugins.phpnu[PK\.t.t?classes/recording.phpnu[PK\C{mm@classes/form/guest_add.phpnu[PK\Ť classes/form/guest_login.phpnu[PK\q7'classes/form/text_with_copy_element.phpnu[PK\v)classes/external/view_bigbluebuttonbn.phpnu[PK\^E!classes/external/meeting_info.phpnu[PK\b##-classes/external/get_recordings_to_import.phpnu[PK\h ;classes/external/can_join.phpnu[PK\lj |Iclasses/external/end_meeting.phpnu[PK\|ZZ![classes/external/get_join_url.phpnu[PK\P<#+lclasses/external/get_recordings.phpnu[PK\KOO(classes/external/completion_validate.phpnu[PK\*cO%%%+classes/external/update_recording.phpnu[PK\N0--4classes/external/get_bigbluebuttonbns_by_courses.phpnu[PK\x226classes/privacy/provider.phpnu[PK\5151]classes/local/config.phpnu[PK\۾| 9 9'#classes/local/proxy/recording_proxy.phpnu[PK\؆;]classes/local/proxy/curl.phpnu[PK\0(T"Coclasses/local/proxy/proxy_base.phpnu[PK\HLL+classes/local/proxy/bigbluebutton_proxy.phpnu[PK\I;classes/local/bigbluebutton/recordings/recording_action.phpnu[PK\q2297classes/local/bigbluebutton/recordings/recording_data.phpnu[PK\>fRRj!classes/local/helpers/reset.phpnu[PK\0 0 $ 3classes/local/helpers/mod_helper.phpnu[PK\{B--Sclasses/local/helpers/files.phpnu[PK\ƪy==classes/local/helpers/roles.phpnu[PK\lj2 #ؾclasses/local/helpers/user_info.phpnu[PK\## +classes/local/extension/mod_form_addons.phpnu[PK\k/ classes/local/extension/mod_instance_helper.phpnu[PK\< -8?classes/local/extension/broker_meeting_events_addons.phpnu[PK\kg9 46classes/local/extension/custom_completion_addons.phpnu[PK\*22-classes/local/extension/action_url_addons.phpnu[PK\h++.$classes/local/plugins/admin_plugin_manager.phpnu[PK\6ېo o 6e$classes/local/plugins/admin_page_manage_extensions.phpnu[PK\ڪ;:/classes/local/exceptions/server_not_available_exception.phpnu[PK\UúKK3M8classes/local/exceptions/meeting_join_exception.phpnu[PK\ @4=classes/local/exceptions/bigbluebutton_exception.phpnu[PK\OT16(6(^Dclasses/extension.phpnu[PK\W; lclasses/broker.phpnu[PK\zclasses/plugininfo/bbbext.phpnu[PK\muuclasses/instance.phpnu[PK\İq=classes/search/activity.phpnu[PK\#vLBclasses/search/tags.phpnu[PK\]*Tclasses/output/recording_name_editable.phpnu[PK\ѣ׻ +:\classes/output/instance_updated_message.phpnu[PK\q q Pfclasses/output/renderer.phpnu[PK\C̺1 tclasses/output/recording_description_editable.phpnu[PK\ZM *q|classes/output/recording_row_actionbar.phpnu[PK\Kclasses/output/import_view.phpnu[PK\ AA/classes/output/view_page.phpnu[PK\b%classes/output/recording_editable.phpnu[PK\zz)classes/output/recording_row_playback.phpnu[PK\U>>>rclasses/output/index.phpnu[PK\Gr (classes/output/recording_row_preview.phpnu[PK\)\66&" classes/test/testcase_helper_trait.phpnu[PK\-x classes/test/subplugins_test_helper_trait.phpnu[PK\h>D>D, classes/logger.phpnu[PK\Կ`aa q classes/settings.phpnu[PK\L||% templates/recordings_session.mustachenu[PK\3˴ƥ +# templates/instance_updated_message.mustachenu[PK\un,- templates/participant_single_select.mustachenu[PK\Ҧ#= templates/participant_form.mustachenu[PK\ߩ D templates/import_view.mustachenu[PK\H H # P templates/recordings_table.mustachenu[PK\7'(Y templates/recording_row_preview.mustachenu[PK\I/Wa templates/room_view.mustachenu[PK\88*v templates/recording_row_actionbar.mustachenu[PK\4::2} templates/guest_links.mustachenu[PK\[գ! templates/recentactivity.mustachenu[PK\* )Њ templates/element_text_with_copy.mustachenu[PK\}F0) templates/recording_row_playback.mustachenu[PK\|ٛ templates/view_page.mustachenu[PK\%<' templates/participant_form_add.mustachenu[PK\N++% templates/end_session_button.mustachenu[PK\s  * templates/search_input_recordings.mustachenu[PK\ZU=  upgrade.txtnu[PK\;| Ӿ bbb_broker.phpnu[PK\gRR  index.phpnu[PK\C]k7k7} amd/src/recordings.jsnu[PK\```- amd/src/events.jsnu[PK\+=KK amd/src/index.jsnu[PK\m  Y amd/src/rooms.jsnu[PK\e^@u u $ amd/src/roomupdater.jsnu[PK\yZoo]2 amd/src/actions.jsnu[PK\sGx7x7; amd/src/modform.jsnu[PK\DSSr amd/src/repository.jsnu[PK\pt t ` amd/src/guest_access_modal.jsnu[PK\^^KQKQ! amd/build/recordings.min.js.mapnu[PK\/B amd/build/actions.min.jsnu[PK\"" amd/build/modform.min.jsnu[PK\1"" amd/build/recordings.min.jsnu[PK\kj) amd/build/repository.min.jsnu[PK\-|{8 0 amd/build/actions.min.js.mapnu[PK\; < amd/build/roomupdater.min.js.mapnu[PK\nm %P amd/build/rooms.min.jsnu[PK\c<^O'\ amd/build/guest_access_modal.min.js.mapnu[PK\ m\\[n amd/build/repository.min.js.mapnu[PK\B  amd/build/events.min.js.mapnu[PK\ZkkP amd/build/index.min.js.mapnu[PK\}ظ amd/build/events.min.jsnu[PK\ԥRR amd/build/index.min.jsnu[PK\Ê # amd/build/guest_access_modal.min.jsnu[PK\G;TT amd/build/roomupdater.min.jsnu[PK\OOY amd/build/modform.min.js.mapnu[PK\0G662 amd/build/rooms.min.js.mapnu[PK\fww  UPGRADING.mdnu[PK\&e cli/migrate_recordings.phpnu[PK\-`hll#F"cli/update_dismissed_recordings.phpnu[PK\l^F"6tests/fixtures/bbpresentation.pptxnu[PK\i. tests/fixtures/extension/simple/db/install.xmlnu[PK\hDJvv9ptests/fixtures/extension/simple/lang/en/bbbext_simple.phpnu[PK\z??XOtests/fixtures/extension/simple/backup/moodle2/restore_bbbext_simple_subplugin.class.phpnu[PK\[>Wtests/fixtures/extension/simple/backup/moodle2/backup_bbbext_simple_subplugin.class.phpnu[PK\͢߀+Ktests/fixtures/extension/simple/version.phpnu[PK\R?K&tests/fixtures/extension/simple/classes/bigbluebuttonbn/mod_form_addons.phpnu[PK\wAmA Otests/fixtures/extension/simple/classes/bigbluebuttonbn/mod_instance_helper.phpnu[PK\WXtests/fixtures/extension/simple/classes/bigbluebuttonbn/broker_meeting_events_addons.phpnu[PK\ Titests/fixtures/extension/simple/classes/bigbluebuttonbn/custom_completion_addons.phpnu[PK\(9M&tests/fixtures/extension/simple/classes/bigbluebuttonbn/action_url_addons.phpnu[PK\i~,.tests/fixtures/extension/simple/settings.phpnu[PK\m^(^(74tests/backup_restore_test.phpnu[PK\1!7\tests/generator/behat_mod_bigbluebuttonbn_generator.phpnu[PK\IDvCCltests/generator/lib.phpnu[PK\)t..tests/recording_test.phpnu[PK\jǪFbb5tests/external/get_bigbluebuttons_by_courses_test.phpnu[PK\"+tests/external/completion_validate_test.phpnu[PK\_|E=R=R& tests/external/get_recordings_test.phpnu[PK\xx n`tests/external/can_join_test.phpnu[PK\rOO#6ptests/external/end_meeting_test.phpnu[PK\㿪&&$؆tests/external/get_join_url_test.phpnu[PK\+;D,tests/external/view_bigbluebuttonbn_test.phpnu[PK\{`tests/coverage.phpnu[PK\ / /tests/privacy/provider_test.phpnu[PK\z\XlLL.tests/local/proxy/bigbluebutton_proxy_test.phpnu[PK\g %tests/local/proxy/proxy_base_test.phpnu[PK\=AO&&* tests/local/proxy/recording_proxy_test.phpnu[PK\Pkss<Rtests/local/bigbluebutton/recordings/recording_data_test.phpnu[PK\ ,~NVJVJ1#tests/local/extension_test.phpnu[PK\ߣ^"mtests/local/helpers/reset_test.phpnu[PK\ъ("5tests/local/helpers/roles_test.phpnu[PK\"kx#x#-Atests/local/helpers/mod_helper_trait_test.phpnu[PK\yy&tests/local/helpers/user_info_test.phpnu[PK\D^, & &"tests/local/helpers/files_test.phpnu[PK\zxxAtests/lib_test.phpnu[PK\ y5N5NNltests/meeting_test.phpnu[PK\)Z ɺtests/logger_test.phpnu[PK\.>,tests/output/recording_row_playback_test.phpnu[PK\·] tests/search_test.phpnu[PK\Fߐ0tests/task/completion_update_state_task_test.phpnu[PK\M=uJJ)tests/task/check_recordings_task_test.phpnu[PK\՜ %tests/task/send_guest_emails_test.phpnu[PK\e *tests/task/base_send_notification_test.phpnu[PK\^-S?S?+'tests/task/upgrade_recordings_task_test.phpnu[PK\C]]5gtests/task/send_recording_ready_notification_test.phpnu[PK\v*O[[Wtests/plugin_test.phpnu[PK\ |7474$tests/completion/completion_test.phpnu[PK\VWWtests/instance_test.phpnu[PK\>:1tests/behat/fixtures/show_simpleplugin_values.phpnu[PK\3!!! tests/behat/meeting_roles.featurenu[PK\1Ht t /%tests/behat/add_instance_in_empty_state.featurenu[PK\4 R/tests/behat/completion.featurenu[PK\#ee=tests/behat/recordings.featurenu[PK\J@ )@Mtests/behat/join_meeting_as_guest.featurenu[PK\/3Wtests/behat/end_meeting.featurenu[PK\7`Ritests/behat/group_mode.featurenu[PK\`