ÿØÿà 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*\edSSview.phpnu[. /** * Generic content bank visualizer. * * @package core_contentbank * @copyright 2020 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core_contentbank\content; require('../config.php'); require_login(); $id = required_param('id', PARAM_INT); $deletecontent = optional_param('deletecontent', null, PARAM_INT); $PAGE->requires->js_call_amd('core_contentbank/actions', 'init'); $record = $DB->get_record('contentbank_content', ['id' => $id], '*', MUST_EXIST); $context = context::instance_by_id($record->contextid, MUST_EXIST); require_capability('moodle/contentbank:access', $context); // If notifications had been sent we don't pay attention to message parameter. if (empty($SESSION->notifications)) { $statusmsg = optional_param('statusmsg', '', PARAM_ALPHANUMEXT); $errormsg = optional_param('errormsg', '', PARAM_ALPHANUMEXT); } $returnurl = new \moodle_url('/contentbank/index.php', ['contextid' => $context->id]); $plugin = core_plugin_manager::instance()->get_plugin_info($record->contenttype); if (!$plugin || !$plugin->is_enabled()) { throw new \moodle_exception('unsupported', 'core_contentbank', $returnurl); } $title = get_string('contentbank'); \core_contentbank\helper::get_page_ready($context, $title, true); if ($PAGE->course) { require_login($PAGE->course->id); } $cb = new \core_contentbank\contentbank(); $content = $cb->get_content_from_id($record->id); $contenttype = $content->get_content_type_instance(); if (!$content->is_view_allowed()) { $cburl = new \moodle_url('/contentbank/index.php', ['contextid' => $context->id, 'errormsg' => 'notavailable']); redirect($cburl); } if ($context->contextlevel == CONTEXT_COURSECAT) { $PAGE->set_primary_active_tab('home'); } $PAGE->set_url(new \moodle_url('/contentbank/view.php', ['id' => $id])); if ($context->id == \context_system::instance()->id) { $PAGE->set_context(context_course::instance($context->id)); } else { $PAGE->set_context($context); } $PAGE->navbar->add($record->name); $title .= ": ".$record->name; $PAGE->set_title($title); $PAGE->set_pagetype('contentbank'); $PAGE->set_pagelayout('incourse'); $PAGE->set_secondary_active_tab('contentbank'); echo $OUTPUT->header(); // If needed, display notifications. if (!empty($errormsg) && get_string_manager()->string_exists($errormsg, 'core_contentbank')) { $errormsg = get_string($errormsg, 'core_contentbank'); echo $OUTPUT->notification($errormsg); } else if (!empty($statusmsg) && get_string_manager()->string_exists($statusmsg, 'core_contentbank')) { if ($statusmsg == 'contentvisibilitychanged') { switch ($content->get_visibility()) { case content::VISIBILITY_PUBLIC: $visibilitymsg = get_string('public', 'core_contentbank'); break; case content::VISIBILITY_UNLISTED: $visibilitymsg = get_string('unlisted', 'core_contentbank'); break; default: throw new \moodle_exception('contentvisibilitynotfound', 'error', $returnurl, $content->get_visibility()); break; } $statusmsg = get_string($statusmsg, 'core_contentbank', $visibilitymsg); } else { $statusmsg = get_string($statusmsg, 'core_contentbank'); } echo $OUTPUT->notification($statusmsg, 'notifysuccess'); } if ($contenttype->can_access()) { $viewcontent = new core_contentbank\output\viewcontent($contenttype, $content); echo $OUTPUT->render($viewcontent); } else { $message = get_string('contenttypenoaccess', 'core_contentbank', $record->contenttype); echo $OUTPUT->notification($message, 'error'); } echo $OUTPUT->footer(); PK*\??customfield.phpnu[. /** * Manage Content Bank custom fields * * @package core_contentbank * @copyright 2024 Daniel Neis Araujo * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../config.php'); require_once($CFG->libdir.'/adminlib.php'); admin_externalpage_setup('contentbank'); $output = $PAGE->get_renderer('core_customfield'); $handler = core_contentbank\customfield\content_handler::create(); $outputpage = new \core_customfield\output\management($handler); echo $output->header(), $output->heading(new lang_string('contentbank')), $output->render($outputpage), $output->footer(); PK*\i"lib.phpnu[. /** * This plugin is used to access the content bank files. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/repository/lib.php'); /** * repository_contentbank class is used to browse the content bank files * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_contentbank extends repository { /** * Get file listing. * * @param string $encodedpath * @param string $page * @return array */ public function get_listing($encodedpath = '', $page = '') { global $SITE; $ret = []; $ret['dynload'] = true; $ret['nosearch'] = false; $ret['nologin'] = true; // Return the parameters from the encoded path if the encoded path is not empty. if (!empty($encodedpath)) { $params = json_decode(base64_decode($encodedpath), true); if (is_array($params) && isset($params['contextid'])) { $context = context::instance_by_id(clean_param($params['contextid'], PARAM_INT)); } } // Return the current context if the context was not specified in the encoded path. // The current context should be an instance of context_system, context_coursecat or course related contexts. if (empty($context) && !empty($this->context)) { if ($this->context instanceof \context_system || $this->context instanceof \context_coursecat) { $context = $this->context; } else if ($coursecontext = $this->context->get_course_context(false)) { // Skip if front page context. if ($coursecontext->instanceid !== $SITE->id) { $context = $coursecontext; } } } // If not, return the system context as a default context. if (empty($context)) { $context = context_system::instance(); } $ret['list'] = []; $ret['path'] = []; // Get the content bank browser for the specified context. if ($browser = \repository_contentbank\helper::get_contentbank_browser($context)) { $manageurl = new moodle_url('/contentbank/index.php', ['contextid' => $context->id]); $canaccesscontent = has_capability('moodle/contentbank:access', $context); $ret['manage'] = $canaccesscontent ? $manageurl->out() : ''; $ret['list'] = $browser->get_content(); $ret['path'] = $browser->get_navigation(); } return $ret; } /** * Is this repository used to browse moodle files? * * @return boolean */ public function has_moodle_files() { return true; } /** * Tells how the file can be picked from this repository. * * @return int */ public function supported_returntypes() { return FILE_INTERNAL | FILE_REFERENCE; } /** * Which return type should be selected by default. * * @return int */ public function default_returntype() { return FILE_REFERENCE; } /** * Is this repository accessing private data? * * @return bool */ public function contains_private_data() { return false; } /** * Repository method to make sure that user can access particular file. * * This is checked when user tries to pick the file from repository to deal with * potential parameter substitutions in request * * @param string $source * @return bool whether the file is accessible by current user */ public function file_is_accessible($source) { global $DB; $fileparams = json_decode(base64_decode($source)); $itemid = clean_param($fileparams->itemid, PARAM_INT); $contextid = clean_param($fileparams->contextid, PARAM_INT); $contentbankfile = $DB->get_record('contentbank_content', ['id' => $itemid]); $plugin = \core_plugin_manager::instance()->get_plugin_info($contentbankfile->contenttype); $managerclass = "\\$contentbankfile->contenttype\\content"; if ($plugin && $plugin->is_enabled() && class_exists($managerclass)) { $context = \context::instance_by_id($contextid); $browser = \repository_contentbank\helper::get_contentbank_browser($context); return $browser->can_access_content(); } return false; } /** * Return search results. * * @param string $search * @param int $page * @return array */ public function search($search, $page = 0) { $ret = []; $ret['nologin'] = true; $ret['list'] = \repository_contentbank\contentbank_search::get_search_contents($search); return $ret; } } PK*\`2'classes/customfield/content_handler.phpnu[. namespace core_contentbank\customfield; use core_customfield\api; use core_customfield\field_controller; /** * Content handler for content bank custom fields * * @package core_contentbank * @copyright 2024 Daniel Neis Araujo * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class content_handler extends \core_customfield\handler { /** * @var content_handler */ static protected $singleton; /** * @var \context */ protected $parentcontext; /** @var int Field is displayed in the content bank edit page, visible to everybody */ const VISIBLETOALL = 2; /** @var int Field is not displayed in the content bank edit page */ const NOTVISIBLE = 0; /** * Returns a singleton * * @param int $itemid * @return \core_contentbank\customfield\content_handler */ public static function create(int $itemid = 0): \core_contentbank\customfield\content_handler { if (static::$singleton === null) { self::$singleton = new static(0); } return self::$singleton; } /** * Run reset code after unit tests to reset the singleton usage. */ public static function reset_caches(): void { if (!PHPUNIT_TEST) { throw new \coding_exception('This feature is only intended for use in unit tests'); } static::$singleton = null; } /** * The current user can configure custom fields on this component. * * @return bool true if the current can configure custom fields, false otherwise */ public function can_configure(): bool { return has_capability('moodle/contentbank:configurecustomfields', $this->get_configuration_context()); } /** * The current user can edit custom fields on the content bank. * * @param field_controller $field * @param int $instanceid id of the course to test edit permission * @return bool true if the current can edit custom fields, false otherwise */ public function can_edit(field_controller $field, int $instanceid = 0): bool { $context = $this->get_instance_context($instanceid); return (!$field->get_configdata_property('locked') || has_capability('moodle/contentbank:changelockedcustomfields', $context)); } /** * The current user can view custom fields on the given course. * * @param field_controller $field * @param int $instanceid id of the course to test edit permission * @return bool true if the current can edit custom fields, false otherwise */ public function can_view(field_controller $field, int $instanceid): bool { $visibility = $field->get_configdata_property('visibility'); if ($visibility == self::NOTVISIBLE) { return false; } else { return true; } } /** * Context that should be used for new categories created by this handler * * @return \context the context for configuration */ public function get_configuration_context(): \context { return \context_system::instance(); } /** * URL for configuration of the fields on this handler. * * @return \moodle_url The URL to configure custom fields for this component */ public function get_configuration_url(): \moodle_url { return new \moodle_url('/contentbank/customfield.php'); } /** * Returns the context for the data associated with the given instanceid. * * @param int $instanceid id of the record to get the context for * @return \context the context for the given record */ public function get_instance_context(int $instanceid = 0): \context { global $DB; $contextid = $DB->get_field('contentbank_content', 'contextid', ['id' => $instanceid]); if (!$contextid || $contextid == SITEID) { return \context_system::instance(); } else { return \context::instance_by_id($contextid); } } /** * Allows to add custom controls to the field configuration form that will be saved in configdata * * @param \MoodleQuickForm $mform */ public function config_form_definition(\MoodleQuickForm $mform) { $mform->addElement('header', 'content_handler_header', get_string('customfieldsettings', 'core_course')); $mform->setExpanded('content_handler_header', true); // If field is locked. $mform->addElement('selectyesno', 'configdata[locked]', get_string('customfield_islocked', 'core_course')); $mform->addHelpButton('configdata[locked]', 'customfield_islocked', 'core_course'); // Field data visibility. $visibilityoptions = [self::VISIBLETOALL => get_string('customfield_visibletoall', 'core_course'), self::NOTVISIBLE => get_string('customfield_notvisible', 'core_course')]; $mform->addElement('select', 'configdata[visibility]', get_string('customfield_visibility', 'core_course'), $visibilityoptions); $mform->addHelpButton('configdata[visibility]', 'customfield_visibility', 'core_course'); } /** * Creates or updates custom field data. * * @param \restore_task $task * @param array $data */ public function restore_instance_data_from_backup(\restore_task $task, array $data) { $courseid = $task->get_courseid(); $context = $this->get_instance_context($courseid); $editablefields = $this->get_editable_fields($courseid); $records = api::get_instance_fields_data($editablefields, $courseid); $target = $task->get_target(); $override = ($target != \backup::TARGET_CURRENT_ADDING && $target != \backup::TARGET_EXISTING_ADDING); foreach ($records as $d) { $field = $d->get_field(); if ($field->get('shortname') === $data['shortname'] && $field->get('type') === $data['type']) { if (!$d->get('id') || $override) { $d->set($d->datafield(), $data['value']); $d->set('value', $data['value']); $d->set('valueformat', $data['valueformat']); $d->set('contextid', $context->id); $d->save(); } return; } } } } PK*\f""classes/form/upload_files.phpnu[. namespace core_contentbank\form; use core\output\notification; /** * Upload files to content bank form * * @package core_contentbank * @copyright 2020 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class upload_files extends \core_form\dynamic_form { /** * Add elements to this form. */ public function definition() { $mform = $this->_form; $mform->addElement('hidden', 'contextid'); $mform->setType('contextid', PARAM_INT); $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); $mform->addElement('filepicker', 'file', get_string('file', 'core_contentbank'), null, $this->get_options()); $mform->addHelpButton('file', 'file', 'core_contentbank'); $mform->addRule('file', null, 'required'); } /** * Validate incoming data. * * @param array $data * @param array $files * @return array */ public function validation($data, $files) { $errors = array(); $draftitemid = $data['file']; $options = $this->get_options(); if (file_is_draft_area_limit_reached($draftitemid, $options['areamaxbytes'])) { $errors['file'] = get_string('userquotalimit', 'error'); } return $errors; } /** * Check if current user has access to this form, otherwise throw exception * * Sometimes permission check may depend on the action and/or id of the entity. * If necessary, form data is available in $this->_ajaxformdata or * by calling $this->optional_param() */ protected function check_access_for_dynamic_submission(): void { require_capability('moodle/contentbank:upload', $this->get_context_for_dynamic_submission()); // Check the context used by the content bank is allowed. $cb = new \core_contentbank\contentbank(); if (!$cb->is_context_allowed($this->get_context_for_dynamic_submission())) { throw new \moodle_exception('contextnotallowed', 'core_contentbank'); } // If $id is defined, the file content will be replaced (instead of uploading a new one). // Check that the user has the right permissions to replace this content file. $id = $this->optional_param('id', null, PARAM_INT); if ($id) { $content = $cb->get_content_from_id($id); $contenttype = $content->get_content_type_instance(); if (!$contenttype->can_manage($content) || !$contenttype->can_upload()) { throw new \moodle_exception('nopermissions', 'error', '', null, get_string('replacecontent', 'contentbank')); } } } /** * Returns form context * * If context depends on the form data, it is available in $this->_ajaxformdata or * by calling $this->optional_param() * * @return \context */ protected function get_context_for_dynamic_submission(): \context { $contextid = $this->optional_param('contextid', null, PARAM_INT); return \context::instance_by_id($contextid, MUST_EXIST); } /** * File upload options * * @return array * @throws \coding_exception */ protected function get_options(): array { global $CFG; $maxbytes = $CFG->userquota; $maxareabytes = $CFG->userquota; if (has_capability('moodle/user:ignoreuserquota', $this->get_context_for_dynamic_submission())) { $maxbytes = USER_CAN_IGNORE_FILE_SIZE_LIMITS; $maxareabytes = FILE_AREA_MAX_BYTES_UNLIMITED; } $cb = new \core_contentbank\contentbank(); $id = $this->optional_param('id', null, PARAM_INT); if ($id) { $content = $cb->get_content_from_id($id); $contenttype = $content->get_content_type_instance(); $extensions = $contenttype->get_manageable_extensions(); $acceptedtypes = implode(',', $extensions); } else { $acceptedtypes = $cb->get_supported_extensions_as_string($this->get_context_for_dynamic_submission()); } return ['subdirs' => 1, 'maxbytes' => $maxbytes, 'maxfiles' => -1, 'accepted_types' => $acceptedtypes, 'areamaxbytes' => $maxareabytes]; } /** * Process the form submission, used if form was submitted via AJAX * * This method can return scalar values or arrays that can be json-encoded, they will be passed to the caller JS. * * Submission data can be accessed as: $this->get_data() * * @return mixed */ public function process_dynamic_submission() { global $USER; // Get the file and create the content based on it. $usercontext = \context_user::instance($USER->id); $fs = get_file_storage(); $files = $fs->get_area_files($usercontext->id, 'user', 'draft', $this->get_data()->file, 'itemid, filepath, filename', false); if (!empty($files)) { $file = reset($files); $cb = new \core_contentbank\contentbank(); try { if ($this->get_data()->id) { $content = $cb->get_content_from_id($this->get_data()->id); $contenttype = $content->get_content_type_instance(); $content = $contenttype->replace_content($file, $content); } else { $content = $cb->create_content_from_file($this->get_context_for_dynamic_submission(), $USER->id, $file); } $params = ['id' => $content->get_id(), 'contextid' => $this->get_context_for_dynamic_submission()->id]; $url = new \moodle_url('/contentbank/view.php', $params); } catch (\moodle_exception $e) { // Redirect to the right page (depending on if content is new or existing) and display an error. if ($this->get_data()->id) { $content = $cb->get_content_from_id($this->get_data()->id); $params = [ 'id' => $content->get_id(), 'contextid' => $this->get_context_for_dynamic_submission()->id, 'errormsg' => $e->errorcode, ]; $url = new \moodle_url('/contentbank/view.php', $params); } else { $url = new \moodle_url('/contentbank/index.php', [ 'contextid' => $this->get_context_for_dynamic_submission()->id, 'errormsg' => $e->errorcode], ); } } return ['returnurl' => $url->out(false)]; } return null; } /** * Load in existing data as form defaults * * Can be overridden to retrieve existing values from db by entity id and also * to preprocess editor and filemanager elements * * Example: * $this->set_data(get_entity($this->_ajaxformdata['id'])); */ public function set_data_for_dynamic_submission(): void { $data = (object)[ 'contextid' => $this->optional_param('contextid', null, PARAM_INT), 'id' => $this->optional_param('id', null, PARAM_INT), ]; $this->set_data($data); } /** * Returns url to set in $PAGE->set_url() when form is being rendered or submitted via AJAX * * This is used in the form elements sensitive to the page url, such as Atto autosave in 'editor' * * If the form has arguments (such as 'id' of the element being edited), the URL should * also have respective argument. * * @return \moodle_url */ protected function get_page_url_for_dynamic_submission(): \moodle_url { $params = ['contextid' => $this->get_context_for_dynamic_submission()->id]; $id = $this->optional_param('id', null, PARAM_INT); if ($id) { $url = '/contentbank/view.php'; $params['id'] = $id; } else { $url = '/contentbank/index.php'; } return new \moodle_url($url, $params); } } PK*\ȲA A classes/form/edit_content.phpnu[. /** * Provides {@see \core_contentbank\form\edit_content} class. * * @package core_contentbank * @copyright 2020 Victor Deniz * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_contentbank\form; use moodleform; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/formslib.php'); /** * Defines the form for editing a content. * * @package core_contentbank * @copyright 2020 Victor Deniz * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class edit_content extends moodleform { /** @var int Context the content belongs to. */ protected $contextid; /** @var string Content type plugin name. */ protected $plugin; /** @var int Content id in the content bank. */ protected $id; /** * Constructor. * * @param string $action The action attribute for the form. * @param array $customdata Data to set during instance creation. * @param string $method Form method. */ public function __construct(?string $action = null, ?array $customdata = null, string $method = 'post') { $this->contextid = $customdata['contextid']; $this->plugin = $customdata['plugin']; $this->id = $customdata['id'] ?? 0; parent::__construct($action, $customdata, $method); $mform =& $this->_form; $mform->addElement('hidden', 'contextid', $this->contextid); $this->_form->setType('contextid', PARAM_INT); $mform->addElement('hidden', 'plugin', $this->plugin); $this->_form->setType('plugin', PARAM_PLUGIN); $mform->addElement('hidden', 'id', $this->id); $this->_form->setType('id', PARAM_INT); } /** * Add elements to form */ protected function definition() { global $DB; // Add custom fields to the form. $content = $DB->get_record('contentbank_content', ['id' => $this->id]); $handler = \core_contentbank\customfield\content_handler::create(); $handler->instance_form_definition($this->_form, $this->id); if ($content) { $handler->instance_form_before_set_data($content); } $this->set_data($content); } /** * Overrides formslib's add_action_buttons() method. * * * @param bool $cancel * @param string|null $submitlabel * * @return void */ public function add_action_buttons($cancel = true, $submitlabel = null): void { if (is_null($submitlabel)) { $submitlabel = get_string('save'); } parent::add_action_buttons($cancel, $submitlabel); } } PK*\ѱ/#classes/external/rename_content.phpnu[. namespace core_contentbank\external; use core_contentbank\contentbank; 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; /** * This is the external method for renaming a content. * * @package core_contentbank * @copyright 2020 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class rename_content extends external_api { /** * rename_content parameters. * * @since Moodle 3.9 * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters( [ 'contentid' => new external_value(PARAM_INT, 'The content id to rename', VALUE_REQUIRED), 'name' => new external_value(PARAM_RAW, 'The new name for the content', VALUE_REQUIRED), ] ); } /** * Rename content from the contentbank. * * @since Moodle 3.9 * @param int $contentid The content id to rename. * @param string $name The new name. * @return array True if the content has been renamed; false and the warning, otherwise. */ public static function execute(int $contentid, string $name): array { global $DB; $result = false; $warnings = []; $params = self::validate_parameters(self::execute_parameters(), [ 'contentid' => $contentid, 'name' => $name, ]); $params['name'] = clean_param($params['name'], PARAM_TEXT); // If name is empty don't try to rename and return a more detailed message. if (trim($params['name']) === '') { $warnings[] = [ 'item' => $params['contentid'], 'warningcode' => 'emptynamenotallowed', 'message' => get_string('emptynamenotallowed', 'core_contentbank') ]; } else { try { $record = $DB->get_record('contentbank_content', ['id' => $params['contentid']], '*', MUST_EXIST); $cb = new contentbank(); $content = $cb->get_content_from_id($record->id); $contenttype = $content->get_content_type_instance(); $context = \context::instance_by_id($record->contextid, MUST_EXIST); self::validate_context($context); // Check capability. if ($contenttype->can_manage($content)) { // This content can be renamed. if ($contenttype->rename_content($content, $params['name'])) { $result = true; } else { $warnings[] = [ 'item' => $params['contentid'], 'warningcode' => 'contentnotrenamed', 'message' => get_string('contentnotrenamed', 'core_contentbank') ]; } } else { // The user has no permission to manage this content. $warnings[] = [ 'item' => $params['contentid'], 'warningcode' => 'nopermissiontomanage', 'message' => get_string('nopermissiontomanage', 'core_contentbank') ]; } } catch (\moodle_exception $e) { // The content or the context don't exist. $warnings[] = [ 'item' => $params['contentid'], 'warningcode' => 'exception', 'message' => $e->getMessage() ]; } } return [ 'result' => $result, 'warnings' => $warnings ]; } /** * rename_content return. * * @since Moodle 3.9 * @return external_single_structure */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'result' => new external_value(PARAM_BOOL, 'The processing result'), 'warnings' => new external_warnings() ]); } } PK*\d #classes/external/delete_content.phpnu[. namespace core_contentbank\external; use core_contentbank\contentbank; 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; /** * This is the external method for deleting a content. * * @package core_contentbank * @since Moodle 3.9 * @copyright 2020 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class delete_content extends external_api { /** * Parameters. * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters([ 'contentids' => new external_multiple_structure( new external_value(PARAM_INT, 'The content id to delete', VALUE_REQUIRED) ) ]); } /** * Delete content from the contentbank. * * @param array $contentids List of content ids to delete. * @return array True if the content has been deleted; false and the warning, otherwise. */ public static function execute(array $contentids): array { global $DB; $result = false; $warnings = []; $params = self::validate_parameters(self::execute_parameters(), ['contentids' => $contentids]); $cb = new contentbank(); foreach ($params['contentids'] as $contentid) { try { $record = $DB->get_record('contentbank_content', ['id' => $contentid], '*', MUST_EXIST); $content = $cb->get_content_from_id($record->id); $contenttype = $content->get_content_type_instance(); $context = \context::instance_by_id($record->contextid, MUST_EXIST); self::validate_context($context); // Check capability. if ($contenttype->can_delete($content)) { // This content can be deleted. if (!$contenttype->delete_content($content)) { $warnings[] = [ 'item' => $contentid, 'warningcode' => 'contentnotdeleted', 'message' => get_string('contentnotdeleted', 'core_contentbank') ]; } } else { // The user has no permission to delete this content. $warnings[] = [ 'item' => $contentid, 'warningcode' => 'nopermissiontodelete', 'message' => get_string('nopermissiontodelete', 'core_contentbank') ]; } } catch (\moodle_exception $e) { // The content or the context don't exist. $warnings[] = [ 'item' => $contentid, 'warningcode' => 'exception', 'message' => $e->getMessage() ]; } } if (empty($warnings)) { $result = true; } return [ 'result' => $result, 'warnings' => $warnings ]; } /** * Return. * * @return external_single_structure */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'result' => new external_value(PARAM_BOOL, 'The processing result'), 'warnings' => new external_warnings() ]); } } PK*\l!classes/external/copy_content.phpnu[. namespace core_contentbank\external; use core_contentbank\contentbank; 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; /** * This is the external method for copying a content. * * @package core_contentbank * @copyright 2023 Daniel Neis Araujo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class copy_content extends external_api { /** * copy_content parameters. * * @since Moodle 4.3 * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters( [ 'contentid' => new external_value(PARAM_INT, 'The content id to copy', VALUE_REQUIRED), 'name' => new external_value(PARAM_RAW, 'The new name for the content', VALUE_REQUIRED), ] ); } /** * Copy content from the contentbank. * * @since Moodle 4.3 * @param int $contentid The content id to copy. * @param string $name The new name. * @return array Id of the new content; false and the warning, otherwise. */ public static function execute(int $contentid, string $name): array { global $DB; $id = 0; $warnings = []; $params = self::validate_parameters(self::execute_parameters(), [ 'contentid' => $contentid, 'name' => $name, ]); $params['name'] = clean_param($params['name'], PARAM_TEXT); // If name is empty don't try to copy and return a more detailed message. if (trim($params['name']) === '') { $warnings[] = [ 'item' => $params['contentid'], 'warningcode' => 'emptynamenotallowed', 'message' => get_string('emptynamenotallowed', 'core_contentbank'), ]; } else { try { $record = $DB->get_record('contentbank_content', ['id' => $params['contentid']], '*', MUST_EXIST); $cb = new contentbank(); $content = $cb->get_content_from_id($record->id); $contenttype = $content->get_content_type_instance(); $context = \context::instance_by_id($record->contextid, MUST_EXIST); self::validate_context($context); // Check capability. if ($contenttype->can_copy($content)) { // This content can be copied. $crecord = $content->get_content(); unset($crecord->id); $crecord->name = $params['name']; if ($content = $contenttype->create_content($crecord)) { $handler = \core_contentbank\customfield\content_handler::create(); $handler->instance_form_before_set_data($record); $record->id = $content->get_id(); $handler->instance_form_save($record); $fs = get_file_storage(); $files = $fs->get_area_files($context->id, 'contentbank', 'public', $params['contentid'], 'itemid, filepath, filename', false); if (!empty($files)) { $file = reset($files); $content->import_file($file); } $id = $content->get_id(); } else { $warnings[] = [ 'item' => $params['contentid'], 'warningcode' => 'contentnotcopied', 'message' => get_string('contentnotcopied', 'core_contentbank'), ]; } } else { // The user has no permission to manage this content. $warnings[] = [ 'item' => $params['contentid'], 'warningcode' => 'nopermissiontomanage', 'message' => get_string('nopermissiontocopy', 'core_contentbank'), ]; } } catch (\moodle_exception $e) { // The content or the context don't exist. $warnings[] = [ 'item' => $params['contentid'], 'warningcode' => 'exception', 'message' => $e->getMessage(), ]; } } return [ 'id' => $id, 'warnings' => $warnings, ]; } /** * copy_content return. * * @since Moodle 4.3 * @return external_single_structure */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'id' => new external_value(PARAM_INT, 'The id of the new content'), 'warnings' => new external_warnings(), ]); } } PK*\hJ;;+classes/external/set_content_visibility.phpnu[. namespace core_contentbank\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; /** * External API to set the visibility of content bank content. * * @package core_contentbank * @copyright 2020 François Moreau * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class set_content_visibility extends external_api { /** * set_content_visibility parameters. * * @since Moodle 3.11 * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters( [ 'contentid' => new external_value(PARAM_INT, 'The content id to rename', VALUE_REQUIRED), 'visibility' => new external_value(PARAM_INT, 'The new visibility for the content', VALUE_REQUIRED), ] ); } /** * Set visibility of a content from the contentbank. * * @since Moodle 3.11 * @param int $contentid The content id to rename. * @param int $visibility The new visibility. * @return array */ public static function execute(int $contentid, int $visibility): array { global $DB; $result = false; $warnings = []; $params = self::validate_parameters(self::execute_parameters(), [ 'contentid' => $contentid, 'visibility' => $visibility, ]); try { $record = $DB->get_record('contentbank_content', ['id' => $params['contentid']], '*', MUST_EXIST); $contenttypeclass = "\\$record->contenttype\\contenttype"; if (class_exists($contenttypeclass)) { $context = \context::instance_by_id($record->contextid, MUST_EXIST); self::validate_context($context); $contenttype = new $contenttypeclass($context); $contentclass = "\\$record->contenttype\\content"; $content = new $contentclass($record); // Check capability. if ($contenttype->can_manage($content)) { // This content's visibility can be changed. if ($content->set_visibility($params['visibility'])) { $result = true; } else { $warnings[] = [ 'item' => $params['contentid'], 'warningcode' => 'contentvisibilitynotset', 'message' => get_string('contentvisibilitynotset', 'core_contentbank') ]; } } else { // The user has no permission to manage this content. $warnings[] = [ 'item' => $params['contentid'], 'warningcode' => 'nopermissiontomanage', 'message' => get_string('nopermissiontomanage', 'core_contentbank') ]; } } } catch (\moodle_exception $e) { // The content or the context don't exist. $warnings[] = [ 'item' => $params['contentid'], 'warningcode' => 'exception', 'message' => $e->getMessage() ]; } return [ 'result' => $result, 'warnings' => $warnings ]; } /** * set_content_visibility return. * * @since Moodle 3.11 * @return external_single_structure */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'result' => new external_value(PARAM_BOOL, 'The processing result'), 'warnings' => new external_warnings() ]); } } PK*\classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for repository_contentbank. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_contentbank\privacy; /** * Privacy Subsystem for repository_contentbank implementing null_provider. * * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } PK*\Zч;;classes/contentbank.phpnu[. namespace core_contentbank; use core_plugin_manager; use stored_file; use context; /** * Content bank class * * @package core_contentbank * @copyright 2020 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class contentbank { /** @var array All the context levels allowed in the content bank */ private const ALLOWED_CONTEXT_LEVELS = [CONTEXT_SYSTEM, CONTEXT_COURSECAT, CONTEXT_COURSE]; /** @var array Enabled content types. */ private $enabledcontenttypes = null; /** * Obtains the list of core_contentbank_content objects currently active. * * The list does not include players which are disabled. * * @return string[] Array of contentbank contenttypes. */ public function get_enabled_content_types(): array { if (!is_null($this->enabledcontenttypes)) { return $this->enabledcontenttypes; } $enabledtypes = \core\plugininfo\contenttype::get_enabled_plugins(); $types = []; foreach ($enabledtypes as $name) { $contenttypeclassname = "\\contenttype_$name\\contenttype"; $contentclassname = "\\contenttype_$name\\content"; if (class_exists($contenttypeclassname) && class_exists($contentclassname)) { $types[$contenttypeclassname] = $name; } } return $this->enabledcontenttypes = $types; } /** * Obtains an array of supported extensions by active plugins. * * @return array The array with all the extensions supported and the supporting plugin names. */ public function load_all_supported_extensions(): array { $extensionscache = \cache::make('core', 'contentbank_enabled_extensions'); $supportedextensions = $extensionscache->get('enabled_extensions'); if ($supportedextensions === false) { // Load all enabled extensions. $supportedextensions = []; foreach ($this->get_enabled_content_types() as $type) { $classname = "\\contenttype_$type\\contenttype"; $contenttype = new $classname; if ($contenttype->is_feature_supported($contenttype::CAN_UPLOAD)) { $extensions = $contenttype->get_manageable_extensions(); foreach ($extensions as $extension) { if (array_key_exists($extension, $supportedextensions)) { $supportedextensions[$extension][] = $type; } else { $supportedextensions[$extension] = [$type]; } } } } $extensionscache->set('enabled_extensions', $supportedextensions); } return $supportedextensions; } /** * Obtains an array of supported extensions in the given context. * * @param context $context Optional context to check (default null) * @return array The array with all the extensions supported and the supporting plugin names. */ public function load_context_supported_extensions(?context $context = null): array { $extensionscache = \cache::make('core', 'contentbank_context_extensions'); $contextextensions = $extensionscache->get($context->id); if ($contextextensions === false) { $contextextensions = []; $supportedextensions = $this->load_all_supported_extensions(); foreach ($supportedextensions as $extension => $types) { foreach ($types as $type) { $classname = "\\contenttype_$type\\contenttype"; $contenttype = new $classname($context); if ($contenttype->can_upload()) { $contextextensions[$extension] = $type; break; } } } $extensionscache->set($context->id, $contextextensions); } return $contextextensions; } /** * Obtains a string with all supported extensions by active plugins. * Mainly to use as filepicker options parameter. * * @param context $context Optional context to check (default null) * @return string A string with all the extensions supported. */ public function get_supported_extensions_as_string(?context $context = null) { $supported = $this->load_context_supported_extensions($context); $extensions = array_keys($supported); return implode(',', $extensions); } /** * Returns the file extension for a file. * * @param string $filename The name of the file * @return string The extension of the file */ public function get_extension(string $filename) { $dot = strrpos($filename, '.'); if ($dot === false) { return ''; } return strtolower(substr($filename, $dot)); } /** * Get the first content bank plugin supports a file extension. * * @param string $extension Content file extension * @param context $context $context Optional context to check (default null) * @return string contenttype name supports the file extension or null if the extension is not supported by any allowed plugin. */ public function get_extension_supporter(string $extension, ?context $context = null): ?string { $supporters = $this->load_context_supported_extensions($context); if (array_key_exists($extension, $supporters)) { return $supporters[$extension]; } return null; } /** * Find the contents with %$search% in the contextid defined. * If contextid and search are empty, all contents are returned. * In all the cases, only the contents for the enabled contentbank-type plugins are returned. * No content-type permissions are validated here. It is the caller responsability to check that the user can access to them. * The only validation done here is, for each content, a call to the method $content->is_view_allowed(). * * @param string|null $search Optional string to search (for now it will search only into the name). * @param int $contextid Optional contextid to search. * @param array $contenttypenames Optional array with the list of content-type names to search. * @return array The contents for the enabled contentbank-type plugins having $search as name and placed in $contextid. */ public function search_contents(?string $search = null, ?int $contextid = 0, ?array $contenttypenames = null): array { global $DB; $contents = []; // Get only contents for enabled content-type plugins. $contenttypes = []; $enabledcontenttypes = $this->get_enabled_content_types(); foreach ($enabledcontenttypes as $contenttypename) { if (empty($contenttypenames) || in_array($contenttypename, $contenttypenames)) { $contenttypes[] = "contenttype_$contenttypename"; } } if (empty($contenttypes)) { // Early return if there are no content-type plugins enabled. return $contents; } list($sqlcontenttypes, $params) = $DB->get_in_or_equal($contenttypes, SQL_PARAMS_NAMED); $sql = " contenttype $sqlcontenttypes "; // Filter contents on this context (if defined). if (!empty($contextid)) { $params['contextid'] = $contextid; $sql .= ' AND contextid = :contextid '; } // Search for contents having this string (if defined). if (!empty($search)) { $sql .= ' AND ' . $DB->sql_like('name', ':name', false, false); $params['name'] = '%' . $DB->sql_like_escape($search) . '%'; } $records = $DB->get_records_select('contentbank_content', $sql, $params, 'name ASC'); foreach ($records as $record) { $content = $this->get_content_from_id($record->id); if ($content->is_view_allowed()) { $contents[] = $content; } } return $contents; } /** * Return all the context where a user has all the given capabilities. * * @param string $capability The capability the user needs to have. * @param int|null $userid Optional userid. $USER by default. * @return array Array of the courses and course categories where the user has the given capability. */ public function get_contexts_with_capabilities_by_user($capability = 'moodle/contentbank:access', $userid = null): array { global $USER; if (!$userid) { $userid = $USER->id; } $categoriescache = \cache::make('core', 'contentbank_allowed_categories'); $coursescache = \cache::make('core', 'contentbank_allowed_courses'); $categories = $categoriescache->get($userid); $courses = $coursescache->get($userid); if ($categories === false || $courses === false) { // Required fields for preloading the context record. $contextfields = 'ctxid, ctxpath, ctxdepth, ctxlevel, ctxinstance, ctxlocked'; list($categories, $courses) = get_user_capability_contexts($capability, true, $userid, true, "fullname, {$contextfields}", "name, {$contextfields}", 'fullname', 'name'); $categoriescache->set($userid, $categories); $coursescache->set($userid, $courses); } return [$categories, $courses]; } /** * Create content from a file information. * * @param \context $context Context where to upload the file and content. * @param int $userid Id of the user uploading the file. * @param stored_file $file The file to get information from * @return content */ public function create_content_from_file(\context $context, int $userid, stored_file $file): ?content { global $USER; if (empty($userid)) { $userid = $USER->id; } // Get the contenttype to manage given file's extension. $filename = $file->get_filename(); $extension = $this->get_extension($filename); $plugin = $this->get_extension_supporter($extension, $context); $classname = '\\contenttype_'.$plugin.'\\contenttype'; $record = new \stdClass(); $record->name = $filename; $record->usercreated = $userid; $contentype = new $classname($context); $content = $contentype->upload_content($file, $record); $event = \core\event\contentbank_content_uploaded::create_from_record($content->get_content()); $event->trigger(); return $content; } /** * Delete content bank content by context. * * @param context $context The context to delete content from. * @return bool */ public function delete_contents(context $context): bool { global $DB; $result = true; $records = $DB->get_records('contentbank_content', ['contextid' => $context->id]); foreach ($records as $record) { $content = $this->get_content_from_id($record->id); $contenttype = $content->get_content_type_instance(); if (!$contenttype->delete_content($content)) { $result = false; } } return $result; } /** * Move content bank content from a context to another. * * @param context $from The context to get content from. * @param context $to The context to move content to. * @return bool */ public function move_contents(context $from, context $to): bool { global $DB; $result = true; $records = $DB->get_records('contentbank_content', ['contextid' => $from->id]); foreach ($records as $record) { $content = $this->get_content_from_id($record->id); $contenttype = $content->get_content_type_instance(); if (!$contenttype->move_content($content, $to)) { $result = false; } } return $result; } /** * Get the list of content types that have the requested feature. * * @param string $feature Feature code e.g CAN_UPLOAD. * @param null|\context $context Optional context to check the permission to use the feature. * @param bool $enabled Whether check only the enabled content types or all of them. * * @return string[] List of content types where the user has permission to access the feature. */ public function get_contenttypes_with_capability_feature(string $feature, ?\context $context = null, bool $enabled = true): array { $contenttypes = []; // Check enabled content types or all of them. if ($enabled) { $contenttypestocheck = $this->get_enabled_content_types(); } else { $plugins = core_plugin_manager::instance()->get_plugins_of_type('contenttype'); foreach ($plugins as $plugin) { $contenttypeclassname = "\\{$plugin->type}_{$plugin->name}\\contenttype"; $contenttypestocheck[$contenttypeclassname] = $plugin->name; } } foreach ($contenttypestocheck as $classname => $name) { $contenttype = new $classname($context); // The method names that check the features permissions must follow the pattern can_feature. if ($contenttype->{"can_$feature"}()) { $contenttypes[$classname] = $name; } } return $contenttypes; } /** * Return a content class form a content id. * * @throws coding_exception if the ID is not valid or some class does no exists * @param int $id the content id * @return content the content class instance */ public function get_content_from_id(int $id): content { global $DB; $record = $DB->get_record('contentbank_content', ['id' => $id], '*', MUST_EXIST); $contentclass = "\\$record->contenttype\\content"; return new $contentclass($record); } /** * Whether the context is allowed. * * @param context $context Context to check. * @return bool */ public function is_context_allowed(context $context): bool { return in_array($context->contextlevel, self::ALLOWED_CONTEXT_LEVELS); } } PK*\Dclasses/helper.phpnu[. /** * Content bank files repository helpers. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_contentbank; use repository_contentbank\browser\contentbank_browser; /** * Helper class for content bank files repository. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class helper { /** * Get the content bank repository browser for a certain context. * * @param \context $context The context * @return \repository_contentbank\browser\contentbank_browser|null The content bank repository browser */ public static function get_contentbank_browser(\context $context): ?contentbank_browser { switch ($context->contextlevel) { case CONTEXT_SYSTEM: return new \repository_contentbank\browser\contentbank_browser_context_system($context); case CONTEXT_COURSECAT: return new \repository_contentbank\browser\contentbank_browser_context_coursecat($context); case CONTEXT_COURSE: return new \repository_contentbank\browser\contentbank_browser_context_course($context); } return null; } /** * Create the context folder node. * * @param string $name The name of the context folder node * @param string $path The path to the context folder node * @return array The context folder node */ public static function create_context_folder_node(string $name, string $path): array { global $OUTPUT; return [ 'title' => $name, 'datemodified' => '', 'datecreated' => '', 'path' => $path, 'thumbnail' => $OUTPUT->image_url(file_folder_icon())->out(false), 'children' => [] ]; } /** * Create the content bank content node. * * @param \core_contentbank\content $content The content bank content * @return array|null The content bank content node */ public static function create_contentbank_content_node(\core_contentbank\content $content): ?array { global $OUTPUT; // Only content files are currently supported, but should be able to create content folder nodes in the future. // Early return if the content is not a stored file. if (!$file = $content->get_file()) { return null; } $params = [ 'contextid' => $file->get_contextid(), 'component' => $file->get_component(), 'filearea' => $file->get_filearea(), 'itemid' => $file->get_itemid(), 'filepath' => $file->get_filepath(), 'filename' => $file->get_filename() ]; $contenttype = $content->get_content_type_instance(); $encodedpath = base64_encode(json_encode($params)); $node = [ 'shorttitle' => $content->get_name(), 'title' => $file->get_filename(), 'datemodified' => $file->get_timemodified(), 'datecreated' => $file->get_timecreated(), 'author' => $file->get_author(), 'license' => $file->get_license(), 'isref' => $file->is_external_file(), 'size' => $file->get_filesize(), 'source' => $encodedpath, 'icon' => $contenttype->get_icon($content), 'thumbnail' => $contenttype->get_icon($content) ]; if ($file->get_status() == 666) { $node['originalmissing'] = true; } return $node; } /** * Generate a navigation node. * * @param \context $context The context * @return array The navigation node */ public static function create_navigation_node(\context $context): array { return [ 'path' => base64_encode(json_encode(['contextid' => $context->id])), 'name' => $context->get_context_name(false) ]; } } PK*\:06TTclasses/output/viewcontent.phpnu[. namespace core_contentbank\output; use context; use core_contentbank\content; use core_contentbank\contenttype; use moodle_url; use renderable; use renderer_base; use stdClass; use templatable; /** * Class containing data for the content view. * * @copyright 2020 Victor Deniz * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class viewcontent implements renderable, templatable { /** * @var contenttype Content bank content type. */ private $contenttype; /** * @var stdClass Record of the contentbank_content table. */ private $content; /** * Construct this renderable. * * @param contenttype $contenttype Content bank content type. * @param content $content Record of the contentbank_content table. */ public function __construct(contenttype $contenttype, content $content) { $this->contenttype = $contenttype; $this->content = $content; } /** * Get the content of the "More" dropdown in the tertiary navigation * * @return array|null The options to be displayed in a dropdown in the tertiary navigation * @throws \moodle_exception */ protected function get_edit_actions_dropdown(): ?array { global $PAGE; $options = []; if ($this->contenttype->can_manage($this->content)) { // Add the visibility item to the menu. switch($this->content->get_visibility()) { case content::VISIBILITY_UNLISTED: $visibilitylabel = get_string('visibilitysetpublic', 'core_contentbank'); $newvisibility = content::VISIBILITY_PUBLIC; break; case content::VISIBILITY_PUBLIC: $visibilitylabel = get_string('visibilitysetunlisted', 'core_contentbank'); $newvisibility = content::VISIBILITY_UNLISTED; break; default: $url = new \moodle_url('/contentbank/index.php', ['contextid' => $this->content->get_contextid()]); throw new moodle_exception('contentvisibilitynotfound', 'error', $url, $this->content->get_visibility()); } if ($visibilitylabel) { $options[$visibilitylabel] = [ 'data-action' => 'setcontentvisibility', 'data-visibility' => $newvisibility, 'data-contentid' => $this->content->get_id(), ]; } // Add the rename content item to the menu. $options[get_string('rename')] = [ 'data-action' => 'renamecontent', 'data-contentname' => $this->content->get_name(), 'data-contentid' => $this->content->get_id(), ]; if ($this->contenttype->can_upload()) { $options[get_string('replacecontent', 'contentbank')] = ['data-action' => 'upload']; $PAGE->requires->js_call_amd( 'core_contentbank/upload', 'initModal', [ '[data-action=upload]', \core_contentbank\form\upload_files::class, $this->content->get_contextid(), $this->content->get_id() ] ); } } if ($this->contenttype->can_download($this->content)) { $url = new moodle_url($this->contenttype->get_download_url($this->content)); $options[get_string('download')] = [ 'url' => $url->out() ]; } if ($this->contenttype->can_copy($this->content)) { // Add the copy content item to the menu. $options[get_string('copycontent', 'contentbank')] = [ 'data-action' => 'copycontent', 'data-contentname' => get_string('copyof', 'contentbank', $this->content->get_name()), 'data-contentid' => $this->content->get_id(), ]; } // Add the delete content item to the menu. if ($this->contenttype->can_delete($this->content)) { $options[get_string('delete')] = [ 'data-action' => 'deletecontent', 'data-contentname' => $this->content->get_name(), 'data-uses' => count($this->content->get_uses()), 'data-contentid' => $this->content->get_id(), 'data-contextid' => $this->content->get_contextid(), 'class' => 'text-danger', ]; } $dropdown = []; if ($options) { foreach ($options as $key => $attribs) { $url = $attribs['url'] ?? '#'; $extraclasses = $attribs['class'] ?? ''; $dropdown['options'][] = [ 'label' => $key, 'url' => $url, 'extraclasses' => $extraclasses, 'attributes' => array_map(function ($key, $value) { return [ 'name' => $key, 'value' => $value ]; }, array_keys($attribs), $attribs) ]; } } return $dropdown; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output * * @return stdClass */ public function export_for_template(renderer_base $output): stdClass { $data = new stdClass(); // Get the content type html. $contenthtml = $this->contenttype->get_view_content($this->content); $data->contenthtml = $contenthtml; $handler = \core_contentbank\customfield\content_handler::create(); $customfields = $handler->get_instance_data($this->content->get_id()); $data->customfieldshtml = $handler->display_custom_fields_data($customfields); // Check if the user can edit this content type. if ($this->contenttype->can_edit($this->content)) { $data->usercanedit = true; $urlparams = [ 'contextid' => $this->content->get_contextid(), 'plugin' => $this->contenttype->get_plugin_name(), 'id' => $this->content->get_id() ]; $editcontenturl = new moodle_url('/contentbank/edit.php', $urlparams); $data->editcontenturl = $editcontenturl->out(false); } // Close/exit link for those users who can access that context. $context = context::instance_by_id($this->content->get_contextid()); if (has_capability('moodle/contentbank:access', $context)) { $closeurl = new moodle_url('/contentbank/index.php', ['contextid' => $context->id]); $data->closeurl = $closeurl->out(false); } $data->actionmenu = $this->get_edit_actions_dropdown(); $data->heading = $this->content->get_name(); if ($this->content->get_visibility() == content::VISIBILITY_UNLISTED) { $data->heading = get_string('visibilitytitleunlisted', 'contentbank', $data->heading); } return $data; } } PK*\<;""classes/output/bankcontent.phpnu[. namespace core_contentbank\output; use core\context\{course, coursecat}; use core\context_helper; use core_contentbank\content; use core_contentbank\contentbank; use renderable; use templatable; use renderer_base; use stdClass; /** * Class containing data for bank content * * @package core_contentbank * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class bankcontent implements renderable, templatable { /** * @var \core_contentbank\content[] Array of content bank contents. */ private $contents; /** * @var array $toolbar object. */ private $toolbar; /** * @var \context Given context. Null by default. */ private $context; /** * @var array Course categories that the user has access to. */ private $allowedcategories; /** * @var array Courses that the user has access to. */ private $allowedcourses; /** * Construct this renderable. * * @param \core_contentbank\content[] $contents Array of content bank contents. * @param array $toolbar List of content bank toolbar options. * @param \context|null $context Optional context to check (default null) * @param contentbank $cb Contenbank object. */ public function __construct(array $contents, array $toolbar, ?\context $context, contentbank $cb) { $this->contents = $contents; $this->toolbar = $toolbar; $this->context = $context; list($this->allowedcategories, $this->allowedcourses) = $cb->get_contexts_with_capabilities_by_user(); } /** * Export the data. * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output): stdClass { global $PAGE, $SITE; $PAGE->requires->js_call_amd('core_contentbank/search', 'init'); $PAGE->requires->js_call_amd('core_contentbank/sort', 'init'); $data = new stdClass(); $contentdata = array(); foreach ($this->contents as $content) { $file = $content->get_file(); $filesize = $file ? $file->get_filesize() : 0; $mimetype = $file ? get_mimetype_description($file) : ''; $contenttypeclass = $content->get_content_type().'\\contenttype'; $contenttype = new $contenttypeclass($this->context); if ($content->get_visibility() == content::VISIBILITY_UNLISTED) { $name = get_string('visibilitytitleunlisted', 'contentbank', $content->get_name()); } else { $name = $content->get_name(); } $author = \core_user::get_user($content->get_content()->usercreated); $contentdata[] = array( 'name' => $name, 'title' => strtolower($name), 'link' => $contenttype->get_view_url($content), 'icon' => $contenttype->get_icon($content), 'uses' => count($content->get_uses()), 'timemodified' => $content->get_timemodified(), 'bytes' => $filesize, 'size' => display_size($filesize), 'type' => $mimetype, 'author' => fullname($author), 'visibilityunlisted' => $content->get_visibility() == content::VISIBILITY_UNLISTED ); } $data->viewlist = get_user_preferences('core_contentbank_view_list'); $data->contents = $contentdata; // The tools are displayed in the action bar on the index page. foreach ($this->toolbar as $tool) { // Customize the output of a tool, like dropdowns. $method = 'export_tool_'.$tool['action']; if (method_exists($this, $method)) { $this->$method($tool); } else { $this->export_tool_default($tool); } $data->tools[] = $tool; } $allowedcontexts = []; $systemcontext = \context_system::instance(); if (has_capability('moodle/contentbank:access', $systemcontext)) { $allowedcontexts[$systemcontext->id] = get_string('coresystem'); } $options = []; foreach ($this->allowedcategories as $allowedcategory) { context_helper::preload_from_record(clone $allowedcategory); $options[$allowedcategory->ctxid] = format_string($allowedcategory->name, true, [ 'context' => coursecat::instance($allowedcategory->ctxinstance), ]); } if (!empty($options)) { $allowedcontexts['categories'] = [get_string('coursecategories') => $options]; } $options = []; foreach ($this->allowedcourses as $allowedcourse) { // Don't add the frontpage course to the list. if ($allowedcourse->id != $SITE->id) { context_helper::preload_from_record(clone $allowedcourse); $options[$allowedcourse->ctxid] = format_string($allowedcourse->fullname, true, [ 'context' => course::instance($allowedcourse->ctxinstance), ]); } } if (!empty($options)) { $allowedcontexts['courses'] = [get_string('courses') => $options]; } if (!empty($allowedcontexts)) { $strchoosecontext = get_string('choosecontext', 'core_contentbank'); $singleselect = new \single_select( new \moodle_url('/contentbank/index.php'), 'contextid', $allowedcontexts, $this->context->id, $strchoosecontext ); $singleselect->set_label($strchoosecontext, ['class' => 'visually-hidden']); $data->allowedcontexts = $singleselect->export_for_template($output); } return $data; } /** * Adds the content type items to display to the Add dropdown. * * Each content type is represented as an object with the properties: * - name: the name of the content type. * - baseurl: the base content type editor URL. * - types: different types of the content type to display as dropdown items. * * @param array $tool Data for rendering the Add dropdown, including the editable content types. */ private function export_tool_add(array &$tool) { $editabletypes = $tool['contenttypes']; $addoptions = []; foreach ($editabletypes as $class => $type) { $contentype = new $class($this->context); // Get the creation options of each content type. $types = $contentype->get_contenttype_types(); if ($types) { // Add a text describing the content type as first option. This will be displayed in the drop down to // separate the options for the different content types. $contentdesc = new stdClass(); $contentdesc->typename = get_string('description', $contentype->get_contenttype_name()); array_unshift($types, $contentdesc); // Context data for the template. $addcontenttype = new stdClass(); // Content type name. $addcontenttype->name = $type; // Content type editor base URL. $tool['link']->param('plugin', $type); $addcontenttype->baseurl = $tool['link']->out(); // Different types of the content type. $addcontenttype->types = $types; $addoptions[] = $addcontenttype; } } $tool['contenttypes'] = $addoptions; } /** * This is the default output of a tool. * It will be displayed as a button by default. * * @param array $tool Data for rendering the Add dropdown, including the editable content types. * @return void */ private function export_tool_default(array &$tool): void { if (empty($tool['checkbox']) && empty($tool['dropdown'])) { $tool['button'] = true; } } } PK*\z!mmclasses/output/customfields.phpnu[. namespace core_contentbank\output; use renderable; use templatable; use renderer_base; use core_contentbank\content; /** * Content bank Custom fields renderable class. * * @package core_contentbank * @copyright 2024 Daniel Neis Araujo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class customfields implements renderable, templatable { /** * Constructor. * * @param \core_contentbank\content $content The content object. */ public function __construct(content $content) { $this->content = $content; } /** * Export the data. * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output) { global $DB; $context = new \stdClass(); $context->url = $this->content->get_file_url(); $context->name = $this->content->get_name(); $handler = \core_contentbank\customfield\content_handler::create(); $customfields = $handler->get_instance_data($this->content->get_id()); $context->data = $handler->display_custom_fields_data($customfields); return $context; } } PK*\ENNclasses/contenttype.phpnu[. namespace core_contentbank; use core\event\contentbank_content_created; use core\event\contentbank_content_deleted; use core\event\contentbank_content_viewed; use stored_file; use moodle_url; /** * Content type manager class * * @package core_contentbank * @copyright 2020 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class contenttype { /** @var string Constant representing whether the plugin implements uploading feature */ const CAN_UPLOAD = 'upload'; /** @var string Constant representing whether the plugin implements edition feature */ const CAN_EDIT = 'edit'; /** * @var string Constant representing whether the plugin implements download feature * @since Moodle 3.10 */ const CAN_DOWNLOAD = 'download'; /** * @var string Constant representing whether the plugin implements copy feature * @since Moodle 4.3 */ const CAN_COPY = 'copy'; /** @var \context This contenttype's context. **/ protected $context = null; /** * Content type constructor * * @param \context $context Optional context to check (default null) */ public function __construct(?\context $context = null) { if (empty($context)) { $context = \context_system::instance(); } $this->context = $context; } /** * Fills content_bank table with appropiate information. * * @throws dml_exception A DML specific exception is thrown for any creation error. * @param \stdClass $record An optional content record compatible object (default null) * @return content Object with content bank information. */ public function create_content(?\stdClass $record = null): content { global $USER, $DB, $CFG; $entry = new \stdClass(); if (isset($record->visibility)) { $entry->visibility = $record->visibility; } else { $usercreated = $record->usercreated ?? $USER->id; $entry->visibility = get_user_preferences('core_contentbank_visibility', $CFG->defaultpreference_core_contentbank_visibility, $usercreated); } $entry->contenttype = $this->get_contenttype_name(); $entry->contextid = $this->context->id; $entry->name = $record->name ?? ''; $entry->usercreated = $record->usercreated ?? $USER->id; $entry->timecreated = time(); $entry->usermodified = $entry->usercreated; $entry->timemodified = $entry->timecreated; $entry->configdata = $record->configdata ?? ''; $entry->instanceid = $record->instanceid ?? 0; $entry->id = $DB->insert_record('contentbank_content', $entry); $classname = '\\'.$entry->contenttype.'\\content'; $content = new $classname($entry); // Trigger an event for creating the content. $event = contentbank_content_created::create_from_record($content->get_content()); $event->trigger(); return $content; } /** * Create a new content from an uploaded file. * * @throws file_exception If file operations fail * @throws dml_exception if the content creation fails * @param stored_file $file the uploaded file * @param \stdClass|null $record an optional content record * @return content Object with content bank information. */ public function upload_content(stored_file $file, ?\stdClass $record = null): content { if (empty($record)) { $record = new \stdClass(); $record->name = $file->get_filename(); } $content = $this->create_content($record); try { $content->import_file($file); } catch (\moodle_exception $e) { $this->delete_content($content); throw new \moodle_exception($e->errorcode); } return $content; } /** * Replace a content using an uploaded file. * * @throws file_exception If file operations fail * @throws dml_exception if the content creation fails * @param stored_file $file the uploaded file * @param content $content the original content record * @return content Object with the updated content bank information. */ public function replace_content(stored_file $file, content $content): content { $content->import_file($file); $content->update_content(); return $content; } /** * Delete this content from the content_bank. * This method can be overwritten by the plugins if they need to delete specific information. * * @param content $content The content to delete. * @return boolean true if the content has been deleted; false otherwise. */ public function delete_content(content $content): bool { global $DB; // Delete the file if it exists. if ($file = $content->get_file()) { $file->delete(); } // Delete the contentbank DB entry. $result = $DB->delete_records('contentbank_content', ['id' => $content->get_id()]); if ($result) { // Trigger an event for deleting this content. $record = $content->get_content(); $event = contentbank_content_deleted::create([ 'objectid' => $content->get_id(), 'relateduserid' => $record->usercreated, 'context' => \context::instance_by_id($record->contextid), 'other' => [ 'contenttype' => $content->get_content_type(), 'name' => $content->get_name() ] ]); $event->add_record_snapshot('contentbank_content', $record); $event->trigger(); } return $result; } /** * Rename this content from the content_bank. * This method can be overwritten by the plugins if they need to change some other specific information. * * @param content $content The content to rename. * @param string $name The name of the content. * @return boolean true if the content has been renamed; false otherwise. */ public function rename_content(content $content, string $name): bool { return $content->set_name($name); } /** * Move content to another context. * This method can be overwritten by the plugins if they need to change some other specific information. * * @param content $content The content to rename. * @param \context $context The new context. * @return boolean true if the content has been renamed; false otherwise. */ public function move_content(content $content, \context $context): bool { return $content->set_contextid($context->id); } /** * Returns the contenttype name of this content. * * @return string Content type of the current instance */ public function get_contenttype_name(): string { $classname = get_class($this); $contenttype = explode('\\', $classname); return array_shift($contenttype); } /** * Returns the plugin name of the current instance. * * @return string Plugin name of the current instance */ public function get_plugin_name(): string { $contenttype = $this->get_contenttype_name(); $plugin = explode('_', $contenttype); return array_pop($plugin); } /** * Returns the URL where the content will be visualized. * * @param content $content The content to be displayed. * @return string URL where to visualize the given content. */ public function get_view_url(content $content): string { return new moodle_url('/contentbank/view.php', ['id' => $content->get_id()]); } /** * Returns the HTML content to add to view.php visualizer. * * @param content $content The content to be displayed. * @return string HTML code to include in view.php. */ public function get_view_content(content $content): string { global $PAGE; // Trigger an event for viewing this content. $event = contentbank_content_viewed::create_from_record($content->get_content()); $event->trigger(); if ($content->has_custom_fields()) { $renderer = $PAGE->get_renderer('core'); $renderable = new \core_contentbank\output\customfields($content); return $renderer->render($renderable); } return ''; } /** * Returns the URL to download the content. * * @since Moodle 3.10 * @param content $content The content to be downloaded. * @return string URL with the content to download. */ public function get_download_url(content $content): string { $downloadurl = ''; $file = $content->get_file(); if (!empty($file)) { $url = \moodle_url::make_pluginfile_url( $file->get_contextid(), $file->get_component(), $file->get_filearea(), $file->get_itemid(), $file->get_filepath(), $file->get_filename() ); $downloadurl = $url->out(false); } return $downloadurl; } /** * Returns the HTML code to render the icon for content bank contents. * * @param content $content The content to be displayed. * @return string HTML code to render the icon */ public function get_icon(content $content): string { global $OUTPUT; return $OUTPUT->image_url('f/unknown')->out(false); } /** * Returns user has access capability for the main content bank and the content itself (base on is_access_allowed from plugin). * * @return bool True if content could be accessed. False otherwise. */ final public function can_access(): bool { $classname = 'contenttype/'.$this->get_plugin_name(); $capability = $classname.":access"; $hascapabilities = has_capability('moodle/contentbank:access', $this->context) && has_capability($capability, $this->context); return $hascapabilities && $this->is_access_allowed(); } /** * Returns user has access capability for the content itself. * * @return bool True if content could be accessed. False otherwise. */ protected function is_access_allowed(): bool { // Plugins can overwrite this function to add any check they need. return true; } /** * Returns the user has permission to upload new content. * * @return bool True if content could be uploaded. False otherwise. */ final public function can_upload(): bool { if (!$this->is_feature_supported(self::CAN_UPLOAD)) { return false; } if (!$this->can_access()) { return false; } $classname = 'contenttype/'.$this->get_plugin_name(); $uploadcap = $classname.':upload'; $hascapabilities = has_capability('moodle/contentbank:upload', $this->context) && has_capability($uploadcap, $this->context); return $hascapabilities && $this->is_upload_allowed(); } /** * Returns plugin allows uploading. * * @return bool True if plugin allows uploading. False otherwise. */ protected function is_upload_allowed(): bool { // Plugins can overwrite this function to add any check they need. return true; } /** * Check if the user can delete this content. * * @param content $content The content to be deleted. * @return bool True if content could be uploaded. False otherwise. */ final public function can_delete(content $content): bool { global $USER; if ($this->context->id != $content->get_content()->contextid) { // The content has to have exactly the same context as this contenttype. return false; } $hascapability = has_capability('moodle/contentbank:deleteanycontent', $this->context); if ($content->get_content()->usercreated == $USER->id) { // This content has been created by the current user; check if she can delete her content. $hascapability = $hascapability || has_capability('moodle/contentbank:deleteowncontent', $this->context); } return $hascapability && $this->is_delete_allowed($content); } /** * Returns if content allows deleting. * * @param content $content The content to be deleted. * @return bool True if content allows uploading. False otherwise. */ protected function is_delete_allowed(content $content): bool { // Plugins can overwrite this function to add any check they need. return true; } /** * Check if the user can managed this content. * * @param content $content The content to be managed. * @return bool True if content could be managed. False otherwise. */ final public function can_manage(content $content): bool { global $USER; if ($this->context->id != $content->get_content()->contextid) { // The content has to have exactly the same context as this contenttype. return false; } // Check main contentbank management permission. $hascapability = has_capability('moodle/contentbank:manageanycontent', $this->context); if ($content->get_content()->usercreated == $USER->id) { // This content has been created by the current user; check if they can manage their content. $hascapability = $hascapability || has_capability('moodle/contentbank:manageowncontent', $this->context); } return $hascapability && $this->is_manage_allowed($content); } /** * Returns if content allows managing. * * @param content $content The content to be managed. * @return bool True if content allows uploading. False otherwise. */ protected function is_manage_allowed(content $content): bool { // Plugins can overwrite this function to add any check they need. return true; } /** * Returns whether or not the user has permission to use the editor. * This function will be called with the content to be edited as parameter, * or null when is checking permission to create a new content using the editor. * * @param content $content The content to be edited or null when creating a new content. * @return bool True if the user can edit content. False otherwise. */ final public function can_edit(?content $content = null): bool { if (!$this->is_feature_supported(self::CAN_EDIT)) { return false; } if (!$this->can_access()) { return false; } if (!is_null($content) && !$this->can_manage($content)) { return false; } $classname = 'contenttype/'.$this->get_plugin_name(); $editioncap = $classname.':useeditor'; $hascapabilities = has_all_capabilities(['moodle/contentbank:useeditor', $editioncap], $this->context); return $hascapabilities && $this->is_edit_allowed($content); } /** * Returns plugin allows edition. * * @param content $content The content to be edited. * @return bool True if plugin allows edition. False otherwise. */ protected function is_edit_allowed(?content $content): bool { // Plugins can overwrite this function to add any check they need. return true; } /** * Returns whether or not the user has permission to download the content. * * @since Moodle 3.10 * @param content $content The content to be downloaded. * @return bool True if the user can download the content. False otherwise. */ final public function can_download(content $content): bool { if (!$this->is_feature_supported(self::CAN_DOWNLOAD)) { return false; } if (!$this->can_access()) { return false; } $hascapability = has_capability('moodle/contentbank:downloadcontent', $this->context); return $hascapability && $this->is_download_allowed($content); } /** * Returns whether or not the user has permission to copy the content. * * @since Moodle 4.3 * @param content $content The content to be copied. * @return bool True if the user can copy the content. False otherwise. */ final public function can_copy(content $content): bool { global $USER; if (!$this->is_feature_supported(self::CAN_COPY)) { return false; } if (!$this->can_access()) { return false; } if (!$this->is_copy_allowed($content)) { return false; } $hascapability = has_capability('moodle/contentbank:copyanycontent', $this->context); if (!$hascapability && ($content->get_content()->usercreated == $USER->id)) { $hascapability = has_capability('moodle/contentbank:copycontent', $this->context); } return $hascapability; } /** * Returns plugin allows downloading. * * @since Moodle 3.10 * @param content $content The content to be downloaed. * @return bool True if plugin allows downloading. False otherwise. */ protected function is_download_allowed(content $content): bool { // Plugins can overwrite this function to add any check they need. return true; } /** * Returns plugin allows copying. * * @since Moodle 4.3 * @param content $content The content to be copied. * @return bool True if plugin allows copying. False otherwise. */ protected function is_copy_allowed(content $content): bool { // Plugins can overwrite this function to add any check they need. return true; } /** * Returns the plugin supports the feature. * * @param string $feature Feature code e.g CAN_UPLOAD * @return bool True if content could be uploaded. False otherwise. */ final public function is_feature_supported(string $feature): bool { return in_array($feature, $this->get_implemented_features()); } /** * Return an array of implemented features by the plugins. * * @return array */ abstract protected function get_implemented_features(): array; /** * Return an array of extensions the plugins could manage. * * @return array */ abstract public function get_manageable_extensions(): array; /** * Returns the list of different types of the given content type. * * A content type can have one or more options for creating content. This method will report all of them or only the content * type itself if it has no other options. * * @return array An object for each type: * - string typename: descriptive name of the type. * - string typeeditorparams: params required by this content type editor. * - url typeicon: this type icon. */ abstract public function get_contenttype_types(): array; } PK*\+_55classes/content.phpnu[. /** * Content manager class * * @package core_contentbank * @copyright 2020 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_contentbank; use core_text; use stored_file; use stdClass; use coding_exception; use context; use moodle_url; use core\event\contentbank_content_updated; /** * Content manager class * * @package core_contentbank * @copyright 2020 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class content { /** * @var int Visibility value. Public content is visible to all users with access to the content bank of the * appropriate context. */ public const VISIBILITY_PUBLIC = 1; /** * @var int Visibility value. Unlisted content is only visible to the author and to users with * moodle/contentbank:viewunlistedcontent capability. */ public const VISIBILITY_UNLISTED = 2; /** @var stdClass $content The content of the current instance. **/ protected $content = null; /** * Content bank constructor * * @param stdClass $record A contentbank_content record. * @throws coding_exception If content type is not right. */ public function __construct(stdClass $record) { // Content type should exist and be linked to plugin classname. $classname = $record->contenttype.'\\content'; if (get_class($this) != $classname) { throw new coding_exception(get_string('contenttypenotfound', 'error', $record->contenttype)); } $typeclass = $record->contenttype.'\\contenttype'; if (!class_exists($typeclass)) { throw new coding_exception(get_string('contenttypenotfound', 'error', $record->contenttype)); } // A record with the id must exist in 'contentbank_content' table. // To improve performance, we are only checking the id is set, but no querying the database. if (!isset($record->id)) { throw new coding_exception(get_string('invalidcontentid', 'error')); } $this->content = $record; } /** * Returns $this->content. * * @return stdClass $this->content. */ public function get_content(): stdClass { return $this->content; } /** * Returns $this->content->contenttype. * * @return string $this->content->contenttype. */ public function get_content_type(): string { return $this->content->contenttype; } /** * Return the contenttype instance of this content. * * @return contenttype The content type instance */ public function get_content_type_instance(): contenttype { $context = context::instance_by_id($this->content->contextid); $contenttypeclass = "\\{$this->content->contenttype}\\contenttype"; return new $contenttypeclass($context); } /** * Returns $this->content->timemodified. * * @return int $this->content->timemodified. */ public function get_timemodified(): int { return $this->content->timemodified; } /** * Updates content_bank table with information in $this->content. * * @return boolean True if the content has been succesfully updated. False otherwise. * @throws \coding_exception if not loaded. */ public function update_content(): bool { global $USER, $DB; // A record with the id must exist in 'contentbank_content' table. // To improve performance, we are only checking the id is set, but no querying the database. if (!isset($this->content->id)) { throw new coding_exception(get_string('invalidcontentid', 'error')); } $this->content->usermodified = $USER->id; $this->content->timemodified = time(); $result = $DB->update_record('contentbank_content', $this->content); if ($result) { // Trigger an event for updating this content. $event = contentbank_content_updated::create_from_record($this->content); $event->trigger(); } return $result; } /** * Set a new name to the content. * * @param string $name The name of the content. * @return bool True if the content has been succesfully updated. False otherwise. * @throws \coding_exception if not loaded. */ public function set_name(string $name): bool { $name = trim($name); if ($name === '') { return false; } // Clean name. $name = clean_param($name, PARAM_TEXT); if (core_text::strlen($name) > 255) { $name = core_text::substr($name, 0, 255); } $oldname = $this->content->name; $this->content->name = $name; $updated = $this->update_content(); if (!$updated) { $this->content->name = $oldname; } return $updated; } /** * Returns the name of the content. * * @return string The name of the content. */ public function get_name(): string { return $this->content->name; } /** * Set a new contextid to the content. * * @param int $contextid The new contextid of the content. * @return bool True if the content has been succesfully updated. False otherwise. */ public function set_contextid(int $contextid): bool { if ($this->content->contextid == $contextid) { return true; } $oldcontextid = $this->content->contextid; $this->content->contextid = $contextid; $updated = $this->update_content(); if ($updated) { // Move files to new context $fs = get_file_storage(); $fs->move_area_files_to_new_context($oldcontextid, $contextid, 'contentbank', 'public', $this->content->id); } else { $this->content->contextid = $oldcontextid; } return $updated; } /** * Returns the contextid of the content. * * @return int The id of the content context. */ public function get_contextid(): string { return $this->content->contextid; } /** * Returns the content ID. * * @return int The content ID. */ public function get_id(): int { return $this->content->id; } /** * Change the content instanceid value. * * @param int $instanceid New instanceid for this content * @return boolean True if the instanceid has been succesfully updated. False otherwise. */ public function set_instanceid(int $instanceid): bool { $this->content->instanceid = $instanceid; return $this->update_content(); } /** * Returns the $instanceid of this content. * * @return int contentbank instanceid */ public function get_instanceid(): int { return $this->content->instanceid; } /** * Change the content config values. * * @param string $configdata New config information for this content * @return boolean True if the configdata has been succesfully updated. False otherwise. */ public function set_configdata(string $configdata): bool { $this->content->configdata = $configdata; return $this->update_content(); } /** * Return the content config values. * * @return mixed Config information for this content (json decoded) */ public function get_configdata() { return $this->content->configdata; } /** * Sets a new content visibility and saves it to database. * * @param int $visibility Must be self::PUBLIC or self::UNLISTED * @return bool * @throws coding_exception */ public function set_visibility(int $visibility): bool { if (!in_array($visibility, [self::VISIBILITY_PUBLIC, self::VISIBILITY_UNLISTED])) { return false; } $this->content->visibility = $visibility; return $this->update_content(); } /** * Return true if the content may be shown to other users in the content bank. * * @return boolean */ public function get_visibility(): int { return $this->content->visibility; } /** * Import a file as a valid content. * * By default, all content has a public file area to interact with the content bank * repository. This method should be overridden by contentypes which does not simply * upload to the public file area. * * If any, the method will return the final stored_file. This way it can be invoked * as parent::import_file in case any plugin want to store the file in the public area * and also parse it. * * @param stored_file $file File to store in the content file area. * @return stored_file|null the stored content file or null if the file is discarted. */ public function import_file(stored_file $file): ?stored_file { $originalfile = $this->get_file(); if ($originalfile) { $originalfile->replace_file_with($file); return $originalfile; } else { $fs = get_file_storage(); $filerecord = [ 'contextid' => $this->get_contextid(), 'component' => 'contentbank', 'filearea' => 'public', 'itemid' => $this->get_id(), 'filepath' => '/', 'filename' => $file->get_filename(), 'timecreated' => time(), ]; return $fs->create_file_from_storedfile($filerecord, $file); } } /** * Returns the $file related to this content. * * @return stored_file File stored in content bank area related to the given itemid. * @throws \coding_exception if not loaded. */ public function get_file(): ?stored_file { $itemid = $this->get_id(); $fs = get_file_storage(); $files = $fs->get_area_files( $this->content->contextid, 'contentbank', 'public', $itemid, 'itemid, filepath, filename', false ); if (!empty($files)) { $file = reset($files); return $file; } return null; } /** * Returns the places where the file associated to this content is used or an empty array if the content has no file. * * @return array of stored_file where current file content is used or empty array if it hasn't any file. * @since 3.11 */ public function get_uses(): ?array { $references = []; $file = $this->get_file(); if ($file != null) { $fs = get_file_storage(); $references = $fs->get_references_by_storedfile($file); } return $references; } /** * Returns the file url related to this content. * * @return string URL of the file stored in content bank area related to the given itemid. * @throws \coding_exception if not loaded. */ public function get_file_url(): string { if (!$file = $this->get_file()) { return ''; } $fileurl = moodle_url::make_pluginfile_url( $this->content->contextid, 'contentbank', 'public', $file->get_itemid(), $file->get_filepath(), $file->get_filename() ); return $fileurl; } /** * Returns user has access permission for the content itself (based on what plugin needs). * * @return bool True if content could be accessed. False otherwise. */ public function is_view_allowed(): bool { // Plugins can overwrite this method in case they want to check something related to content properties. global $USER; $context = \context::instance_by_id($this->get_contextid()); $displaypreference = get_user_preferences('core_contentbank_displayunlisted', 1); if (($this->get_visibility() == self::VISIBILITY_UNLISTED) && !$displaypreference) { return false; } return $USER->id == $this->content->usercreated || $this->get_visibility() == self::VISIBILITY_PUBLIC || has_capability('moodle/contentbank:viewunlistedcontent', $context); } /** * Checks if there are any custom field related to this content. * * @return bool True if there is at least one populated field. */ public function has_custom_fields(): bool { $handler = \core_contentbank\customfield\content_handler::create(); return !empty($handler->get_instance_data($this->get_id())); } } PK*\IsIItemplates/copycontent.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 core_contentbank/copycontent Example context (json): { "contentid": 17, "name": "Accordion" } }}
PK*\Jtemplates/viewcontent.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 comments. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template core_contentbank/viewcontent View content page. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * contenthtml - string - content html. * usercanedit - boolean - whether the user has permission to edit the content. * editcontenturl - string - edit page URL. * closeurl - string - close landing page. Example context (json): { "contenthtml" : "", "usercanedit" : true, "editcontenturl" : "http://something/contentbank/edit.php?contextid=1&plugin=h5p&id=1", "closeurl" : "http://moodle.test/h5pcb/moodle/contentbank/index.php", "customfieldshtml" : "custom fields html" } }}
{{>core_contentbank/viewcontent/toolbarview}}
{{{ contenthtml }}}
{{{ customfieldshtml }}}
PK*\T *templates/viewcontent/toolbarview.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 comments. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template core_contentbank/viewcontent/toolbarview Contentbank view toolbar. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * contenthtml - string - content html. * usercanedit - boolean - whether the user has permission to edit the content. * editcontenturl - string - edit page URL. * closeurl - string - close landing page. Example context (json): { "usercanedit" : true, "heading" : "This is a heading", "editcontenturl" : "http://something/contentbank/edit.php?contextid=1&plugin=h5p&id=1", "closeurl" : "http://moodle.test/h5pcb/moodle/contentbank/index.php", "actionmenu": { "options": [ { "url": "www.google.com", "label": "Google", "attributes": [ { "name": "data-attrib", "value": "1" } ] } ] } } }} {{#heading}}

{{heading}}

{{/heading}}
{{#usercanedit}} {{/usercanedit}} {{#actionmenu}}
{{> core_contentbank/contentbankmenu }}
{{/actionmenu}} {{#closeurl}} {{/closeurl}}
PK*\Y,yy"templates/contentbankmenu.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 comments. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template core_contentbank/contentbankmenu Contentbank view toolbar. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * options - JSON object - the options available in the dropdown Example context (json): { "options": [ { "url": "www.google.com", "label": "Google", "attributes": [ { "name": "data-attrib", "value": "1" } ] } ] } }} PK*\ 89191templates/bankcontent.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 core_contentbank/bankcontent Example context (json): { "contents": [ { "name": "Accordion.h5p", "title": "accordion.h5p", "timemodified": 1589792272, "size": "699.3KB", "bytes": 716126, "type": "Archive (H5P)", "author": "Admin user", "link": "http://something/contentbank/contenttype/h5p/view.php?url=http://something/pluginfile.php/1/contentbank/public/accordion.h5p", "icon" : "http://something/theme/image.php/boost/core/1581597850/f/h5p-64", "visibilityunlisted": true }, { "name": "resume.pdf", "title": "resume", "timemodified": 1589792039, "size": "699.3KB", "uses": 3, "bytes": 716126, "type": "Archive (PDF)", "author": "Admin user", "icon": "http://something/theme/image.php/boost/core/1584597850/f/pdf-64", "visibilityunlisted": false } ], "tools": [ { "name": "Add", "dropdown": true, "link": "http://something/contentbank/edit.php?contextid=1", "contenttypes": [ { "name": "H5P Interactive Content", "baseurl": "http://something/contentbank/edit.php?contextid=1&plugin=h5p", "types": [ { "typename": "H5P Interactive Content" }, { "typename": "Accordion", "typeeditorparams": "library=Accordion-1.4", "typeicon": "http://something/pluginfile.php/1/core_h5p/libraries/13/H5P.Accordion-1.4/icon.svg" } ] } ] }, { "name": "Upload", "link": "http://something/contentbank/contenttype/h5p/view.php?url=http://something/pluginfile.php/1/contentbank/public/accordion.h5p", "icon" : "i/upload" }, { "icon": "i/export" } ], "allowedcontexts": [ { "name": "contextid", "method": "get", "action": "http://localhost/stable_master/contentbank/index.php", "options": [ { "value": "1", "name": "System", "selected": true, "optgroup": false }, { "value": "32", "name": "Category 1", "selected": false, "optgroup": false } ] } ] } }}
{{>core_contentbank/bankcontent/search}}
{{>core_contentbank/bankcontent/toolbar}}
{{>core_contentbank/bankcontent/navigation}}
{{#pix}} i/folder {{/pix}}
{{#contents.0}}
{{#contents}}
{{ uses }}
{{#userdate}} {{ timemodified }}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}} {{/userdate}}
{{ size }}
{{{ type }}}
{{{ author }}}
{{/contents}}
{{/contents.0}} {{^contents.0}}
{{#str}} nocontentavailable, core_contentbank {{/str}}
{{/contents.0}}
PK*\fY* * /templates/bankcontent/toolbar_dropdown.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 core_contentbank/bankcontent/toolbar_dropdown Example context (json): { "name": "Add", "dropdown": true, "link": "http://something/contentbank/edit.php?contextid=1", "contenttypes": [ { "name": "h5p", "baseurl": "http://something/contentbank/edit.php?contextid=1&plugin=h5p", "types": [ { "typename": "H5P Interactive Content" }, { "typename": "Accordion", "typeeditorparams": "library=Accordion-1.4", "typeicon": "http://something/pluginfile.php/1/core_h5p/libraries/13/H5P.Accordion-1.4/icon.svg" } ] } ] } }}
PK*\U%templates/bankcontent/search.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 core_contentbank/bankcontent/search Example context (json): { } }} {{< core/search_input_auto }} {{$label}}{{#str}} searchcontentbankbyname, contentbank {{/str}}{{/label}} {{$placeholder}}{{#str}} search, core {{/str}}{{/placeholder}} {{/ core/search_input_auto }} PK*\LQff)templates/bankcontent/navigation.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 core_contentbank/bankcontent/navigation Example context (json): { } }} {{#allowedcontexts}} {{> core/single_select }} {{/allowedcontexts}} PK*\`,v &templates/bankcontent/toolbar.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 core_contentbank/bankcontent/toolbar Example context (json): { "tools": [ { "name": "Add", "dropdown": true, "link": "http://something/contentbank/edit.php?contextid=1", "contenttypes": [ { "name": "h5p", "baseurl": "http://something/contentbank/edit.php?contextid=1&plugin=h5p", "types": [ { "typename": "H5P Interactive Content" }, { "typename": "Accordion", "typeeditorparams": "library=Accordion-1.4", "typeicon": "http://something/pluginfile.php/1/core_h5p/libraries/13/H5P.Accordion-1.4/icon.svg" } ] } ] }, { "name": "Upload", "link": "http://something/contentbank/contenttype/h5p/view.php?url=http://something/pluginfile.php/1/contentbank/public/accordion.h5p", "icon" : "i/upload" }, { "icon": "i/export" } ] } }} {{#tools}} {{#dropdown}} {{>core_contentbank/bankcontent/toolbar_dropdown}} {{/dropdown}} {{#button}} {{#pix}} {{{ icon }}} {{/pix}} {{{ name }}} {{/button}} {{#checkbox}}
{{/checkbox}} {{/tools}} PK*\fhtemplates/customfields.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 core_contentbank/customfields This template renders the view of the document on content bank. Example context (json): { } }}
{{{data}}}
PK*\@TT templates/renamecontent.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 core_contentbank/renamecontent Example context (json): { "contentid": 17, "name": "Accordion" } }}
PK*\2kk 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 core libraries and APIs, information provided here is intended especially for developers. === 3.11 === * Added "get_uses()" method to content class to return places where a content is used. * Added set_visibility()/get_visibility() methods to let users decide if their content should be listed in the content bank. * The contentbank/upload.php page for displaying the upload files form has been removed. The form for uploading/replacing files now is displayed in a modal. PK*\BQQedit.phpnu[. /** * Create or update contents through the specific content type editor * * @package core_contentbank * @copyright 2020 Victor Deniz * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require('../config.php'); require_login(); $contextid = required_param('contextid', PARAM_INT); $pluginname = required_param('plugin', PARAM_PLUGIN); $id = optional_param('id', null, PARAM_INT); $library = optional_param('library', null, PARAM_RAW); $context = context::instance_by_id($contextid, MUST_EXIST); $cb = new \core_contentbank\contentbank(); if (!$cb->is_context_allowed($context)) { throw new \moodle_exception('contextnotallowed', 'core_contentbank'); } require_capability('moodle/contentbank:access', $context); $returnurl = new \moodle_url('/contentbank/view.php', ['id' => $id]); if (!empty($id)) { $record = $DB->get_record('contentbank_content', ['id' => $id], '*', MUST_EXIST); $contentclass = "$record->contenttype\\content"; $content = new $contentclass($record); // Set the heading title. $heading = $content->get_name(); // The content type of the content overwrites the pluginname param value. $contenttypename = $content->get_content_type(); $breadcrumbtitle = get_string('edit'); } else { $contenttypename = "contenttype_$pluginname"; $heading = get_string('addinganew', 'moodle', get_string('description', $contenttypename)); $content = null; $breadcrumbtitle = get_string('add'); } // Check plugin is enabled. $plugin = core_plugin_manager::instance()->get_plugin_info($contenttypename); if (!$plugin || !$plugin->is_enabled()) { throw new \moodle_exception('unsupported', 'core_contentbank', $returnurl); } // Create content type instance. $contenttypeclass = "$contenttypename\\contenttype"; if (class_exists($contenttypeclass)) { $contenttype = new $contenttypeclass($context); } else { throw new \moodle_exception('unsupported', 'core_contentbank', $returnurl); } // Checks the user can edit this content and content type. if (!$contenttype->can_edit($content)) { throw new \moodle_exception('contenttypenoedit', 'core_contentbank', $returnurl); } $values = [ 'contextid' => $contextid, 'plugin' => $pluginname, 'id' => $id, 'heading' => $heading, 'library' => $library ]; $title = get_string('contentbank'); \core_contentbank\helper::get_page_ready($context, $title, true); if ($PAGE->course) { require_login($PAGE->course->id); } if ($context->contextlevel == CONTEXT_COURSECAT) { $PAGE->set_primary_active_tab('home'); } $PAGE->set_url(new \moodle_url('/contentbank/edit.php', $values)); if ($context->id == \context_system::instance()->id) { $PAGE->set_context(context_course::instance($context->id)); } else { $PAGE->set_context($context); } if ($content) { $PAGE->navbar->add($content->get_name(), new \moodle_url('/contentbank/view.php', ['id' => $id])); } $PAGE->navbar->add($breadcrumbtitle); $PAGE->set_title($title); $PAGE->set_pagelayout('incourse'); $PAGE->set_secondary_active_tab('contentbank'); // Instantiate the content type form. $editorclass = "$contenttypename\\form\\editor"; if (!class_exists($editorclass)) { throw new \moodle_exception('noformdesc'); } $editorform = new $editorclass(null, $values); if ($editorform->is_cancelled()) { if (empty($id)) { $returnurl = new \moodle_url('/contentbank/index.php', ['contextid' => $context->id]); } redirect($returnurl); } else if ($data = $editorform->get_data()) { if (empty($id)) { $msg = get_string('contentcreated', 'contentbank'); } else { $msg = get_string('contentupdated', 'contentbank'); } $id = $editorform->save_content($data); // Just in case we've created a new content. $returnurl->param('id', $id); redirect($returnurl, $msg, null, \core\output\notification::NOTIFY_SUCCESS); } echo $OUTPUT->header(); $editorform->display(); echo $OUTPUT->footer(); PK*\% index.phpnu[. /** * List content in content bank. * * @package core_contentbank * @copyright 2020 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require('../config.php'); require_login(); $contextid = optional_param('contextid', \context_system::instance()->id, PARAM_INT); $search = optional_param('search', '', PARAM_CLEAN); $context = context::instance_by_id($contextid, MUST_EXIST); $cb = new \core_contentbank\contentbank(); if (!$cb->is_context_allowed($context)) { throw new \moodle_exception('contextnotallowed', 'core_contentbank'); } require_capability('moodle/contentbank:access', $context); // If notifications had been sent we don't pay attention to message parameter. if (empty($SESSION->notifications)) { $statusmsg = optional_param('statusmsg', '', PARAM_ALPHANUMEXT); $errormsg = optional_param('errormsg', '', PARAM_ALPHANUMEXT); } $title = get_string('contentbank'); \core_contentbank\helper::get_page_ready($context, $title); if ($PAGE->course) { require_login($PAGE->course->id); } $PAGE->set_url('/contentbank/index.php', ['contextid' => $contextid]); if ($contextid == \context_system::instance()->id) { $PAGE->set_context(context_course::instance($contextid)); } else { $PAGE->set_context($context); } if ($context->contextlevel == CONTEXT_COURSECAT) { $PAGE->set_primary_active_tab('home'); } $PAGE->set_title($title); $PAGE->add_body_class('limitedwidth'); $PAGE->set_pagetype('contentbank'); $PAGE->set_secondary_active_tab('contentbank'); // Get all contents managed by active plugins where the user has permission to render them. $contenttypes = []; $enabledcontenttypes = $cb->get_enabled_content_types(); foreach ($enabledcontenttypes as $contenttypename) { $contenttypeclass = "\\contenttype_$contenttypename\\contenttype"; $contenttype = new $contenttypeclass($context); if ($contenttype->can_access()) { $contenttypes[] = $contenttypename; } } // Get the toolbar ready. $toolbar = array (); if (has_capability('moodle/contentbank:viewunlistedcontent', $context)) { $display = get_user_preferences('core_contentbank_displayunlisted', 1); $toolbar[] = [ 'name' => 'displayunlisted', 'id' => 'displayunlisted', 'checkbox' => true, 'checked' => !empty($display), 'label' => get_string('displayunlisted', 'contentbank'), 'class' => 'displayunlisted m-2', 'action' => 'displayunlisted', ]; $PAGE->requires->js_call_amd( 'core_contentbank/displayunlisted', 'init', ['[data-action=displayunlisted]'] ); } // Place the Add button in the toolbar. if (has_capability('moodle/contentbank:useeditor', $context)) { // Get the content types for which the user can use an editor. $editabletypes = $cb->get_contenttypes_with_capability_feature(\core_contentbank\contenttype::CAN_EDIT, $context); if (!empty($editabletypes)) { // Editor base URL. $editbaseurl = new moodle_url('/contentbank/edit.php', ['contextid' => $contextid]); $toolbar[] = [ 'name' => get_string('add'), 'link' => $editbaseurl, 'dropdown' => true, 'contenttypes' => $editabletypes, 'action' => 'add' ]; } } // Place the Upload button in the toolbar. if (has_capability('moodle/contentbank:upload', $context)) { // Don' show upload button if there's no plugin to support any file extension. $accepted = $cb->get_supported_extensions_as_string($context); if (!empty($accepted)) { $importurl = new moodle_url('/contentbank/index.php', ['contextid' => $contextid]); $toolbar[] = [ 'name' => get_string('upload', 'contentbank'), 'link' => $importurl->out(false), 'icon' => 'i/upload', 'action' => 'upload' ]; $PAGE->requires->js_call_amd( 'core_contentbank/upload', 'initModal', ['[data-action=upload]', \core_contentbank\form\upload_files::class, $contextid] ); } } echo $OUTPUT->header(); echo $OUTPUT->heading($title, 2); echo $OUTPUT->box_start('generalbox'); // If needed, display notifications. if (!empty($errormsg) && get_string_manager()->string_exists($errormsg, 'core_contentbank')) { $errormsg = get_string($errormsg, 'core_contentbank'); echo $OUTPUT->notification($errormsg); } else if (!empty($statusmsg) && get_string_manager()->string_exists($statusmsg, 'core_contentbank')) { $statusmsg = get_string($statusmsg, 'core_contentbank'); echo $OUTPUT->notification($statusmsg, 'notifysuccess'); } $foldercontents = $cb->search_contents($search, $contextid, $contenttypes); // Render the contentbank contents. $folder = new \core_contentbank\output\bankcontent($foldercontents, $toolbar, $context, $cb); echo $OUTPUT->render($folder); echo $OUTPUT->box_end(); echo $OUTPUT->footer(); PK*\c\\amd/src/upload.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 . /** * Module to handle AJAX interactions with content bank upload files. * * @module core_contentbank/upload * @copyright 2021 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import ModalForm from 'core_form/modalform'; import {getString} from 'core/str'; /** * Initialize upload files to the content bank form as Modal form. * * @param {String} elementSelector * @param {String} formClass * @param {Integer} contextId * @param {Integer} contentId */ export const initModal = (elementSelector, formClass, contextId, contentId) => { const element = document.querySelector(elementSelector); element.addEventListener('click', function(e) { e.preventDefault(); const form = new ModalForm({ formClass, args: { contextid: contextId, id: contentId, }, modalConfig: {title: getString('upload', 'contentbank')}, returnFocus: e.target, }); form.addEventListener(form.events.FORM_SUBMITTED, (event) => { document.location = event.detail.returnurl; }); form.show(); }); }; PK*\ K?&?&amd/src/sort.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 . /** * Content bank UI actions. * * @module core_contentbank/sort * @copyright 2020 Bas Brands * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import selectors from './selectors'; import {getString} from 'core/str'; import Prefetch from 'core/prefetch'; import Notification from 'core/notification'; import {setUserPreference} from 'core_user/repository'; /** * Set up the contentbank views. * * @method init */ export const init = () => { const contentBank = document.querySelector(selectors.regions.contentbank); Prefetch.prefetchStrings('contentbank', ['contentname', 'uses', 'lastmodified', 'size', 'type', 'author']); Prefetch.prefetchStrings('moodle', ['sortbyx', 'sortbyxreverse']); registerListenerEvents(contentBank); }; /** * Register contentbank related event listeners. * * @method registerListenerEvents * @param {HTMLElement} contentBank The DOM node of the content bank */ const registerListenerEvents = (contentBank) => { contentBank.addEventListener('click', e => { const viewList = contentBank.querySelector(selectors.actions.viewlist); const viewGrid = contentBank.querySelector(selectors.actions.viewgrid); const fileArea = contentBank.querySelector(selectors.regions.filearea); const shownItems = fileArea.querySelectorAll(selectors.elements.listitem); // View as Grid button. if (e.target.closest(selectors.actions.viewgrid)) { contentBank.classList.remove('view-list'); contentBank.classList.add('view-grid'); if (fileArea && shownItems) { fileArea.setAttribute('role', 'list'); shownItems.forEach(listItem => { listItem.setAttribute('role', 'listitem'); listItem.querySelectorAll(selectors.elements.cell).forEach(cell => cell.removeAttribute('role')); }); const heading = fileArea.querySelector(selectors.elements.heading); if (heading) { heading.removeAttribute('role'); heading.querySelectorAll(selectors.elements.cell).forEach(cell => cell.removeAttribute('role')); } } viewGrid.classList.add('active'); viewList.classList.remove('active'); setViewListPreference(false); return; } // View as List button. if (e.target.closest(selectors.actions.viewlist)) { contentBank.classList.remove('view-grid'); contentBank.classList.add('view-list'); if (fileArea && shownItems) { fileArea.setAttribute('role', 'table'); shownItems.forEach(listItem => { listItem.setAttribute('role', 'row'); listItem.querySelectorAll(selectors.elements.cell).forEach(cell => cell.setAttribute('role', 'cell')); }); const heading = fileArea.querySelector(selectors.elements.heading); if (heading) { heading.setAttribute('role', 'row'); heading.querySelectorAll(selectors.elements.cell).forEach(cell => cell.setAttribute('role', 'columnheader')); } } viewList.classList.add('active'); viewGrid.classList.remove('active'); setViewListPreference(true); return; } if (fileArea && shownItems) { // Sort by file name alphabetical const sortByName = e.target.closest(selectors.actions.sortname); if (sortByName) { const ascending = updateSortButtons(contentBank, sortByName); updateSortOrder(fileArea, shownItems, 'data-file', ascending); return; } // Sort by uses. const sortByUses = e.target.closest(selectors.actions.sortuses); if (sortByUses) { const ascending = updateSortButtons(contentBank, sortByUses); updateSortOrder(fileArea, shownItems, 'data-uses', ascending); return; } // Sort by date. const sortByDate = e.target.closest(selectors.actions.sortdate); if (sortByDate) { const ascending = updateSortButtons(contentBank, sortByDate); updateSortOrder(fileArea, shownItems, 'data-timemodified', ascending); return; } // Sort by size. const sortBySize = e.target.closest(selectors.actions.sortsize); if (sortBySize) { const ascending = updateSortButtons(contentBank, sortBySize); updateSortOrder(fileArea, shownItems, 'data-bytes', ascending); return; } // Sort by type. const sortByType = e.target.closest(selectors.actions.sorttype); if (sortByType) { const ascending = updateSortButtons(contentBank, sortByType); updateSortOrder(fileArea, shownItems, 'data-type', ascending); return; } // Sort by author. const sortByAuthor = e.target.closest(selectors.actions.sortauthor); if (sortByAuthor) { const ascending = updateSortButtons(contentBank, sortByAuthor); updateSortOrder(fileArea, shownItems, 'data-author', ascending); } return; } }); }; /** * Set the contentbank user preference in list view * * @param {Bool} viewList view ContentBank as list. * @return {Promise} Repository promise. */ const setViewListPreference = function(viewList) { // If the given status is not hidden, the preference has to be deleted with a null value. if (viewList === false) { viewList = null; } return setUserPreference('core_contentbank_view_list', viewList) .catch(Notification.exception); }; /** * Update the sort button view. * * @method updateSortButtons * @param {HTMLElement} contentBank The DOM node of the contentbank button * @param {HTMLElement} sortButton The DOM node of the sort button * @return {Bool} sort ascending */ const updateSortButtons = (contentBank, sortButton) => { const sortButtons = contentBank.querySelectorAll(selectors.elements.sortbutton); sortButtons.forEach((button) => { if (button !== sortButton) { button.classList.remove('dir-asc'); button.classList.remove('dir-desc'); button.classList.add('dir-none'); button.closest(selectors.elements.cell).setAttribute('aria-sort', 'none'); updateButtonTitle(button, false); } }); let ascending = true; if (sortButton.classList.contains('dir-none')) { sortButton.classList.remove('dir-none'); sortButton.classList.add('dir-asc'); sortButton.closest(selectors.elements.cell).setAttribute('aria-sort', 'ascending'); } else if (sortButton.classList.contains('dir-asc')) { sortButton.classList.remove('dir-asc'); sortButton.classList.add('dir-desc'); sortButton.closest(selectors.elements.cell).setAttribute('aria-sort', 'descending'); ascending = false; } else if (sortButton.classList.contains('dir-desc')) { sortButton.classList.remove('dir-desc'); sortButton.classList.add('dir-asc'); sortButton.closest(selectors.elements.cell).setAttribute('aria-sort', 'ascending'); } updateButtonTitle(sortButton, ascending); return ascending; }; /** * Update the button title. * * @method updateButtonTitle * @param {HTMLElement} button Button to update * @param {Bool} ascending Sort direction * @return {Promise} string promise */ const updateButtonTitle = (button, ascending) => { const sortString = (ascending ? 'sortbyxreverse' : 'sortbyx'); return getString(button.dataset.string, 'contentbank') .then(columnName => { return getString(sortString, 'core', columnName); }) .then(sortByString => { button.setAttribute('title', sortByString); return sortByString; }) .catch(); }; /** * Update the sort order of the itemlist and update the DOM * * @method updateSortOrder * @param {HTMLElement} fileArea the Dom container for the itemlist * @param {Array} itemList Nodelist of Dom elements * @param {String} attribute the attribut to sort on * @param {Bool} ascending Sort Ascending */ const updateSortOrder = (fileArea, itemList, attribute, ascending) => { const sortList = [].slice.call(itemList).sort(function(a, b) { let aa = a.getAttribute(attribute); let bb = b.getAttribute(attribute); if (!isNaN(aa)) { aa = parseInt(aa); bb = parseInt(bb); } if (ascending) { return aa > bb ? 1 : -1; } else { return aa < bb ? 1 : -1; } }); sortList.forEach(listItem => fileArea.appendChild(listItem)); }; PK*\]z5z5amd/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 . /** * Module to manage content bank actions, such as delete or rename. * * @module core_contentbank/actions * @copyright 2020 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([ 'jquery', 'core/ajax', 'core/notification', 'core/str', 'core/templates', 'core/url', 'core/modal_save_cancel', 'core/modal_events'], function($, Ajax, Notification, Str, Templates, Url, ModalSaveCancel, ModalEvents) { /** * List of action selectors. * * @type {{DELETE_CONTENT: string}} */ var ACTIONS = { DELETE_CONTENT: '[data-action="deletecontent"]', RENAME_CONTENT: '[data-action="renamecontent"]', SET_CONTENT_VISIBILITY: '[data-action="setcontentvisibility"]', COPY_CONTENT: '[data-action="copycontent"]', }; /** * Actions class. */ var Actions = function() { this.registerEvents(); }; /** * Register event listeners. */ Actions.prototype.registerEvents = function() { $(ACTIONS.DELETE_CONTENT).click(function(e) { e.preventDefault(); var contentname = $(this).data('contentname'); var contentuses = $(this).data('uses'); var contentid = $(this).data('contentid'); var contextid = $(this).data('contextid'); var strings = [ { key: 'deletecontent', component: 'core_contentbank' }, { key: 'deletecontentconfirm', component: 'core_contentbank', param: { name: contentname, } }, { key: 'deletecontentconfirmlinked', component: 'core_contentbank', }, { key: 'delete', component: 'core' }, ]; var deleteButtonText = ''; Str.get_strings(strings).then(function(langStrings) { var modalTitle = langStrings[0]; var modalContent = langStrings[1]; if (contentuses > 0) { modalContent += ' ' + langStrings[2]; } deleteButtonText = langStrings[3]; return ModalSaveCancel.create({ title: modalTitle, body: modalContent, large: true, removeOnClose: true, show: true, buttons: { save: deleteButtonText, }, }); }).then(function(modal) { modal.getRoot().on(ModalEvents.save, function() { // The action is now confirmed, sending an action for it. return deleteContent(contentid, contextid); }); return; }).catch(Notification.exception); }); $(ACTIONS.RENAME_CONTENT).click(function(e) { e.preventDefault(); var contentname = $(this).data('contentname'); var contentid = $(this).data('contentid'); var strings = [ { key: 'renamecontent', component: 'core_contentbank' }, { key: 'rename', component: 'core_contentbank' }, ]; var saveButtonText = ''; Str.get_strings(strings).then(function(langStrings) { var modalTitle = langStrings[0]; saveButtonText = langStrings[1]; return ModalSaveCancel.create({ title: modalTitle, body: Templates.render('core_contentbank/renamecontent', {'contentid': contentid, 'name': contentname}), removeOnClose: true, show: true, buttons: { save: saveButtonText, }, }); }).then(function(modal) { modal.getRoot().on(ModalEvents.save, function(e) { // The action is now confirmed, sending an action for it. var newname = $("#newname").val().trim(); if (newname) { renameContent(contentid, newname); } else { var errorStrings = [ { key: 'error', }, { key: 'emptynamenotallowed', component: 'core_contentbank', }, ]; Str.get_strings(errorStrings).then(function(langStrings) { Notification.alert(langStrings[0], langStrings[1]); }).catch(Notification.exception); e.preventDefault(); } }); return; }).catch(Notification.exception); }); $(ACTIONS.COPY_CONTENT).click(function(e) { e.preventDefault(); var contentname = $(this).data('contentname'); var contentid = $(this).data('contentid'); var strings = [ { key: 'copycontent', component: 'core_contentbank' }, { key: 'error', }, { key: 'emptynamenotallowed', component: 'core_contentbank', }, ]; let errorTitle, errorMessage; Str.get_strings(strings).then(function(langStrings) { var modalTitle = langStrings[0]; errorTitle = langStrings[1]; errorMessage = langStrings[2]; return ModalSaveCancel.create({ title: modalTitle, body: Templates.render('core_contentbank/copycontent', {'contentid': contentid, 'name': contentname}), removeOnClose: true, show: true, }); }).then(function(modal) { modal.getRoot().on(ModalEvents.save, function() { // The action is now confirmed, sending an action for it. var newname = $("#newname").val().trim(); if (newname) { copyContent(contentid, newname); } else { Notification.alert(errorTitle, errorMessage); return false; } }); return; }).catch(Notification.exception); }); $(ACTIONS.SET_CONTENT_VISIBILITY).click(function(e) { e.preventDefault(); var contentid = $(this).data('contentid'); var visibility = $(this).data('visibility'); setContentVisibility(contentid, visibility); }); }; /** * Delete content from the content bank. * * @param {int} contentid The content to delete. * @param {int} contextid The contextid where the content belongs. */ function deleteContent(contentid, contextid) { var request = { methodname: 'core_contentbank_delete_content', args: { contentids: {contentid} } }; var requestType = 'success'; Ajax.call([request])[0].then(function(data) { if (data.result) { return 'contentdeleted'; } requestType = 'error'; return 'contentnotdeleted'; }).done(function(message) { var params = { contextid: contextid }; if (requestType == 'success') { params.statusmsg = message; } else { params.errormsg = message; } // Redirect to the main content bank page and display the message as a notification. window.location.href = Url.relativeUrl('contentbank/index.php', params, false); }).fail(Notification.exception); } /** * Rename content in the content bank. * * @param {int} contentid The content to rename. * @param {string} name The new name for the content. */ function renameContent(contentid, name) { var request = { methodname: 'core_contentbank_rename_content', args: { contentid: contentid, name: name } }; var requestType = 'success'; Ajax.call([request])[0].then(function(data) { if (data.result) { return 'contentrenamed'; } requestType = 'error'; return data.warnings[0].message; }).then(function(message) { var params = null; if (requestType == 'success') { params = { id: contentid, statusmsg: message }; // Redirect to the content view page and display the message as a notification. window.location.href = Url.relativeUrl('contentbank/view.php', params, false); } else { // Fetch error notifications. Notification.addNotification({ message: message, type: 'error' }); Notification.fetchNotifications(); } return; }).catch(Notification.exception); } /** * Copy content in the content bank. * * @param {int} contentid The content to copy. * @param {string} name The name for the new content. */ function copyContent(contentid, name) { var request = { methodname: 'core_contentbank_copy_content', args: { contentid: contentid, name: name } }; Ajax.call([request])[0].then(function(data) { if (data.id == 0) { // Fetch error notifications. Notification.addNotification({ message: data.warnings[0].message, type: 'error' }); Notification.fetchNotifications(); return data.warnings[0].message; } else { let params = { id: data.id, statusmsg: 'contentcopied' }; // Redirect to the content view page and display the message as a notification. window.location.href = Url.relativeUrl('contentbank/view.php', params, false); } return ''; }).catch(Notification.exception); } /** * Set content visibility in the content bank. * * @param {int} contentid The content to modify * @param {int} visibility The new visibility value */ function setContentVisibility(contentid, visibility) { var request = { methodname: 'core_contentbank_set_content_visibility', args: { contentid: contentid, visibility: visibility } }; var requestType = 'success'; Ajax.call([request])[0].then(function(data) { if (data.result) { return 'contentvisibilitychanged'; } requestType = 'error'; return data.warnings[0].message; }).then(function(message) { var params = null; if (requestType == 'success') { params = { id: contentid, statusmsg: message }; // Redirect to the content view page and display the message as a notification. window.location.href = Url.relativeUrl('contentbank/view.php', params, false); } else { // Fetch error notifications. Notification.addNotification({ message: message, type: 'error' }); Notification.fetchNotifications(); } return; }).catch(Notification.exception); } return /** @alias module:core_contentbank/actions */ { // Public variables and functions. /** * Initialise the contentbank actions. * * @method init * @return {Actions} */ 'init': function() { return new Actions(); } }; }); PK*\gISYYamd/src/search.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 . /** * Search methods for finding contents in the content bank. * * @module core_contentbank/search * @copyright 2020 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import $ from 'jquery'; import selectors from 'core_contentbank/selectors'; import {getString} from 'core/str'; import Pending from 'core/pending'; import {debounce} from 'core/utils'; /** * Set up the search. * * @method init */ export const init = () => { const pendingPromise = new Pending(); const root = $(selectors.regions.contentbank); registerListenerEvents(root); pendingPromise.resolve(); }; /** * Register contentbank search related event listeners. * * @method registerListenerEvents * @param {Object} root The root element for the contentbank. */ const registerListenerEvents = (root) => { const searchInput = root.find(selectors.elements.searchinput)[0]; root.on('click', selectors.actions.search, function(e) { e.preventDefault(); toggleSearchResultsView(root, searchInput.value); }); root.on('click', selectors.actions.clearSearch, function(e) { e.preventDefault(); searchInput.value = ""; searchInput.focus(); toggleSearchResultsView(root, searchInput.value); }); // The search input is also triggered. searchInput.addEventListener('input', debounce(() => { // Display the search results. toggleSearchResultsView(root, searchInput.value); }, 300)); }; /** * Toggle (display/hide) the search results depending on the value of the search query. * * @method toggleSearchResultsView * @param {HTMLElement} body The root element for the contentbank. * @param {String} searchQuery The search query. */ const toggleSearchResultsView = async(body, searchQuery) => { const clearSearchButton = body.find(selectors.actions.clearSearch)[0]; const navbarBreadcrumb = body.find(selectors.elements.cbnavbarbreadcrumb)[0]; const navbarTotal = body.find(selectors.elements.cbnavbartotalsearch)[0]; // Update the results. const filteredContents = filterContents(body, searchQuery); if (searchQuery.length > 0) { // As the search query is present, search results should be displayed. // Display the "clear" search button in the activity chooser search bar. clearSearchButton.classList.remove('d-none'); // Change the cb-navbar to display total items found. navbarBreadcrumb.classList.add('d-none'); navbarTotal.innerHTML = await getString('itemsfound', 'core_contentbank', filteredContents.length); navbarTotal.classList.remove('d-none'); } else { // As search query is not present, the search results should be removed. // Hide the "clear" search button in the activity chooser search bar. clearSearchButton.classList.add('d-none'); // Display again the breadcrumb in the navbar. navbarBreadcrumb.classList.remove('d-none'); navbarTotal.classList.add('d-none'); } }; /** * Return the list of contents which have a name that matches the given search term. * * @method filterContents * @param {HTMLElement} body The root element for the contentbank. * @param {String} searchTerm The search term to match. * @return {Array} */ const filterContents = (body, searchTerm) => { const contents = Array.from(body.find(selectors.elements.listitem)); const searchResults = []; contents.forEach((content) => { const contentName = content.getAttribute('data-name'); if (searchTerm === '' || contentName.toLowerCase().includes(searchTerm.toLowerCase())) { // The content matches the search criteria so it should be displayed and hightlighted. searchResults.push(content); const contentNameElement = content.querySelector(selectors.regions.cbcontentname); contentNameElement.innerHTML = highlight(contentName, searchTerm); content.classList.remove('d-none'); } else { content.classList.add('d-none'); } }); return searchResults; }; /** * Highlight a given string in a text. * * @method highlight * @param {String} text The whole text. * @param {String} highlightText The piece of text to highlight. * @return {String} */ const highlight = (text, highlightText) => { let result = text; if (highlightText !== '') { const pos = text.toLowerCase().indexOf(highlightText.toLowerCase()); if (pos > -1) { result = text.substr(0, pos) + '' + text.substr(pos, highlightText.length) + '' + text.substr(pos + highlightText.length); } } return result; }; PK*\E7amd/src/displayunlisted.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 . /** * Module to handle toggling "Display unlisted" preference * * @module core_contentbank/displayunlisted * @copyright 2023 Daniel Neis Araujo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import Notification from 'core/notification'; import Pending from 'core/pending'; import {setUserPreference} from 'core_user/repository'; /** * Initialize module, add event listeners * * @param {String} elementSelector */ export const init = elementSelector => { document.addEventListener('click', event => { const element = event.target.closest(elementSelector); if (element) { const pendingPromise = new Pending('core_contentbank/displayunlisted'); setUserPreference('core_contentbank_displayunlisted', !!element.checked) .then(() => { pendingPromise.resolve(); return document.location.reload(); }) .catch(Notification.exception); } }); }; PK*\~k  amd/src/selectors.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 . /** * Define all of the selectors we will be using on the contentbank interface. * * @module core_contentbank/selectors * @copyright 2020 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * A small helper function to build queryable data selectors. * * @method getDataSelector * @param {String} name * @param {String} value * @return {string} */ const getDataSelector = (name, value) => { return `[data-${name}="${value}"]`; }; export default { regions: { cbcontentname: getDataSelector('region', 'cb-content-name'), contentbank: getDataSelector('region', 'contentbank'), filearea: getDataSelector('region', 'filearea') }, actions: { search: getDataSelector('action', 'searchcontent'), clearSearch: getDataSelector('action', 'clearsearch'), viewgrid: getDataSelector('action', 'viewgrid'), viewlist: getDataSelector('action', 'viewlist'), sortname: getDataSelector('action', 'sortname'), sortuses: getDataSelector('action', 'sortuses'), sortdate: getDataSelector('action', 'sortdate'), sortsize: getDataSelector('action', 'sortsize'), sorttype: getDataSelector('action', 'sorttype'), sortauthor: getDataSelector('action', 'sortauthor'), }, elements: { listitem: '.cb-listitem', heading: '.cb-heading', cell: '.cb-column', cbnavbarbreadcrumb: '.cb-navbar-breadbrumb', cbnavbartotalsearch: '.cb-navbar-totalsearch', searchinput: '[data-action="search"]', sortbutton: '.cb-btnsort' }, }; PK*\}amd/build/search.min.js.mapnu[{"version":3,"file":"search.min.js","sources":["../src/search.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 * Search methods for finding contents in the content bank.\n *\n * @module core_contentbank/search\n * @copyright 2020 Sara Arjona \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport selectors from 'core_contentbank/selectors';\nimport {getString} from 'core/str';\nimport Pending from 'core/pending';\nimport {debounce} from 'core/utils';\n\n/**\n * Set up the search.\n *\n * @method init\n */\nexport const init = () => {\n const pendingPromise = new Pending();\n\n const root = $(selectors.regions.contentbank);\n registerListenerEvents(root);\n\n pendingPromise.resolve();\n};\n\n/**\n * Register contentbank search related event listeners.\n *\n * @method registerListenerEvents\n * @param {Object} root The root element for the contentbank.\n */\nconst registerListenerEvents = (root) => {\n\n const searchInput = root.find(selectors.elements.searchinput)[0];\n\n root.on('click', selectors.actions.search, function(e) {\n e.preventDefault();\n toggleSearchResultsView(root, searchInput.value);\n });\n\n root.on('click', selectors.actions.clearSearch, function(e) {\n e.preventDefault();\n searchInput.value = \"\";\n searchInput.focus();\n toggleSearchResultsView(root, searchInput.value);\n });\n\n // The search input is also triggered.\n searchInput.addEventListener('input', debounce(() => {\n // Display the search results.\n toggleSearchResultsView(root, searchInput.value);\n }, 300));\n\n};\n\n/**\n * Toggle (display/hide) the search results depending on the value of the search query.\n *\n * @method toggleSearchResultsView\n * @param {HTMLElement} body The root element for the contentbank.\n * @param {String} searchQuery The search query.\n */\nconst toggleSearchResultsView = async(body, searchQuery) => {\n const clearSearchButton = body.find(selectors.actions.clearSearch)[0];\n\n const navbarBreadcrumb = body.find(selectors.elements.cbnavbarbreadcrumb)[0];\n const navbarTotal = body.find(selectors.elements.cbnavbartotalsearch)[0];\n // Update the results.\n const filteredContents = filterContents(body, searchQuery);\n if (searchQuery.length > 0) {\n // As the search query is present, search results should be displayed.\n\n // Display the \"clear\" search button in the activity chooser search bar.\n clearSearchButton.classList.remove('d-none');\n\n // Change the cb-navbar to display total items found.\n navbarBreadcrumb.classList.add('d-none');\n navbarTotal.innerHTML = await getString('itemsfound', 'core_contentbank', filteredContents.length);\n navbarTotal.classList.remove('d-none');\n } else {\n // As search query is not present, the search results should be removed.\n\n // Hide the \"clear\" search button in the activity chooser search bar.\n clearSearchButton.classList.add('d-none');\n\n // Display again the breadcrumb in the navbar.\n navbarBreadcrumb.classList.remove('d-none');\n navbarTotal.classList.add('d-none');\n }\n};\n\n/**\n * Return the list of contents which have a name that matches the given search term.\n *\n * @method filterContents\n * @param {HTMLElement} body The root element for the contentbank.\n * @param {String} searchTerm The search term to match.\n * @return {Array}\n */\nconst filterContents = (body, searchTerm) => {\n const contents = Array.from(body.find(selectors.elements.listitem));\n const searchResults = [];\n contents.forEach((content) => {\n const contentName = content.getAttribute('data-name');\n if (searchTerm === '' || contentName.toLowerCase().includes(searchTerm.toLowerCase())) {\n // The content matches the search criteria so it should be displayed and hightlighted.\n searchResults.push(content);\n const contentNameElement = content.querySelector(selectors.regions.cbcontentname);\n contentNameElement.innerHTML = highlight(contentName, searchTerm);\n content.classList.remove('d-none');\n } else {\n content.classList.add('d-none');\n }\n });\n\n return searchResults;\n};\n\n/**\n * Highlight a given string in a text.\n *\n * @method highlight\n * @param {String} text The whole text.\n * @param {String} highlightText The piece of text to highlight.\n * @return {String}\n */\nconst highlight = (text, highlightText) => {\n let result = text;\n if (highlightText !== '') {\n const pos = text.toLowerCase().indexOf(highlightText.toLowerCase());\n if (pos > -1) {\n result = text.substr(0, pos) + '' + text.substr(pos, highlightText.length) + '' +\n text.substr(pos + highlightText.length);\n }\n }\n\n return result;\n};\n"],"names":["pendingPromise","Pending","root","selectors","regions","contentbank","registerListenerEvents","resolve","searchInput","find","elements","searchinput","on","actions","search","e","preventDefault","toggleSearchResultsView","value","clearSearch","focus","addEventListener","async","body","searchQuery","clearSearchButton","navbarBreadcrumb","cbnavbarbreadcrumb","navbarTotal","cbnavbartotalsearch","filteredContents","filterContents","length","classList","remove","add","innerHTML","searchTerm","contents","Array","from","listitem","searchResults","forEach","content","contentName","getAttribute","toLowerCase","includes","push","querySelector","cbcontentname","highlight","text","highlightText","result","pos","indexOf","substr"],"mappings":";;;;;;;gOAkCoB,WACVA,eAAiB,IAAIC,iBAErBC,MAAO,mBAAEC,mBAAUC,QAAQC,aACjCC,uBAAuBJ,MAEvBF,eAAeO,iBASbD,uBAA0BJ,aAEtBM,YAAcN,KAAKO,KAAKN,mBAAUO,SAASC,aAAa,GAE9DT,KAAKU,GAAG,QAAST,mBAAUU,QAAQC,QAAQ,SAASC,GAChDA,EAAEC,iBACFC,wBAAwBf,KAAMM,YAAYU,UAG9ChB,KAAKU,GAAG,QAAST,mBAAUU,QAAQM,aAAa,SAASJ,GACrDA,EAAEC,iBACFR,YAAYU,MAAQ,GACpBV,YAAYY,QACZH,wBAAwBf,KAAMM,YAAYU,UAI9CV,YAAYa,iBAAiB,SAAS,oBAAS,KAE3CJ,wBAAwBf,KAAMM,YAAYU,SAC3C,OAWDD,wBAA0BK,MAAMC,KAAMC,qBAClCC,kBAAoBF,KAAKd,KAAKN,mBAAUU,QAAQM,aAAa,GAE7DO,iBAAmBH,KAAKd,KAAKN,mBAAUO,SAASiB,oBAAoB,GACpEC,YAAcL,KAAKd,KAAKN,mBAAUO,SAASmB,qBAAqB,GAEhEC,iBAAmBC,eAAeR,KAAMC,aAC1CA,YAAYQ,OAAS,GAIrBP,kBAAkBQ,UAAUC,OAAO,UAGnCR,iBAAiBO,UAAUE,IAAI,UAC/BP,YAAYQ,gBAAkB,kBAAU,aAAc,mBAAoBN,iBAAiBE,QAC3FJ,YAAYK,UAAUC,OAAO,YAK7BT,kBAAkBQ,UAAUE,IAAI,UAGhCT,iBAAiBO,UAAUC,OAAO,UAClCN,YAAYK,UAAUE,IAAI,YAY5BJ,eAAiB,CAACR,KAAMc,oBACpBC,SAAWC,MAAMC,KAAKjB,KAAKd,KAAKN,mBAAUO,SAAS+B,WACnDC,cAAgB,UACtBJ,SAASK,SAASC,gBACRC,YAAcD,QAAQE,aAAa,gBACtB,KAAfT,YAAqBQ,YAAYE,cAAcC,SAASX,WAAWU,eAAgB,CAEnFL,cAAcO,KAAKL,SACQA,QAAQM,cAAc/C,mBAAUC,QAAQ+C,eAChDf,UAAYgB,UAAUP,YAAaR,YACtDO,QAAQX,UAAUC,OAAO,eAEzBU,QAAQX,UAAUE,IAAI,aAIvBO,eAWLU,UAAY,CAACC,KAAMC,qBACjBC,OAASF,QACS,KAAlBC,cAAsB,OAChBE,IAAMH,KAAKN,cAAcU,QAAQH,cAAcP,eACjDS,KAAO,IACPD,OAASF,KAAKK,OAAO,EAAGF,KAAO,2BAA6BH,KAAKK,OAAOF,IAAKF,cAActB,QAAU,UACjGqB,KAAKK,OAAOF,IAAMF,cAActB,gBAIrCuB"}PK*\ABBamd/build/actions.min.jsnu[/** * Module to manage content bank actions, such as delete or rename. * * @module core_contentbank/actions * @copyright 2020 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("core_contentbank/actions",["jquery","core/ajax","core/notification","core/str","core/templates","core/url","core/modal_save_cancel","core/modal_events"],(function($,Ajax,Notification,Str,Templates,Url,ModalSaveCancel,ModalEvents){var ACTIONS_DELETE_CONTENT='[data-action="deletecontent"]',ACTIONS_RENAME_CONTENT='[data-action="renamecontent"]',ACTIONS_SET_CONTENT_VISIBILITY='[data-action="setcontentvisibility"]',ACTIONS_COPY_CONTENT='[data-action="copycontent"]',Actions=function(){this.registerEvents()};return Actions.prototype.registerEvents=function(){$(ACTIONS_DELETE_CONTENT).click((function(e){e.preventDefault();var contentname=$(this).data("contentname"),contentuses=$(this).data("uses"),contentid=$(this).data("contentid"),contextid=$(this).data("contextid"),strings=[{key:"deletecontent",component:"core_contentbank"},{key:"deletecontentconfirm",component:"core_contentbank",param:{name:contentname}},{key:"deletecontentconfirmlinked",component:"core_contentbank"},{key:"delete",component:"core"}],deleteButtonText="";Str.get_strings(strings).then((function(langStrings){var modalTitle=langStrings[0],modalContent=langStrings[1];return contentuses>0&&(modalContent+=" "+langStrings[2]),deleteButtonText=langStrings[3],ModalSaveCancel.create({title:modalTitle,body:modalContent,large:!0,removeOnClose:!0,show:!0,buttons:{save:deleteButtonText}})})).then((function(modal){modal.getRoot().on(ModalEvents.save,(function(){return function(contentid,contextid){var request={methodname:"core_contentbank_delete_content",args:{contentids:{contentid:contentid}}},requestType="success";Ajax.call([request])[0].then((function(data){return data.result?"contentdeleted":(requestType="error","contentnotdeleted")})).done((function(message){var params={contextid:contextid};"success"==requestType?params.statusmsg=message:params.errormsg=message,window.location.href=Url.relativeUrl("contentbank/index.php",params,!1)})).fail(Notification.exception)}(contentid,contextid)}))})).catch(Notification.exception)})),$(ACTIONS_RENAME_CONTENT).click((function(e){e.preventDefault();var contentname=$(this).data("contentname"),contentid=$(this).data("contentid"),saveButtonText="";Str.get_strings([{key:"renamecontent",component:"core_contentbank"},{key:"rename",component:"core_contentbank"}]).then((function(langStrings){var modalTitle=langStrings[0];return saveButtonText=langStrings[1],ModalSaveCancel.create({title:modalTitle,body:Templates.render("core_contentbank/renamecontent",{contentid:contentid,name:contentname}),removeOnClose:!0,show:!0,buttons:{save:saveButtonText}})})).then((function(modal){modal.getRoot().on(ModalEvents.save,(function(e){var newname=$("#newname").val().trim();if(newname)!function(contentid,name){var request={methodname:"core_contentbank_rename_content",args:{contentid:contentid,name:name}},requestType="success";Ajax.call([request])[0].then((function(data){return data.result?"contentrenamed":(requestType="error",data.warnings[0].message)})).then((function(message){var params=null;"success"==requestType?(params={id:contentid,statusmsg:message},window.location.href=Url.relativeUrl("contentbank/view.php",params,!1)):(Notification.addNotification({message:message,type:"error"}),Notification.fetchNotifications())})).catch(Notification.exception)}(contentid,newname);else{Str.get_strings([{key:"error"},{key:"emptynamenotallowed",component:"core_contentbank"}]).then((function(langStrings){Notification.alert(langStrings[0],langStrings[1])})).catch(Notification.exception),e.preventDefault()}}))})).catch(Notification.exception)})),$(ACTIONS_COPY_CONTENT).click((function(e){e.preventDefault();var contentname=$(this).data("contentname"),contentid=$(this).data("contentid");let errorTitle,errorMessage;Str.get_strings([{key:"copycontent",component:"core_contentbank"},{key:"error"},{key:"emptynamenotallowed",component:"core_contentbank"}]).then((function(langStrings){var modalTitle=langStrings[0];return errorTitle=langStrings[1],errorMessage=langStrings[2],ModalSaveCancel.create({title:modalTitle,body:Templates.render("core_contentbank/copycontent",{contentid:contentid,name:contentname}),removeOnClose:!0,show:!0})})).then((function(modal){modal.getRoot().on(ModalEvents.save,(function(){var newname=$("#newname").val().trim();if(!newname)return Notification.alert(errorTitle,errorMessage),!1;!function(contentid,name){var request={methodname:"core_contentbank_copy_content",args:{contentid:contentid,name:name}};Ajax.call([request])[0].then((function(data){if(0==data.id)return Notification.addNotification({message:data.warnings[0].message,type:"error"}),Notification.fetchNotifications(),data.warnings[0].message;{let params={id:data.id,statusmsg:"contentcopied"};window.location.href=Url.relativeUrl("contentbank/view.php",params,!1)}return""})).catch(Notification.exception)}(contentid,newname)}))})).catch(Notification.exception)})),$(ACTIONS_SET_CONTENT_VISIBILITY).click((function(e){e.preventDefault(),function(contentid,visibility){var request={methodname:"core_contentbank_set_content_visibility",args:{contentid:contentid,visibility:visibility}},requestType="success";Ajax.call([request])[0].then((function(data){return data.result?"contentvisibilitychanged":(requestType="error",data.warnings[0].message)})).then((function(message){var params=null;"success"==requestType?(params={id:contentid,statusmsg:message},window.location.href=Url.relativeUrl("contentbank/view.php",params,!1)):(Notification.addNotification({message:message,type:"error"}),Notification.fetchNotifications())})).catch(Notification.exception)}($(this).data("contentid"),$(this).data("visibility"))}))},{init:function(){return new Actions}}})); //# sourceMappingURL=actions.min.js.mapPK*\l)IIamd/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/**\n * Module to manage content bank actions, such as delete or rename.\n *\n * @module core_contentbank/actions\n * @copyright 2020 Sara Arjona \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core/ajax',\n 'core/notification',\n 'core/str',\n 'core/templates',\n 'core/url',\n 'core/modal_save_cancel',\n 'core/modal_events'],\nfunction($, Ajax, Notification, Str, Templates, Url, ModalSaveCancel, ModalEvents) {\n\n /**\n * List of action selectors.\n *\n * @type {{DELETE_CONTENT: string}}\n */\n var ACTIONS = {\n DELETE_CONTENT: '[data-action=\"deletecontent\"]',\n RENAME_CONTENT: '[data-action=\"renamecontent\"]',\n SET_CONTENT_VISIBILITY: '[data-action=\"setcontentvisibility\"]',\n COPY_CONTENT: '[data-action=\"copycontent\"]',\n };\n\n /**\n * Actions class.\n */\n var Actions = function() {\n this.registerEvents();\n };\n\n /**\n * Register event listeners.\n */\n Actions.prototype.registerEvents = function() {\n $(ACTIONS.DELETE_CONTENT).click(function(e) {\n e.preventDefault();\n\n var contentname = $(this).data('contentname');\n var contentuses = $(this).data('uses');\n var contentid = $(this).data('contentid');\n var contextid = $(this).data('contextid');\n\n var strings = [\n {\n key: 'deletecontent',\n component: 'core_contentbank'\n },\n {\n key: 'deletecontentconfirm',\n component: 'core_contentbank',\n param: {\n name: contentname,\n }\n },\n {\n key: 'deletecontentconfirmlinked',\n component: 'core_contentbank',\n },\n {\n key: 'delete',\n component: 'core'\n },\n ];\n\n var deleteButtonText = '';\n Str.get_strings(strings).then(function(langStrings) {\n var modalTitle = langStrings[0];\n var modalContent = langStrings[1];\n if (contentuses > 0) {\n modalContent += ' ' + langStrings[2];\n }\n deleteButtonText = langStrings[3];\n\n return ModalSaveCancel.create({\n title: modalTitle,\n body: modalContent,\n large: true,\n removeOnClose: true,\n show: true,\n buttons: {\n save: deleteButtonText,\n },\n });\n }).then(function(modal) {\n modal.getRoot().on(ModalEvents.save, function() {\n // The action is now confirmed, sending an action for it.\n return deleteContent(contentid, contextid);\n });\n\n return;\n }).catch(Notification.exception);\n });\n\n $(ACTIONS.RENAME_CONTENT).click(function(e) {\n e.preventDefault();\n\n var contentname = $(this).data('contentname');\n var contentid = $(this).data('contentid');\n\n var strings = [\n {\n key: 'renamecontent',\n component: 'core_contentbank'\n },\n {\n key: 'rename',\n component: 'core_contentbank'\n },\n ];\n\n var saveButtonText = '';\n Str.get_strings(strings).then(function(langStrings) {\n var modalTitle = langStrings[0];\n saveButtonText = langStrings[1];\n\n return ModalSaveCancel.create({\n title: modalTitle,\n body: Templates.render('core_contentbank/renamecontent', {'contentid': contentid, 'name': contentname}),\n removeOnClose: true,\n show: true,\n buttons: {\n save: saveButtonText,\n },\n });\n }).then(function(modal) {\n modal.getRoot().on(ModalEvents.save, function(e) {\n // The action is now confirmed, sending an action for it.\n var newname = $(\"#newname\").val().trim();\n if (newname) {\n renameContent(contentid, newname);\n } else {\n var errorStrings = [\n {\n key: 'error',\n },\n {\n key: 'emptynamenotallowed',\n component: 'core_contentbank',\n },\n ];\n Str.get_strings(errorStrings).then(function(langStrings) {\n Notification.alert(langStrings[0], langStrings[1]);\n }).catch(Notification.exception);\n e.preventDefault();\n }\n });\n\n return;\n }).catch(Notification.exception);\n });\n\n $(ACTIONS.COPY_CONTENT).click(function(e) {\n e.preventDefault();\n\n var contentname = $(this).data('contentname');\n var contentid = $(this).data('contentid');\n\n var strings = [\n {\n key: 'copycontent',\n component: 'core_contentbank'\n },\n {\n key: 'error',\n },\n {\n key: 'emptynamenotallowed',\n component: 'core_contentbank',\n },\n ];\n\n let errorTitle, errorMessage;\n Str.get_strings(strings).then(function(langStrings) {\n var modalTitle = langStrings[0];\n errorTitle = langStrings[1];\n errorMessage = langStrings[2];\n\n return ModalSaveCancel.create({\n title: modalTitle,\n body: Templates.render('core_contentbank/copycontent', {'contentid': contentid, 'name': contentname}),\n removeOnClose: true,\n show: true,\n });\n }).then(function(modal) {\n modal.getRoot().on(ModalEvents.save, function() {\n // The action is now confirmed, sending an action for it.\n var newname = $(\"#newname\").val().trim();\n if (newname) {\n copyContent(contentid, newname);\n } else {\n Notification.alert(errorTitle, errorMessage);\n return false;\n }\n });\n return;\n }).catch(Notification.exception);\n });\n\n $(ACTIONS.SET_CONTENT_VISIBILITY).click(function(e) {\n e.preventDefault();\n\n var contentid = $(this).data('contentid');\n var visibility = $(this).data('visibility');\n\n setContentVisibility(contentid, visibility);\n });\n };\n\n /**\n * Delete content from the content bank.\n *\n * @param {int} contentid The content to delete.\n * @param {int} contextid The contextid where the content belongs.\n */\n function deleteContent(contentid, contextid) {\n var request = {\n methodname: 'core_contentbank_delete_content',\n args: {\n contentids: {contentid}\n }\n };\n\n var requestType = 'success';\n Ajax.call([request])[0].then(function(data) {\n if (data.result) {\n return 'contentdeleted';\n }\n requestType = 'error';\n return 'contentnotdeleted';\n\n }).done(function(message) {\n var params = {\n contextid: contextid\n };\n if (requestType == 'success') {\n params.statusmsg = message;\n } else {\n params.errormsg = message;\n }\n // Redirect to the main content bank page and display the message as a notification.\n window.location.href = Url.relativeUrl('contentbank/index.php', params, false);\n }).fail(Notification.exception);\n }\n\n /**\n * Rename content in the content bank.\n *\n * @param {int} contentid The content to rename.\n * @param {string} name The new name for the content.\n */\n function renameContent(contentid, name) {\n var request = {\n methodname: 'core_contentbank_rename_content',\n args: {\n contentid: contentid,\n name: name\n }\n };\n var requestType = 'success';\n Ajax.call([request])[0].then(function(data) {\n if (data.result) {\n return 'contentrenamed';\n }\n requestType = 'error';\n return data.warnings[0].message;\n\n }).then(function(message) {\n var params = null;\n if (requestType == 'success') {\n params = {\n id: contentid,\n statusmsg: message\n };\n // Redirect to the content view page and display the message as a notification.\n window.location.href = Url.relativeUrl('contentbank/view.php', params, false);\n } else {\n // Fetch error notifications.\n Notification.addNotification({\n message: message,\n type: 'error'\n });\n Notification.fetchNotifications();\n }\n return;\n }).catch(Notification.exception);\n }\n\n /**\n * Copy content in the content bank.\n *\n * @param {int} contentid The content to copy.\n * @param {string} name The name for the new content.\n */\n function copyContent(contentid, name) {\n var request = {\n methodname: 'core_contentbank_copy_content',\n args: {\n contentid: contentid,\n name: name\n }\n };\n Ajax.call([request])[0].then(function(data) {\n if (data.id == 0) {\n // Fetch error notifications.\n Notification.addNotification({\n message: data.warnings[0].message,\n type: 'error'\n });\n Notification.fetchNotifications();\n return data.warnings[0].message;\n } else {\n let params = {\n id: data.id,\n statusmsg: 'contentcopied'\n };\n // Redirect to the content view page and display the message as a notification.\n window.location.href = Url.relativeUrl('contentbank/view.php', params, false);\n }\n return '';\n }).catch(Notification.exception);\n }\n\n /**\n * Set content visibility in the content bank.\n *\n * @param {int} contentid The content to modify\n * @param {int} visibility The new visibility value\n */\n function setContentVisibility(contentid, visibility) {\n var request = {\n methodname: 'core_contentbank_set_content_visibility',\n args: {\n contentid: contentid,\n visibility: visibility\n }\n };\n var requestType = 'success';\n Ajax.call([request])[0].then(function(data) {\n if (data.result) {\n return 'contentvisibilitychanged';\n }\n requestType = 'error';\n return data.warnings[0].message;\n\n }).then(function(message) {\n var params = null;\n if (requestType == 'success') {\n params = {\n id: contentid,\n statusmsg: message\n };\n // Redirect to the content view page and display the message as a notification.\n window.location.href = Url.relativeUrl('contentbank/view.php', params, false);\n } else {\n // Fetch error notifications.\n Notification.addNotification({\n message: message,\n type: 'error'\n });\n Notification.fetchNotifications();\n }\n return;\n }).catch(Notification.exception);\n }\n\n return /** @alias module:core_contentbank/actions */ {\n // Public variables and functions.\n\n /**\n * Initialise the contentbank actions.\n *\n * @method init\n * @return {Actions}\n */\n 'init': function() {\n return new Actions();\n }\n };\n});\n"],"names":["define","$","Ajax","Notification","Str","Templates","Url","ModalSaveCancel","ModalEvents","ACTIONS","Actions","registerEvents","prototype","click","e","preventDefault","contentname","this","data","contentuses","contentid","contextid","strings","key","component","param","name","deleteButtonText","get_strings","then","langStrings","modalTitle","modalContent","create","title","body","large","removeOnClose","show","buttons","save","modal","getRoot","on","request","methodname","args","contentids","requestType","call","result","done","message","params","statusmsg","errormsg","window","location","href","relativeUrl","fail","exception","deleteContent","catch","saveButtonText","render","newname","val","trim","warnings","id","addNotification","type","fetchNotifications","renameContent","alert","errorTitle","errorMessage","copyContent","visibility","setContentVisibility"],"mappings":";;;;;;;AAsBAA,kCAAO,CACH,SACA,YACA,oBACA,WACA,iBACA,WACA,yBACA,sBACJ,SAASC,EAAGC,KAAMC,aAAcC,IAAKC,UAAWC,IAAKC,gBAAiBC,iBAO9DC,uBACgB,gCADhBA,uBAEgB,gCAFhBA,+BAGwB,uCAHxBA,qBAIc,8BAMdC,QAAU,gBACLC,yBAMTD,QAAQE,UAAUD,eAAiB,WAC/BV,EAAEQ,wBAAwBI,OAAM,SAASC,GACrCA,EAAEC,qBAEEC,YAAcf,EAAEgB,MAAMC,KAAK,eAC3BC,YAAclB,EAAEgB,MAAMC,KAAK,QAC3BE,UAAYnB,EAAEgB,MAAMC,KAAK,aACzBG,UAAYpB,EAAEgB,MAAMC,KAAK,aAEzBI,QAAU,CACV,CACIC,IAAK,gBACLC,UAAW,oBAEf,CACID,IAAK,uBACLC,UAAW,mBACXC,MAAO,CACHC,KAAMV,cAGd,CACIO,IAAK,6BACLC,UAAW,oBAEf,CACID,IAAK,SACLC,UAAW,SAIfG,iBAAmB,GACvBvB,IAAIwB,YAAYN,SAASO,MAAK,SAASC,iBAC/BC,WAAaD,YAAY,GACzBE,aAAeF,YAAY,UAC3BX,YAAc,IACda,cAAgB,IAAMF,YAAY,IAEtCH,iBAAmBG,YAAY,GAExBvB,gBAAgB0B,OAAO,CAC1BC,MAAOH,WACPI,KAAMH,aACNI,OAAO,EACPC,eAAe,EACfC,MAAM,EACNC,QAAS,CACLC,KAAMb,uBAGfE,MAAK,SAASY,OACbA,MAAMC,UAAUC,GAAGnC,YAAYgC,MAAM,2BAkI1BpB,UAAWC,eAC1BuB,QAAU,CACVC,WAAY,kCACZC,KAAM,CACFC,WAAY,CAAC3B,UAAAA,aAIjB4B,YAAc,UAClB9C,KAAK+C,KAAK,CAACL,UAAU,GAAGf,MAAK,SAASX,aAC9BA,KAAKgC,OACE,kBAEXF,YAAc,QACP,wBAERG,MAAK,SAASC,aACTC,OAAS,CACThC,UAAWA,WAEI,WAAf2B,YACAK,OAAOC,UAAYF,QAEnBC,OAAOE,SAAWH,QAGtBI,OAAOC,SAASC,KAAOpD,IAAIqD,YAAY,wBAAyBN,QAAQ,MACzEO,KAAKzD,aAAa0D,WA3JFC,CAAc1C,UAAWC,iBAIrC0C,MAAM5D,aAAa0D,cAG1B5D,EAAEQ,wBAAwBI,OAAM,SAASC,GACrCA,EAAEC,qBAEEC,YAAcf,EAAEgB,MAAMC,KAAK,eAC3BE,UAAYnB,EAAEgB,MAAMC,KAAK,aAazB8C,eAAiB,GACrB5D,IAAIwB,YAZU,CACV,CACIL,IAAK,gBACLC,UAAW,oBAEf,CACID,IAAK,SACLC,UAAW,sBAKMK,MAAK,SAASC,iBAC/BC,WAAaD,YAAY,UAC7BkC,eAAiBlC,YAAY,GAEtBvB,gBAAgB0B,OAAO,CAC1BC,MAAOH,WACPI,KAAM9B,UAAU4D,OAAO,iCAAkC,WAAc7C,eAAmBJ,cAC1FqB,eAAe,EACfC,MAAM,EACNC,QAAS,CACLC,KAAMwB,qBAGfnC,MAAK,SAASY,OACbA,MAAMC,UAAUC,GAAGnC,YAAYgC,MAAM,SAAS1B,OAEtCoD,QAAUjE,EAAE,YAAYkE,MAAMC,UAC9BF,kBA0HG9C,UAAWM,UAC1BkB,QAAU,CACVC,WAAY,kCACZC,KAAM,CACF1B,UAAWA,UACXM,KAAMA,OAGVsB,YAAc,UAClB9C,KAAK+C,KAAK,CAACL,UAAU,GAAGf,MAAK,SAASX,aAC9BA,KAAKgC,OACE,kBAEXF,YAAc,QACP9B,KAAKmD,SAAS,GAAGjB,YAEzBvB,MAAK,SAASuB,aACTC,OAAS,KACM,WAAfL,aACAK,OAAS,CACLiB,GAAIlD,UACJkC,UAAWF,SAGfI,OAAOC,SAASC,KAAOpD,IAAIqD,YAAY,uBAAwBN,QAAQ,KAGvElD,aAAaoE,gBAAgB,CACzBnB,QAASA,QACToB,KAAM,UAEVrE,aAAasE,yBAGlBV,MAAM5D,aAAa0D,WA3JNa,CAActD,UAAW8C,aACtB,CAUH9D,IAAIwB,YATe,CACf,CACIL,IAAK,SAET,CACIA,IAAK,sBACLC,UAAW,sBAGWK,MAAK,SAASC,aACxC3B,aAAawE,MAAM7C,YAAY,GAAIA,YAAY,OAChDiC,MAAM5D,aAAa0D,WACtB/C,EAAEC,wBAKXgD,MAAM5D,aAAa0D,cAG1B5D,EAAEQ,sBAAsBI,OAAM,SAASC,GACnCA,EAAEC,qBAEEC,YAAcf,EAAEgB,MAAMC,KAAK,eAC3BE,UAAYnB,EAAEgB,MAAMC,KAAK,iBAgBzB0D,WAAYC,aAChBzE,IAAIwB,YAfU,CACV,CACIL,IAAK,cACLC,UAAW,oBAEf,CACID,IAAK,SAET,CACIA,IAAK,sBACLC,UAAW,sBAKMK,MAAK,SAASC,iBAC/BC,WAAaD,YAAY,UAC7B8C,WAAa9C,YAAY,GACzB+C,aAAe/C,YAAY,GAEpBvB,gBAAgB0B,OAAO,CAC1BC,MAAOH,WACPI,KAAM9B,UAAU4D,OAAO,+BAAgC,WAAc7C,eAAmBJ,cACxFqB,eAAe,EACfC,MAAM,OAEXT,MAAK,SAASY,OACbA,MAAMC,UAAUC,GAAGnC,YAAYgC,MAAM,eAE7B0B,QAAUjE,EAAE,YAAYkE,MAAMC,WAC9BF,eAGA/D,aAAawE,MAAMC,WAAYC,eACxB,YAsGNzD,UAAWM,UACxBkB,QAAU,CACVC,WAAY,gCACZC,KAAM,CACF1B,UAAWA,UACXM,KAAMA,OAGdxB,KAAK+C,KAAK,CAACL,UAAU,GAAGf,MAAK,SAASX,SACnB,GAAXA,KAAKoD,UAELnE,aAAaoE,gBAAgB,CACzBnB,QAASlC,KAAKmD,SAAS,GAAGjB,QAC1BoB,KAAM,UAEVrE,aAAasE,qBACNvD,KAAKmD,SAAS,GAAGjB,QACrB,KACCC,OAAS,CACTiB,GAAIpD,KAAKoD,GACThB,UAAW,iBAGfE,OAAOC,SAASC,KAAOpD,IAAIqD,YAAY,uBAAwBN,QAAQ,SAEpE,MACRU,MAAM5D,aAAa0D,WAnINiB,CAAY1D,UAAW8C,eAOhCH,MAAM5D,aAAa0D,cAG1B5D,EAAEQ,gCAAgCI,OAAM,SAASC,GAC7CA,EAAEC,0BAiIoBK,UAAW2D,gBACjCnC,QAAU,CACVC,WAAY,0CACZC,KAAM,CACF1B,UAAWA,UACX2D,WAAYA,aAGhB/B,YAAc,UAClB9C,KAAK+C,KAAK,CAACL,UAAU,GAAGf,MAAK,SAASX,aAC9BA,KAAKgC,OACE,4BAEXF,YAAc,QACP9B,KAAKmD,SAAS,GAAGjB,YAEzBvB,MAAK,SAASuB,aACTC,OAAS,KACM,WAAfL,aACAK,OAAS,CACLiB,GAAIlD,UACJkC,UAAWF,SAGfI,OAAOC,SAASC,KAAOpD,IAAIqD,YAAY,uBAAwBN,QAAQ,KAGvElD,aAAaoE,gBAAgB,CACzBnB,QAASA,QACToB,KAAM,UAEVrE,aAAasE,yBAGlBV,MAAM5D,aAAa0D,WA9JlBmB,CAHgB/E,EAAEgB,MAAMC,KAAK,aACZjB,EAAEgB,MAAMC,KAAK,mBAmKe,MASzC,kBACG,IAAIR"}PK*\Q!)!hh amd/build/displayunlisted.min.jsnu[define("core_contentbank/displayunlisted",["exports","core/notification","core/pending","core_user/repository"],(function(_exports,_notification,_pending,_repository){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Module to handle toggling "Display unlisted" preference * * @module core_contentbank/displayunlisted * @copyright 2023 Daniel Neis Araujo * @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),_pending=_interopRequireDefault(_pending);_exports.init=elementSelector=>{document.addEventListener("click",(event=>{const element=event.target.closest(elementSelector);if(element){const pendingPromise=new _pending.default("core_contentbank/displayunlisted");(0,_repository.setUserPreference)("core_contentbank_displayunlisted",!!element.checked).then((()=>(pendingPromise.resolve(),document.location.reload()))).catch(_notification.default.exception)}}))}})); //# sourceMappingURL=displayunlisted.min.js.mapPK*\"& amd/build/selectors.min.js.mapnu[{"version":3,"file":"selectors.min.js","sources":["../src/selectors.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 * Define all of the selectors we will be using on the contentbank interface.\n *\n * @module core_contentbank/selectors\n * @copyright 2020 Sara Arjona \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * A small helper function to build queryable data selectors.\n *\n * @method getDataSelector\n * @param {String} name\n * @param {String} value\n * @return {string}\n */\nconst getDataSelector = (name, value) => {\n return `[data-${name}=\"${value}\"]`;\n};\n\nexport default {\n regions: {\n cbcontentname: getDataSelector('region', 'cb-content-name'),\n contentbank: getDataSelector('region', 'contentbank'),\n filearea: getDataSelector('region', 'filearea')\n },\n actions: {\n search: getDataSelector('action', 'searchcontent'),\n clearSearch: getDataSelector('action', 'clearsearch'),\n viewgrid: getDataSelector('action', 'viewgrid'),\n viewlist: getDataSelector('action', 'viewlist'),\n sortname: getDataSelector('action', 'sortname'),\n sortuses: getDataSelector('action', 'sortuses'),\n sortdate: getDataSelector('action', 'sortdate'),\n sortsize: getDataSelector('action', 'sortsize'),\n sorttype: getDataSelector('action', 'sorttype'),\n sortauthor: getDataSelector('action', 'sortauthor'),\n },\n elements: {\n listitem: '.cb-listitem',\n heading: '.cb-heading',\n cell: '.cb-column',\n cbnavbarbreadcrumb: '.cb-navbar-breadbrumb',\n cbnavbartotalsearch: '.cb-navbar-totalsearch',\n searchinput: '[data-action=\"search\"]',\n sortbutton: '.cb-btnsort'\n },\n};\n"],"names":["getDataSelector","name","value","regions","cbcontentname","contentbank","filearea","actions","search","clearSearch","viewgrid","viewlist","sortname","sortuses","sortdate","sortsize","sorttype","sortauthor","elements","listitem","heading","cell","cbnavbarbreadcrumb","cbnavbartotalsearch","searchinput","sortbutton"],"mappings":";;;;;;;;MA+BMA,gBAAkB,CAACC,KAAMC,wBACXD,kBAASC,yBAGd,CACXC,QAAS,CACLC,cAAeJ,gBAAgB,SAAU,mBACzCK,YAAaL,gBAAgB,SAAU,eACvCM,SAAUN,gBAAgB,SAAU,aAExCO,QAAS,CACLC,OAAQR,gBAAgB,SAAU,iBAClCS,YAAaT,gBAAgB,SAAU,eACvCU,SAAUV,gBAAgB,SAAU,YACpCW,SAAUX,gBAAgB,SAAU,YACpCY,SAAUZ,gBAAgB,SAAU,YACpCa,SAAUb,gBAAgB,SAAU,YACpCc,SAAUd,gBAAgB,SAAU,YACpCe,SAAUf,gBAAgB,SAAU,YACpCgB,SAAUhB,gBAAgB,SAAU,YACpCiB,WAAYjB,gBAAgB,SAAU,eAE1CkB,SAAU,CACNC,SAAU,eACVC,QAAS,cACTC,KAAM,aACNC,mBAAoB,wBACpBC,oBAAqB,yBACrBC,YAAa,yBACbC,WAAY"}PK*\amd/build/sort.min.jsnu[define("core_contentbank/sort",["exports","./selectors","core/str","core/prefetch","core/notification","core_user/repository"],(function(_exports,_selectors,_str,_prefetch,_notification,_repository){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Content bank UI actions. * * @module core_contentbank/sort * @copyright 2020 Bas Brands * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_selectors=_interopRequireDefault(_selectors),_prefetch=_interopRequireDefault(_prefetch),_notification=_interopRequireDefault(_notification);_exports.init=()=>{const contentBank=document.querySelector(_selectors.default.regions.contentbank);_prefetch.default.prefetchStrings("contentbank",["contentname","uses","lastmodified","size","type","author"]),_prefetch.default.prefetchStrings("moodle",["sortbyx","sortbyxreverse"]),registerListenerEvents(contentBank)};const registerListenerEvents=contentBank=>{contentBank.addEventListener("click",(e=>{const viewList=contentBank.querySelector(_selectors.default.actions.viewlist),viewGrid=contentBank.querySelector(_selectors.default.actions.viewgrid),fileArea=contentBank.querySelector(_selectors.default.regions.filearea),shownItems=fileArea.querySelectorAll(_selectors.default.elements.listitem);if(e.target.closest(_selectors.default.actions.viewgrid)){if(contentBank.classList.remove("view-list"),contentBank.classList.add("view-grid"),fileArea&&shownItems){fileArea.setAttribute("role","list"),shownItems.forEach((listItem=>{listItem.setAttribute("role","listitem"),listItem.querySelectorAll(_selectors.default.elements.cell).forEach((cell=>cell.removeAttribute("role")))}));const heading=fileArea.querySelector(_selectors.default.elements.heading);heading&&(heading.removeAttribute("role"),heading.querySelectorAll(_selectors.default.elements.cell).forEach((cell=>cell.removeAttribute("role"))))}return viewGrid.classList.add("active"),viewList.classList.remove("active"),void setViewListPreference(!1)}if(e.target.closest(_selectors.default.actions.viewlist)){if(contentBank.classList.remove("view-grid"),contentBank.classList.add("view-list"),fileArea&&shownItems){fileArea.setAttribute("role","table"),shownItems.forEach((listItem=>{listItem.setAttribute("role","row"),listItem.querySelectorAll(_selectors.default.elements.cell).forEach((cell=>cell.setAttribute("role","cell")))}));const heading=fileArea.querySelector(_selectors.default.elements.heading);heading&&(heading.setAttribute("role","row"),heading.querySelectorAll(_selectors.default.elements.cell).forEach((cell=>cell.setAttribute("role","columnheader"))))}return viewList.classList.add("active"),viewGrid.classList.remove("active"),void setViewListPreference(!0)}if(fileArea&&shownItems){const sortByName=e.target.closest(_selectors.default.actions.sortname);if(sortByName){const ascending=updateSortButtons(contentBank,sortByName);return void updateSortOrder(fileArea,shownItems,"data-file",ascending)}const sortByUses=e.target.closest(_selectors.default.actions.sortuses);if(sortByUses){const ascending=updateSortButtons(contentBank,sortByUses);return void updateSortOrder(fileArea,shownItems,"data-uses",ascending)}const sortByDate=e.target.closest(_selectors.default.actions.sortdate);if(sortByDate){const ascending=updateSortButtons(contentBank,sortByDate);return void updateSortOrder(fileArea,shownItems,"data-timemodified",ascending)}const sortBySize=e.target.closest(_selectors.default.actions.sortsize);if(sortBySize){const ascending=updateSortButtons(contentBank,sortBySize);return void updateSortOrder(fileArea,shownItems,"data-bytes",ascending)}const sortByType=e.target.closest(_selectors.default.actions.sorttype);if(sortByType){const ascending=updateSortButtons(contentBank,sortByType);return void updateSortOrder(fileArea,shownItems,"data-type",ascending)}const sortByAuthor=e.target.closest(_selectors.default.actions.sortauthor);if(sortByAuthor){const ascending=updateSortButtons(contentBank,sortByAuthor);updateSortOrder(fileArea,shownItems,"data-author",ascending)}}else;}))},setViewListPreference=function(viewList){return!1===viewList&&(viewList=null),(0,_repository.setUserPreference)("core_contentbank_view_list",viewList).catch(_notification.default.exception)},updateSortButtons=(contentBank,sortButton)=>{contentBank.querySelectorAll(_selectors.default.elements.sortbutton).forEach((button=>{button!==sortButton&&(button.classList.remove("dir-asc"),button.classList.remove("dir-desc"),button.classList.add("dir-none"),button.closest(_selectors.default.elements.cell).setAttribute("aria-sort","none"),updateButtonTitle(button,!1))}));let ascending=!0;return sortButton.classList.contains("dir-none")?(sortButton.classList.remove("dir-none"),sortButton.classList.add("dir-asc"),sortButton.closest(_selectors.default.elements.cell).setAttribute("aria-sort","ascending")):sortButton.classList.contains("dir-asc")?(sortButton.classList.remove("dir-asc"),sortButton.classList.add("dir-desc"),sortButton.closest(_selectors.default.elements.cell).setAttribute("aria-sort","descending"),ascending=!1):sortButton.classList.contains("dir-desc")&&(sortButton.classList.remove("dir-desc"),sortButton.classList.add("dir-asc"),sortButton.closest(_selectors.default.elements.cell).setAttribute("aria-sort","ascending")),updateButtonTitle(sortButton,ascending),ascending},updateButtonTitle=(button,ascending)=>{const sortString=ascending?"sortbyxreverse":"sortbyx";return(0,_str.getString)(button.dataset.string,"contentbank").then((columnName=>(0,_str.getString)(sortString,"core",columnName))).then((sortByString=>(button.setAttribute("title",sortByString),sortByString))).catch()},updateSortOrder=(fileArea,itemList,attribute,ascending)=>{[].slice.call(itemList).sort((function(a,b){let aa=a.getAttribute(attribute),bb=b.getAttribute(attribute);return isNaN(aa)||(aa=parseInt(aa),bb=parseInt(bb)),ascending?aa>bb?1:-1:aafileArea.appendChild(listItem)))}})); //# sourceMappingURL=sort.min.js.mapPK*\Kamd/build/selectors.min.jsnu[define("core_contentbank/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0; /** * Define all of the selectors we will be using on the contentbank interface. * * @module core_contentbank/selectors * @copyright 2020 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ const getDataSelector=(name,value)=>"[data-".concat(name,'="').concat(value,'"]');var _default={regions:{cbcontentname:getDataSelector("region","cb-content-name"),contentbank:getDataSelector("region","contentbank"),filearea:getDataSelector("region","filearea")},actions:{search:getDataSelector("action","searchcontent"),clearSearch:getDataSelector("action","clearsearch"),viewgrid:getDataSelector("action","viewgrid"),viewlist:getDataSelector("action","viewlist"),sortname:getDataSelector("action","sortname"),sortuses:getDataSelector("action","sortuses"),sortdate:getDataSelector("action","sortdate"),sortsize:getDataSelector("action","sortsize"),sorttype:getDataSelector("action","sorttype"),sortauthor:getDataSelector("action","sortauthor")},elements:{listitem:".cb-listitem",heading:".cb-heading",cell:".cb-column",cbnavbarbreadcrumb:".cb-navbar-breadbrumb",cbnavbartotalsearch:".cb-navbar-totalsearch",searchinput:'[data-action="search"]',sortbutton:".cb-btnsort"}};return _exports.default=_default,_exports.default})); //# sourceMappingURL=selectors.min.js.mapPK*\,-$amd/build/displayunlisted.min.js.mapnu[{"version":3,"file":"displayunlisted.min.js","sources":["../src/displayunlisted.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 * Module to handle toggling \"Display unlisted\" preference\n *\n * @module core_contentbank/displayunlisted\n * @copyright 2023 Daniel Neis Araujo\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport {setUserPreference} from 'core_user/repository';\n\n/**\n * Initialize module, add event listeners\n *\n * @param {String} elementSelector\n */\nexport const init = elementSelector => {\n document.addEventListener('click', event => {\n const element = event.target.closest(elementSelector);\n if (element) {\n const pendingPromise = new Pending('core_contentbank/displayunlisted');\n\n setUserPreference('core_contentbank_displayunlisted', !!element.checked)\n .then(() => {\n pendingPromise.resolve();\n return document.location.reload();\n })\n .catch(Notification.exception);\n }\n });\n};\n"],"names":["elementSelector","document","addEventListener","event","element","target","closest","pendingPromise","Pending","checked","then","resolve","location","reload","catch","Notification","exception"],"mappings":";;;;;;;8LAgCoBA,kBAChBC,SAASC,iBAAiB,SAASC,cACzBC,QAAUD,MAAME,OAAOC,QAAQN,oBACjCI,QAAS,OACHG,eAAiB,IAAIC,iBAAQ,sEAEjB,qCAAsCJ,QAAQK,SAC3DC,MAAK,KACFH,eAAeI,UACRV,SAASW,SAASC,YAE5BC,MAAMC,sBAAaC"}PK*\b77amd/build/sort.min.js.mapnu[{"version":3,"file":"sort.min.js","sources":["../src/sort.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 * Content bank UI actions.\n *\n * @module core_contentbank/sort\n * @copyright 2020 Bas Brands \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport selectors from './selectors';\nimport {getString} from 'core/str';\nimport Prefetch from 'core/prefetch';\nimport Notification from 'core/notification';\nimport {setUserPreference} from 'core_user/repository';\n\n/**\n * Set up the contentbank views.\n *\n * @method init\n */\nexport const init = () => {\n const contentBank = document.querySelector(selectors.regions.contentbank);\n Prefetch.prefetchStrings('contentbank', ['contentname', 'uses', 'lastmodified', 'size', 'type', 'author']);\n Prefetch.prefetchStrings('moodle', ['sortbyx', 'sortbyxreverse']);\n registerListenerEvents(contentBank);\n};\n\n/**\n * Register contentbank related event listeners.\n *\n * @method registerListenerEvents\n * @param {HTMLElement} contentBank The DOM node of the content bank\n */\nconst registerListenerEvents = (contentBank) => {\n\n contentBank.addEventListener('click', e => {\n const viewList = contentBank.querySelector(selectors.actions.viewlist);\n const viewGrid = contentBank.querySelector(selectors.actions.viewgrid);\n const fileArea = contentBank.querySelector(selectors.regions.filearea);\n const shownItems = fileArea.querySelectorAll(selectors.elements.listitem);\n\n // View as Grid button.\n if (e.target.closest(selectors.actions.viewgrid)) {\n contentBank.classList.remove('view-list');\n contentBank.classList.add('view-grid');\n if (fileArea && shownItems) {\n fileArea.setAttribute('role', 'list');\n shownItems.forEach(listItem => {\n listItem.setAttribute('role', 'listitem');\n listItem.querySelectorAll(selectors.elements.cell).forEach(cell => cell.removeAttribute('role'));\n });\n\n const heading = fileArea.querySelector(selectors.elements.heading);\n if (heading) {\n heading.removeAttribute('role');\n heading.querySelectorAll(selectors.elements.cell).forEach(cell => cell.removeAttribute('role'));\n }\n }\n viewGrid.classList.add('active');\n viewList.classList.remove('active');\n setViewListPreference(false);\n\n return;\n }\n\n // View as List button.\n if (e.target.closest(selectors.actions.viewlist)) {\n contentBank.classList.remove('view-grid');\n contentBank.classList.add('view-list');\n if (fileArea && shownItems) {\n fileArea.setAttribute('role', 'table');\n shownItems.forEach(listItem => {\n listItem.setAttribute('role', 'row');\n listItem.querySelectorAll(selectors.elements.cell).forEach(cell => cell.setAttribute('role', 'cell'));\n });\n\n const heading = fileArea.querySelector(selectors.elements.heading);\n if (heading) {\n heading.setAttribute('role', 'row');\n heading.querySelectorAll(selectors.elements.cell).forEach(cell => cell.setAttribute('role', 'columnheader'));\n }\n }\n viewList.classList.add('active');\n viewGrid.classList.remove('active');\n setViewListPreference(true);\n\n return;\n }\n\n if (fileArea && shownItems) {\n\n // Sort by file name alphabetical\n const sortByName = e.target.closest(selectors.actions.sortname);\n if (sortByName) {\n const ascending = updateSortButtons(contentBank, sortByName);\n updateSortOrder(fileArea, shownItems, 'data-file', ascending);\n return;\n }\n\n // Sort by uses.\n const sortByUses = e.target.closest(selectors.actions.sortuses);\n if (sortByUses) {\n const ascending = updateSortButtons(contentBank, sortByUses);\n updateSortOrder(fileArea, shownItems, 'data-uses', ascending);\n return;\n }\n\n // Sort by date.\n const sortByDate = e.target.closest(selectors.actions.sortdate);\n if (sortByDate) {\n const ascending = updateSortButtons(contentBank, sortByDate);\n updateSortOrder(fileArea, shownItems, 'data-timemodified', ascending);\n return;\n }\n\n // Sort by size.\n const sortBySize = e.target.closest(selectors.actions.sortsize);\n if (sortBySize) {\n const ascending = updateSortButtons(contentBank, sortBySize);\n updateSortOrder(fileArea, shownItems, 'data-bytes', ascending);\n return;\n }\n\n // Sort by type.\n const sortByType = e.target.closest(selectors.actions.sorttype);\n if (sortByType) {\n const ascending = updateSortButtons(contentBank, sortByType);\n updateSortOrder(fileArea, shownItems, 'data-type', ascending);\n return;\n }\n\n // Sort by author.\n const sortByAuthor = e.target.closest(selectors.actions.sortauthor);\n if (sortByAuthor) {\n const ascending = updateSortButtons(contentBank, sortByAuthor);\n updateSortOrder(fileArea, shownItems, 'data-author', ascending);\n }\n return;\n }\n });\n};\n\n\n/**\n * Set the contentbank user preference in list view\n *\n * @param {Bool} viewList view ContentBank as list.\n * @return {Promise} Repository promise.\n */\nconst setViewListPreference = function(viewList) {\n\n // If the given status is not hidden, the preference has to be deleted with a null value.\n if (viewList === false) {\n viewList = null;\n }\n\n return setUserPreference('core_contentbank_view_list', viewList)\n .catch(Notification.exception);\n};\n\n/**\n * Update the sort button view.\n *\n * @method updateSortButtons\n * @param {HTMLElement} contentBank The DOM node of the contentbank button\n * @param {HTMLElement} sortButton The DOM node of the sort button\n * @return {Bool} sort ascending\n */\nconst updateSortButtons = (contentBank, sortButton) => {\n const sortButtons = contentBank.querySelectorAll(selectors.elements.sortbutton);\n\n sortButtons.forEach((button) => {\n if (button !== sortButton) {\n button.classList.remove('dir-asc');\n button.classList.remove('dir-desc');\n button.classList.add('dir-none');\n\n button.closest(selectors.elements.cell).setAttribute('aria-sort', 'none');\n\n updateButtonTitle(button, false);\n }\n });\n\n let ascending = true;\n\n if (sortButton.classList.contains('dir-none')) {\n sortButton.classList.remove('dir-none');\n sortButton.classList.add('dir-asc');\n sortButton.closest(selectors.elements.cell).setAttribute('aria-sort', 'ascending');\n } else if (sortButton.classList.contains('dir-asc')) {\n sortButton.classList.remove('dir-asc');\n sortButton.classList.add('dir-desc');\n sortButton.closest(selectors.elements.cell).setAttribute('aria-sort', 'descending');\n ascending = false;\n } else if (sortButton.classList.contains('dir-desc')) {\n sortButton.classList.remove('dir-desc');\n sortButton.classList.add('dir-asc');\n sortButton.closest(selectors.elements.cell).setAttribute('aria-sort', 'ascending');\n }\n\n updateButtonTitle(sortButton, ascending);\n\n return ascending;\n};\n\n/**\n * Update the button title.\n *\n * @method updateButtonTitle\n * @param {HTMLElement} button Button to update\n * @param {Bool} ascending Sort direction\n * @return {Promise} string promise\n */\nconst updateButtonTitle = (button, ascending) => {\n\n const sortString = (ascending ? 'sortbyxreverse' : 'sortbyx');\n\n return getString(button.dataset.string, 'contentbank')\n .then(columnName => {\n return getString(sortString, 'core', columnName);\n })\n .then(sortByString => {\n button.setAttribute('title', sortByString);\n return sortByString;\n })\n .catch();\n};\n\n/**\n * Update the sort order of the itemlist and update the DOM\n *\n * @method updateSortOrder\n * @param {HTMLElement} fileArea the Dom container for the itemlist\n * @param {Array} itemList Nodelist of Dom elements\n * @param {String} attribute the attribut to sort on\n * @param {Bool} ascending Sort Ascending\n */\nconst updateSortOrder = (fileArea, itemList, attribute, ascending) => {\n const sortList = [].slice.call(itemList).sort(function(a, b) {\n\n let aa = a.getAttribute(attribute);\n let bb = b.getAttribute(attribute);\n if (!isNaN(aa)) {\n aa = parseInt(aa);\n bb = parseInt(bb);\n }\n\n if (ascending) {\n return aa > bb ? 1 : -1;\n } else {\n return aa < bb ? 1 : -1;\n }\n });\n sortList.forEach(listItem => fileArea.appendChild(listItem));\n};\n"],"names":["contentBank","document","querySelector","selectors","regions","contentbank","prefetchStrings","registerListenerEvents","addEventListener","e","viewList","actions","viewlist","viewGrid","viewgrid","fileArea","filearea","shownItems","querySelectorAll","elements","listitem","target","closest","classList","remove","add","setAttribute","forEach","listItem","cell","removeAttribute","heading","setViewListPreference","sortByName","sortname","ascending","updateSortButtons","updateSortOrder","sortByUses","sortuses","sortByDate","sortdate","sortBySize","sortsize","sortByType","sorttype","sortByAuthor","sortauthor","catch","Notification","exception","sortButton","sortbutton","button","updateButtonTitle","contains","sortString","dataset","string","then","columnName","sortByString","itemList","attribute","slice","call","sort","a","b","aa","getAttribute","bb","isNaN","parseInt","appendChild"],"mappings":";;;;;;;8OAkCoB,WACVA,YAAcC,SAASC,cAAcC,mBAAUC,QAAQC,+BACpDC,gBAAgB,cAAe,CAAC,cAAe,OAAQ,eAAgB,OAAQ,OAAQ,6BACvFA,gBAAgB,SAAU,CAAC,UAAW,mBAC/CC,uBAAuBP,oBASrBO,uBAA0BP,cAE5BA,YAAYQ,iBAAiB,SAASC,UAC5BC,SAAWV,YAAYE,cAAcC,mBAAUQ,QAAQC,UACvDC,SAAWb,YAAYE,cAAcC,mBAAUQ,QAAQG,UACvDC,SAAWf,YAAYE,cAAcC,mBAAUC,QAAQY,UACvDC,WAAaF,SAASG,iBAAiBf,mBAAUgB,SAASC,aAG5DX,EAAEY,OAAOC,QAAQnB,mBAAUQ,QAAQG,UAAW,IAC9Cd,YAAYuB,UAAUC,OAAO,aAC7BxB,YAAYuB,UAAUE,IAAI,aACtBV,UAAYE,WAAY,CACxBF,SAASW,aAAa,OAAQ,QAC9BT,WAAWU,SAAQC,WACfA,SAASF,aAAa,OAAQ,YAC9BE,SAASV,iBAAiBf,mBAAUgB,SAASU,MAAMF,SAAQE,MAAQA,KAAKC,gBAAgB,mBAGtFC,QAAUhB,SAASb,cAAcC,mBAAUgB,SAASY,SACtDA,UACAA,QAAQD,gBAAgB,QACxBC,QAAQb,iBAAiBf,mBAAUgB,SAASU,MAAMF,SAAQE,MAAQA,KAAKC,gBAAgB,kBAG/FjB,SAASU,UAAUE,IAAI,UACvBf,SAASa,UAAUC,OAAO,eAC1BQ,uBAAsB,MAMtBvB,EAAEY,OAAOC,QAAQnB,mBAAUQ,QAAQC,UAAW,IAC9CZ,YAAYuB,UAAUC,OAAO,aAC7BxB,YAAYuB,UAAUE,IAAI,aACtBV,UAAYE,WAAY,CACxBF,SAASW,aAAa,OAAQ,SAC9BT,WAAWU,SAAQC,WACfA,SAASF,aAAa,OAAQ,OAC9BE,SAASV,iBAAiBf,mBAAUgB,SAASU,MAAMF,SAAQE,MAAQA,KAAKH,aAAa,OAAQ,mBAG3FK,QAAUhB,SAASb,cAAcC,mBAAUgB,SAASY,SACtDA,UACAA,QAAQL,aAAa,OAAQ,OAC7BK,QAAQb,iBAAiBf,mBAAUgB,SAASU,MAAMF,SAAQE,MAAQA,KAAKH,aAAa,OAAQ,0BAGpGhB,SAASa,UAAUE,IAAI,UACvBZ,SAASU,UAAUC,OAAO,eAC1BQ,uBAAsB,MAKtBjB,UAAYE,kBAGNgB,WAAaxB,EAAEY,OAAOC,QAAQnB,mBAAUQ,QAAQuB,aAClDD,WAAY,OACNE,UAAYC,kBAAkBpC,YAAaiC,wBACjDI,gBAAgBtB,SAAUE,WAAY,YAAakB,iBAKjDG,WAAa7B,EAAEY,OAAOC,QAAQnB,mBAAUQ,QAAQ4B,aAClDD,WAAY,OACNH,UAAYC,kBAAkBpC,YAAasC,wBACjDD,gBAAgBtB,SAAUE,WAAY,YAAakB,iBAKjDK,WAAa/B,EAAEY,OAAOC,QAAQnB,mBAAUQ,QAAQ8B,aAClDD,WAAY,OACNL,UAAYC,kBAAkBpC,YAAawC,wBACjDH,gBAAgBtB,SAAUE,WAAY,oBAAqBkB,iBAKzDO,WAAajC,EAAEY,OAAOC,QAAQnB,mBAAUQ,QAAQgC,aAClDD,WAAY,OACNP,UAAYC,kBAAkBpC,YAAa0C,wBACjDL,gBAAgBtB,SAAUE,WAAY,aAAckB,iBAKlDS,WAAanC,EAAEY,OAAOC,QAAQnB,mBAAUQ,QAAQkC,aAClDD,WAAY,OACNT,UAAYC,kBAAkBpC,YAAa4C,wBACjDP,gBAAgBtB,SAAUE,WAAY,YAAakB,iBAKjDW,aAAerC,EAAEY,OAAOC,QAAQnB,mBAAUQ,QAAQoC,eACpDD,aAAc,OACRX,UAAYC,kBAAkBpC,YAAa8C,cACjDT,gBAAgBtB,SAAUE,WAAY,cAAekB,sBAc/DH,sBAAwB,SAAStB,iBAGlB,IAAbA,WACAA,SAAW,OAGR,iCAAkB,6BAA8BA,UAClDsC,MAAMC,sBAAaC,YAWtBd,kBAAoB,CAACpC,YAAamD,cAChBnD,YAAYkB,iBAAiBf,mBAAUgB,SAASiC,YAExDzB,SAAS0B,SACbA,SAAWF,aACXE,OAAO9B,UAAUC,OAAO,WACxB6B,OAAO9B,UAAUC,OAAO,YACxB6B,OAAO9B,UAAUE,IAAI,YAErB4B,OAAO/B,QAAQnB,mBAAUgB,SAASU,MAAMH,aAAa,YAAa,QAElE4B,kBAAkBD,QAAQ,WAI9BlB,WAAY,SAEZgB,WAAW5B,UAAUgC,SAAS,aAC9BJ,WAAW5B,UAAUC,OAAO,YAC5B2B,WAAW5B,UAAUE,IAAI,WACzB0B,WAAW7B,QAAQnB,mBAAUgB,SAASU,MAAMH,aAAa,YAAa,cAC/DyB,WAAW5B,UAAUgC,SAAS,YACrCJ,WAAW5B,UAAUC,OAAO,WAC5B2B,WAAW5B,UAAUE,IAAI,YACzB0B,WAAW7B,QAAQnB,mBAAUgB,SAASU,MAAMH,aAAa,YAAa,cACtES,WAAY,GACLgB,WAAW5B,UAAUgC,SAAS,cACrCJ,WAAW5B,UAAUC,OAAO,YAC5B2B,WAAW5B,UAAUE,IAAI,WACzB0B,WAAW7B,QAAQnB,mBAAUgB,SAASU,MAAMH,aAAa,YAAa,cAG1E4B,kBAAkBH,WAAYhB,WAEvBA,WAWLmB,kBAAoB,CAACD,OAAQlB,mBAEzBqB,WAAcrB,UAAY,iBAAmB,iBAE5C,kBAAUkB,OAAOI,QAAQC,OAAQ,eACvCC,MAAKC,aACK,kBAAUJ,WAAY,OAAQI,cAExCD,MAAKE,eACFR,OAAO3B,aAAa,QAASmC,cACtBA,gBAEVb,SAYCX,gBAAkB,CAACtB,SAAU+C,SAAUC,UAAW5B,aACnC,GAAG6B,MAAMC,KAAKH,UAAUI,MAAK,SAASC,EAAGC,OAElDC,GAAKF,EAAEG,aAAaP,WACpBQ,GAAKH,EAAEE,aAAaP,kBACnBS,MAAMH,MACRA,GAAKI,SAASJ,IACdE,GAAKE,SAASF,KAGbpC,UACOkC,GAAKE,GAAK,GAAK,EAEfF,GAAKE,GAAK,GAAK,KAGrB5C,SAAQC,UAAYb,SAAS2D,YAAY9C"}PK*\1l amd/build/search.min.jsnu[define("core_contentbank/search",["exports","jquery","core_contentbank/selectors","core/str","core/pending","core/utils"],(function(_exports,_jquery,_selectors,_str,_pending,_utils){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Search methods for finding contents in the content bank. * * @module core_contentbank/search * @copyright 2020 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=_interopRequireDefault(_jquery),_selectors=_interopRequireDefault(_selectors),_pending=_interopRequireDefault(_pending);_exports.init=()=>{const pendingPromise=new _pending.default,root=(0,_jquery.default)(_selectors.default.regions.contentbank);registerListenerEvents(root),pendingPromise.resolve()};const registerListenerEvents=root=>{const searchInput=root.find(_selectors.default.elements.searchinput)[0];root.on("click",_selectors.default.actions.search,(function(e){e.preventDefault(),toggleSearchResultsView(root,searchInput.value)})),root.on("click",_selectors.default.actions.clearSearch,(function(e){e.preventDefault(),searchInput.value="",searchInput.focus(),toggleSearchResultsView(root,searchInput.value)})),searchInput.addEventListener("input",(0,_utils.debounce)((()=>{toggleSearchResultsView(root,searchInput.value)}),300))},toggleSearchResultsView=async(body,searchQuery)=>{const clearSearchButton=body.find(_selectors.default.actions.clearSearch)[0],navbarBreadcrumb=body.find(_selectors.default.elements.cbnavbarbreadcrumb)[0],navbarTotal=body.find(_selectors.default.elements.cbnavbartotalsearch)[0],filteredContents=filterContents(body,searchQuery);searchQuery.length>0?(clearSearchButton.classList.remove("d-none"),navbarBreadcrumb.classList.add("d-none"),navbarTotal.innerHTML=await(0,_str.getString)("itemsfound","core_contentbank",filteredContents.length),navbarTotal.classList.remove("d-none")):(clearSearchButton.classList.add("d-none"),navbarBreadcrumb.classList.remove("d-none"),navbarTotal.classList.add("d-none"))},filterContents=(body,searchTerm)=>{const contents=Array.from(body.find(_selectors.default.elements.listitem)),searchResults=[];return contents.forEach((content=>{const contentName=content.getAttribute("data-name");if(""===searchTerm||contentName.toLowerCase().includes(searchTerm.toLowerCase())){searchResults.push(content);content.querySelector(_selectors.default.regions.cbcontentname).innerHTML=highlight(contentName,searchTerm),content.classList.remove("d-none")}else content.classList.add("d-none")})),searchResults},highlight=(text,highlightText)=>{let result=text;if(""!==highlightText){const pos=text.toLowerCase().indexOf(highlightText.toLowerCase());pos>-1&&(result=text.substr(0,pos)+''+text.substr(pos,highlightText.length)+""+text.substr(pos+highlightText.length))}return result}})); //# sourceMappingURL=search.min.js.mapPK*\kxYamd/build/upload.min.jsnu[define("core_contentbank/upload",["exports","core_form/modalform","core/str"],(function(_exports,_modalform,_str){var obj; /** * Module to handle AJAX interactions with content bank upload files. * * @module core_contentbank/upload * @copyright 2021 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.initModal=void 0,_modalform=(obj=_modalform)&&obj.__esModule?obj:{default:obj};_exports.initModal=(elementSelector,formClass,contextId,contentId)=>{document.querySelector(elementSelector).addEventListener("click",(function(e){e.preventDefault();const form=new _modalform.default({formClass:formClass,args:{contextid:contextId,id:contentId},modalConfig:{title:(0,_str.getString)("upload","contentbank")},returnFocus:e.target});form.addEventListener(form.events.FORM_SUBMITTED,(event=>{document.location=event.detail.returnurl})),form.show()}))}})); //# sourceMappingURL=upload.min.js.mapPK*\Q Q amd/build/upload.min.js.mapnu[{"version":3,"file":"upload.min.js","sources":["../src/upload.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 * Module to handle AJAX interactions with content bank upload files.\n *\n * @module core_contentbank/upload\n * @copyright 2021 Sara Arjona \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport ModalForm from 'core_form/modalform';\nimport {getString} from 'core/str';\n\n/**\n * Initialize upload files to the content bank form as Modal form.\n *\n * @param {String} elementSelector\n * @param {String} formClass\n * @param {Integer} contextId\n * @param {Integer} contentId\n */\nexport const initModal = (elementSelector, formClass, contextId, contentId) => {\n const element = document.querySelector(elementSelector);\n element.addEventListener('click', function(e) {\n e.preventDefault();\n const form = new ModalForm({\n formClass,\n args: {\n contextid: contextId,\n id: contentId,\n },\n modalConfig: {title: getString('upload', 'contentbank')},\n returnFocus: e.target,\n });\n form.addEventListener(form.events.FORM_SUBMITTED, (event) => {\n document.location = event.detail.returnurl;\n });\n form.show();\n });\n};\n"],"names":["elementSelector","formClass","contextId","contentId","document","querySelector","addEventListener","e","preventDefault","form","ModalForm","args","contextid","id","modalConfig","title","returnFocus","target","events","FORM_SUBMITTED","event","location","detail","returnurl","show"],"mappings":";;;;;;;wKAiCyB,CAACA,gBAAiBC,UAAWC,UAAWC,aAC7CC,SAASC,cAAcL,iBAC/BM,iBAAiB,SAAS,SAASC,GACvCA,EAAEC,uBACIC,KAAO,IAAIC,mBAAU,CACvBT,UAAAA,UACAU,KAAM,CACFC,UAAWV,UACXW,GAAIV,WAERW,YAAa,CAACC,OAAO,kBAAU,SAAU,gBACzCC,YAAaT,EAAEU,SAEnBR,KAAKH,iBAAiBG,KAAKS,OAAOC,gBAAiBC,QAC/ChB,SAASiB,SAAWD,MAAME,OAAOC,aAErCd,KAAKe"}PK*\M9DDcontenttype/h5p/db/access.phpnu[. /** * H5P content bank contenttype capabilities. * * @package contenttype_h5p * @copyright 2020 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = [ 'contenttype/h5p:access' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_COURSE, 'archetypes' => array( 'manager' => CAP_ALLOW, 'coursecreator' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, ) ), 'contenttype/h5p:upload' => [ 'riskbitmask' => RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_COURSE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'coursecreator' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, ] ], 'contenttype/h5p:useeditor' => [ 'riskbitmask' => RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_COURSE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'coursecreator' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, ] ], ]; PK*\b}AA+contenttype/h5p/lang/en/contenttype_h5p.phpnu[. /** * Strings for plugin 'contenttype_h5p' * * @package contenttype_h5p * @copyright 2020 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['description'] = 'H5P interactive content'; $string['pluginname'] = 'H5P'; $string['pluginname_help'] = 'Content bank to upload and share H5P content'; $string['privacy:metadata'] = 'The H5P content bank plugin does not store any personal data.'; $string['h5p:access'] = 'Access H5P content in the content bank'; $string['h5p:upload'] = 'Upload new H5P content'; $string['h5p:useeditor'] = 'Create or edit content using the H5P editor'; PK*\Rcontenttype/h5p/version.phpnu[. /** * Version details * * @package contenttype_h5p * @copyright 2020 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'contenttype_h5p'; // Full name of the plugin (used for diagnostics). PK*\Z'contenttype/h5p/classes/form/editor.phpnu[. /** * Provides the class that defines the form for the H5P authoring tool. * * @package contenttype_h5p * @copyright 2020 Victor Deniz * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace contenttype_h5p\form; use contenttype_h5p\content; use contenttype_h5p\contenttype; use core_contentbank\form\edit_content; use core_h5p\api; use core_h5p\editor as h5peditor; use core_h5p\factory; use core_h5p\helper; use stdClass; /** * Defines the form for editing an H5P content. * * @copyright 2020 Victor Deniz * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class editor extends edit_content { /** @var $h5peditor H5P editor object */ private $h5peditor; /** @var $content The content being edited */ private $content; /** * Defines the form fields. */ protected function definition() { global $DB, $OUTPUT; $mform = $this->_form; $errors = []; $notifications = []; // Id of the content to edit. $id = $this->_customdata['id']; // H5P content type to create. $library = optional_param('library', null, PARAM_TEXT); if (empty($id) && empty($library)) { $returnurl = new \moodle_url('/contentbank/index.php', ['contextid' => $this->_customdata['contextid']]); throw new \moodle_exception('invalidcontentid', 'error', $returnurl); } $this->h5peditor = new h5peditor(); $this->set_display_vertical(); $mform->addElement('html', $OUTPUT->heading($this->_customdata['heading'], 2)); if ($id) { // The H5P editor needs the H5P content id (h5p table). $record = $DB->get_record('contentbank_content', ['id' => $id]); $this->content = new content($record); $file = $this->content->get_file(); $h5p = api::get_content_from_pathnamehash($file->get_pathnamehash()); if (!$h5p) { // H5P content has not been deployed yet. Let's check why. $factory = new \core_h5p\factory(); $factory->get_framework()->set_file($file); $h5pid = helper::save_h5p($factory, $file, new stdClass()); $errors = $factory->get_framework()->getMessages('error'); $notifications = $factory->get_framework()->getMessages('info'); } else { $h5pid = $h5p->id; } if ($h5pid) { $mform->addElement('hidden', 'h5pid', $h5pid); $mform->setType('h5pid', PARAM_INT); $this->h5peditor->set_content($h5pid); } } else { // The H5P editor needs the H5P content type library name for a new content. $mform->addElement('hidden', 'library', $library); $mform->setType('library', PARAM_TEXT); $this->h5peditor->set_library($library, $this->_customdata['contextid'], 'contentbank', 'public'); } $mformid = 'coolh5peditor'; $mform->setAttributes(array('id' => $mformid) + $mform->getAttributes()); if ($errors || $notifications) { // Show the error messages and a Cancel button. foreach ($errors as $error) { $mform->addElement('warning', $error->code, 'notify', $error->message); } foreach ($notifications as $key => $notification) { $mform->addElement('warning', 'notification_'.$key, 'notify', $notification); } $mform->addElement('cancel', 'cancel', get_string('back')); } else { $this->h5peditor->add_editor_to_form($mform); parent::definition(); $this->add_action_buttons(); } } /** * Modify or create an H5P content from the form data. * * @param stdClass $data Form data to create or modify an H5P content. * * @return int The id of the edited or created content. */ public function save_content(stdClass $data): int { global $DB; // The H5P libraries expect data->id as the H5P content id. // The method H5PCore::saveContent throws an error if id is set but empty. if (empty($data->id)) { unset($data->id); } else { // The H5P libraries save in $data->id the H5P content id (h5p table), so the content id is saved in another var. $contentid = $data->id; } $h5pcontentid = $this->h5peditor->save_content($data); $factory = new factory(); $h5pfs = $factory->get_framework(); // Needs the H5P file id to create or update the content bank record. $h5pcontent = $h5pfs->loadContent($h5pcontentid); $fs = get_file_storage(); $file = $fs->get_file_by_hash($h5pcontent['pathnamehash']); // Creating new content. if (!isset($data->h5pid)) { // The initial name of the content is the title of the H5P content. $cbrecord = new stdClass(); $cbrecord->name = json_decode($data->h5pparams)->metadata->title; $context = \context::instance_by_id($data->contextid, MUST_EXIST); // Create entry in content bank. $contenttype = new contenttype($context); $newcontent = $contenttype->create_content($cbrecord); $cfdata = fullclone($data); $cfdata->id = $newcontent->get_id(); $handler = \core_contentbank\customfield\content_handler::create(); $handler->instance_form_save($cfdata, true); if ($file && $newcontent) { $updatedfilerecord = new stdClass(); $updatedfilerecord->id = $file->get_id(); $updatedfilerecord->itemid = $newcontent->get_id(); // As itemid changed, the pathnamehash has to be updated in the file table. $pathnamehash = \file_storage::get_pathname_hash($file->get_contextid(), $file->get_component(), $file->get_filearea(), $updatedfilerecord->itemid, $file->get_filepath(), $file->get_filename()); $updatedfilerecord->pathnamehash = $pathnamehash; $DB->update_record('files', $updatedfilerecord); // The pathnamehash in the h5p table must match the file pathnamehash. $h5pfs->updateContentFields($h5pcontentid, ['pathnamehash' => $pathnamehash]); } } else { // Update content. $this->content->update_content(); $cfdata = fullclone($data); $cfdata->id = $this->content->get_id(); $handler = \core_contentbank\customfield\content_handler::create(); $handler->instance_form_save($cfdata, true); } return $contentid ?? $newcontent->get_id(); } } PK*\peX,contenttype/h5p/classes/privacy/provider.phpnu[. /** * Privacy provider implementation for core_contentbank. * * @package contenttype_h5p * @copyright 2020 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace contenttype_h5p\privacy; /** * Privacy provider implementation for contenttype_h5p. * * @copyright 2020 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } PK*\%/'contenttype/h5p/classes/contenttype.phpnu[. /** * H5P content type manager class * * @package contenttype_h5p * @copyright 2020 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace contenttype_h5p; use core\event\contentbank_content_viewed; use stdClass; use core_h5p\editor_ajax; use core_h5p\file_storage; use core_h5p\local\library\autoloader; use Moodle\H5PCore; /** * H5P content bank manager class * * @package contenttype_h5p * @copyright 2020 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class contenttype extends \core_contentbank\contenttype { /** * Delete this content from the content_bank and remove all the H5P related information. * * @param content $content The content to delete. * @return boolean true if the content has been deleted; false otherwise. */ public function delete_content(\core_contentbank\content $content): bool { // Delete the H5P content. $factory = new \core_h5p\factory(); if (!empty($content->get_file_url())) { \core_h5p\api::delete_content_from_pluginfile_url($content->get_file_url(), $factory); } // Delete the content from the content_bank. return parent::delete_content($content); } /** * Returns the HTML content to add to view.php visualizer. * * @param content $content The content to be displayed. * @return string HTML code to include in view.php. */ public function get_view_content(\core_contentbank\content $content): string { // Trigger an event for viewing this content. $event = contentbank_content_viewed::create_from_record($content->get_content()); $event->trigger(); $fileurl = $content->get_file_url(); $html = \core_h5p\player::display($fileurl, new \stdClass(), true); return $html; } /** * Returns the HTML code to render the icon for H5P content types. * * @param content $content The content to be displayed. * @return string HTML code to render the icon */ public function get_icon(\core_contentbank\content $content): string { global $OUTPUT, $DB; $iconurl = $OUTPUT->image_url('f/h5p')->out(false); $file = $content->get_file(); if (!empty($file)) { $h5p = \core_h5p\api::get_content_from_pathnamehash($file->get_pathnamehash()); if (!empty($h5p)) { \core_h5p\local\library\autoloader::register(); if ($h5plib = $DB->get_record('h5p_libraries', ['id' => $h5p->mainlibraryid])) { $h5pfilestorage = new \core_h5p\file_storage(); $h5picon = $h5pfilestorage->get_icon_url( $h5plib->id, $h5plib->machinename, $h5plib->majorversion, $h5plib->minorversion); if (!empty($h5picon)) { $iconurl = $h5picon; } } } } return $iconurl; } /** * Return an array of implemented features by this plugin. * * @return array */ protected function get_implemented_features(): array { return [self::CAN_UPLOAD, self::CAN_EDIT, self::CAN_DOWNLOAD, self::CAN_COPY]; } /** * Return an array of extensions this contenttype could manage. * * @return array */ public function get_manageable_extensions(): array { return ['.h5p']; } /** * Returns user has access capability for the content itself. * * @return bool True if content could be accessed. False otherwise. */ protected function is_access_allowed(): bool { return true; } /** * Returns the list of different H5P content types the user can create. * * @return array An object for each H5P content type: * - string typename: descriptive name of the H5P content type. * - string typeeditorparams: params required by the H5P editor. * - url typeicon: H5P content type icon. */ public function get_contenttype_types(): array { // Get the H5P content types available. autoloader::register(); $editorajax = new editor_ajax(); $h5pcontenttypes = $editorajax->getLatestLibraryVersions(); $types = []; $h5pfilestorage = new file_storage(); foreach ($h5pcontenttypes as $h5pcontenttype) { if ($h5pcontenttype->enabled) { // Only enabled content-types will be displayed. $library = [ 'name' => $h5pcontenttype->machine_name, 'majorVersion' => $h5pcontenttype->major_version, 'minorVersion' => $h5pcontenttype->minor_version, ]; $key = H5PCore::libraryToString($library); $type = new stdClass(); $type->key = $key; $type->typename = $h5pcontenttype->title; $type->typeeditorparams = 'library=' . $key; $type->typeicon = $h5pfilestorage->get_icon_url( $h5pcontenttype->id, $h5pcontenttype->machine_name, $h5pcontenttype->major_version, $h5pcontenttype->minor_version); $types[] = $type; } } return $types; } } PK*\[Ntt#contenttype/h5p/classes/content.phpnu[. /** * H5P Content manager class * * @package contenttype_h5p * @copyright 2020 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace contenttype_h5p; use core\notification; use core_h5p\factory; /** * H5P Content manager class * * @package contenttype_h5p * @copyright 2020 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class content extends \core_contentbank\content { /** * Returns user has access permission for the content itself. * If the H5P content-type library is disabled, the user won't have access to it. * * @return bool True if content could be accessed. False otherwise. */ public function is_view_allowed(): bool { // Force H5P content to be deployed. $fileurl = $this->get_file_url(); if (empty($fileurl)) { // This should never happen because H5P contents should have always a file. However, this extra-checked has been added // to avoid the contentbank stop working if, for any unkonwn/weird reason, the file doesn't exist. return false; } // Skip capability check when creating the H5P content (because it has been created by trusted users). $h5pplayer = new \core_h5p\player($fileurl, new \stdClass(), true, '', true); // Flush error messages. $h5pplayer->get_messages(); // Check if the H5P entry has been created and if the main library is enabled. $file = $this->get_file(); if (!empty($file)) { $h5p = \core_h5p\api::get_content_from_pathnamehash($file->get_pathnamehash()); if (empty($h5p)) { // If there is no H5P entry for this content, it won't be displayed unless the user has the manageanycontent // capability. Reasons for contents without a proper H5P entry in DB: // - Invalid H5P package (it won't be never deployed). // - Disabled content-type library (it can't be deployed so there is no way to know the mainlibraryid). $context = \context::instance_by_id($this->content->contextid); if (!has_capability('moodle/contentbank:manageanycontent', $context)) { return false; } } else if (!\core_h5p\api::is_library_enabled((object) ['id' => $h5p->mainlibraryid])) { // If the main library is disabled, it won't be displayed. return false; } } return parent::is_view_allowed(); } /** * Import a file as a valid content. * Before importing the file, this method will check if the file is a valid H5P package. If it's not valid, it will thrown * an exception. * * @throws \moodle_exception If file operations fail * @param \stored_file $file File to store in the content file area. * @return \stored_file|null the stored content file or null if the file is discarted. */ public function import_file(\stored_file $file): ?\stored_file { // The H5P content can be only deployed if the author of the .h5p file can update libraries or if all the // content-type libraries exist, to avoid users without the h5p:updatelibraries capability upload malicious content. $onlyupdatelibs = !\core_h5p\helper::can_update_library($file); if (!\core_h5p\api::is_valid_package($file, $onlyupdatelibs)) { $factory = new factory(); $errors = $factory->get_framework()->getMessages('error'); foreach ($errors as $error) { notification::error($error->message); } if (empty($errors) || count($errors) > 1) { throw new \moodle_exception('notvalidpackage', 'h5p'); } throw new \moodle_exception($errors[0]->code, 'h5p'); } return parent::import_file($file); } } PK*\-w*contenttype/h5p/tests/content_h5p_test.phpnu[. namespace contenttype_h5p; /** * Test for H5P content bank plugin. * * @package contenttype_h5p * @category test * @copyright 2020 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \contenttype_h5p\content */ final class content_h5p_test extends \advanced_testcase { /** * Tests for uploaded file. * * @covers ::get_file */ public function test_upload_file(): void { $this->resetAfterTest(); // Create content. $record = new \stdClass(); $record->name = 'Test content'; $record->configdata = ''; $contenttype = new \contenttype_h5p\contenttype(\context_system::instance()); $content = $contenttype->create_content($record); // Create a dummy file. $filename = 'content.h5p'; $dummy = [ 'contextid' => \context_system::instance()->id, 'component' => 'contentbank', 'filearea' => 'public', 'itemid' => $content->get_id(), 'filepath' => '/', 'filename' => $filename ]; $fs = get_file_storage(); $fs->create_file_from_string($dummy, 'dummy content'); $file = $content->get_file(); $this->assertInstanceOf(\stored_file::class, $file); $this->assertEquals($filename, $file->get_filename()); } /** * Tests for is view allowed content. * * @covers ::is_view_allowed * @dataProvider is_view_allowed_provider * * @param string $role User role to use for create and view contents. * @param array $disabledlibraries Library names to disable. * @param array $expected Array with the expected values for the contents in the following order: * ['H5P.Blanks deployed', 'H5P.Accordion deployed', 'H5P.Accordion undeployed', 'Invalid content']. */ public function test_is_view_allowed(string $role, array $disabledlibraries, array $expected): void { global $CFG, $USER, $DB; $this->resetAfterTest(); // Create a course. $course = $this->getDataGenerator()->create_course(); $coursecontext = \context_course::instance($course->id); // Set user. if ($role == 'admin') { $this->setAdminUser(); } else { // Enrol user to the course. $user = $this->getDataGenerator()->create_and_enrol($course, $role); $this->setUser($user); } // Add contents to the content bank. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); $filepath = $CFG->dirroot . '/h5p/tests/fixtures/filltheblanks.h5p'; $contents = $generator->generate_contentbank_data('contenttype_h5p', 1, $USER->id, $coursecontext, true, $filepath); $filltheblanks = array_shift($contents); $filepath = $CFG->dirroot . '/h5p/tests/fixtures/ipsums.h5p'; $contents = $generator->generate_contentbank_data('contenttype_h5p', 2, $USER->id, $coursecontext, true, $filepath); $accordion1 = array_shift($contents); $accordion2 = array_shift($contents); $filepath = $CFG->dirroot . '/h5p/tests/fixtures/invalid.zip'; $contents = $generator->generate_contentbank_data('contenttype_h5p', 1, $USER->id, $coursecontext, true, $filepath); $invalid = array_shift($contents); // Load some of these H5P files though the player to create the H5P DB entries. $h5pplayer = new \core_h5p\player($filltheblanks->get_file_url(), new \stdClass(), true); $h5pplayer = new \core_h5p\player($accordion1->get_file_url(), new \stdClass(), true); // Check the expected H5P content has been created. $this->assertEquals(2, $DB->count_records('h5p')); $this->assertEquals(4, $DB->count_records('contentbank_content')); // Disable libraries. foreach ($disabledlibraries as $libraryname) { $libraryid = $DB->get_field('h5p_libraries', 'id', ['machinename' => $libraryname]); \core_h5p\api::set_library_enabled((int) $libraryid, false); } $this->assertEquals($expected[0], $filltheblanks->is_view_allowed()); $this->assertEquals($expected[1], $accordion1->is_view_allowed()); $this->assertEquals($expected[2], $accordion2->is_view_allowed()); $this->assertEquals($expected[3], $invalid->is_view_allowed()); // Check that after enabling libraries again, all the content return true (but the invalid package). foreach ($disabledlibraries as $libraryname) { $libraryid = $DB->get_field('h5p_libraries', 'id', ['machinename' => $libraryname]); \core_h5p\api::set_library_enabled((int) $libraryid, true); } $this->assertEquals(true, $filltheblanks->is_view_allowed()); $this->assertEquals(true, $accordion1->is_view_allowed()); $this->assertEquals(true, $accordion2->is_view_allowed()); // It will be deployed, so now it will always return true. $this->assertEquals($expected[3], $invalid->is_view_allowed()); } /** * Data provider for test_is_view_allowed. * * @return array */ public static function is_view_allowed_provider(): array { return [ 'Editing teacher with all libraries enabled' => [ 'role' => 'editingteacher', 'disabledlibraries' => [], 'expected' => [true, true, true, false], ], 'Manager with all libraries enabled' => [ 'role' => 'manager', 'disabledlibraries' => [], 'expected' => [true, true, true, true], ], 'Admin with all libraries enabled' => [ 'role' => 'admin', 'disabledlibraries' => [], 'expected' => [true, true, true, true], ], 'Editing teacher with H5P.Accordion disabled' => [ 'role' => 'editingteacher', 'disabledlibraries' => ['H5P.Accordion'], 'expected' => [true, false, false, false], ], 'Manager with H5P.Accordion disabled' => [ 'role' => 'manager', 'disabledlibraries' => ['H5P.Accordion'], 'expected' => [true, false, true, true], ], 'Admin with H5P.Accordion disabled' => [ 'role' => 'admin', 'disabledlibraries' => ['H5P.Accordion'], 'expected' => [true, false, true, true], ], 'Editing teacher with all libraries disabled' => [ 'role' => 'editingteacher', 'disabledlibraries' => ['H5P.Accordion', 'H5P.Blanks'], 'expected' => [false, false, false, false], ], 'Manager with all libraries disabled' => [ 'role' => 'manager', 'disabledlibraries' => ['H5P.Accordion', 'H5P.Blanks'], 'expected' => [false, false, true, true], ], 'Admin with all libraries disabled' => [ 'role' => 'admin', 'disabledlibraries' => ['H5P.Accordion', 'H5P.Blanks'], 'expected' => [false, false, true, true], ], ]; } } PK*\t?.contenttype/h5p/tests/contenttype_h5p_test.phpnu[. namespace contenttype_h5p; /** * Test for H5P content bank plugin. * * @package contenttype_h5p * @category test * @copyright 2020 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \contenttype_h5p\contenttype */ final class contenttype_h5p_test extends \advanced_testcase { /** * Test the behaviour of delete_content(). */ public function test_delete_content(): void { global $CFG, $USER, $DB; $this->resetAfterTest(); $systemcontext = \context_system::instance(); // Create users. $roleid = $DB->get_field('role', 'id', array('shortname' => 'manager')); $manager = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->role_assign($roleid, $manager->id); $this->setUser($manager); // Add an H5P file to the content bank. $filepath = $CFG->dirroot . '/h5p/tests/fixtures/filltheblanks.h5p'; $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); $contents = $generator->generate_contentbank_data('contenttype_h5p', 2, $USER->id, $systemcontext, true, $filepath); $content1 = array_shift($contents); $content2 = array_shift($contents); // Load this H5P file though the player to create the H5P DB entries. $h5pplayer = new \core_h5p\player($content1->get_file_url(), new \stdClass(), true); $h5pplayer->add_assets_to_page(); $h5pplayer->output(); $h5pplayer = new \core_h5p\player($content2->get_file_url(), new \stdClass(), true); $h5pplayer->add_assets_to_page(); $h5pplayer->output(); // Check the H5P content has been created. $this->assertEquals(2, $DB->count_records('h5p')); $this->assertEquals(2, $DB->count_records('contentbank_content')); // Check the H5P content is removed after calling this method. $contenttype = new \contenttype_h5p\contenttype($systemcontext); $contenttype->delete_content($content1); $this->assertEquals(1, $DB->count_records('h5p')); $this->assertEquals(1, $DB->count_records('contentbank_content')); } /** * Tests can_upload behavior. * * @covers ::can_upload */ public function test_can_upload(): void { $this->resetAfterTest(); $systemcontext = \context_system::instance(); $systemtype = new \contenttype_h5p\contenttype($systemcontext); // Admins can upload. $this->setAdminUser(); $this->assertTrue($systemtype->can_upload()); // Teacher can upload in the course but not at system level. $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); $coursecontext = \context_course::instance($course->id); $coursetype = new \contenttype_h5p\contenttype($coursecontext); $this->setUser($teacher); $this->assertTrue($coursetype->can_upload()); $this->assertFalse($systemtype->can_upload()); // Users can't upload. $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $this->assertFalse($coursetype->can_upload()); $this->assertFalse($systemtype->can_upload()); } /** * Tests get_icon result. * * @covers ::get_icon */ public function test_get_icon(): void { global $CFG; $this->resetAfterTest(); $systemcontext = \context_system::instance(); $this->setAdminUser(); $contenttype = new contenttype($systemcontext); // Add an H5P fill the blanks file to the content bank. $filepath = $CFG->dirroot . '/h5p/tests/fixtures/filltheblanks.h5p'; $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); $contents = $generator->generate_contentbank_data('contenttype_h5p', 1, 0, $systemcontext, true, $filepath); $filltheblanks = array_shift($contents); // Add an H5P find the words file to the content bank. $filepath = $CFG->dirroot . '/h5p/tests/fixtures/find-the-words.h5p'; $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); $contents = $generator->generate_contentbank_data('contenttype_h5p', 1, 0, $systemcontext, true, $filepath); $findethewords = array_shift($contents); // Check before deploying the icon for both contents is the same: default one. // Because we don't know specific H5P content type yet. $defaulticon = $contenttype->get_icon($filltheblanks); $this->assertEquals($defaulticon, $contenttype->get_icon($findethewords)); $this->assertStringContainsString('h5p', $defaulticon); // Deploy one of the contents though the player to create the H5P DB entries and know specific content type. $h5pplayer = new \core_h5p\player($findethewords->get_file_url(), new \stdClass(), true); $h5pplayer->add_assets_to_page(); $h5pplayer->output(); // Once the H5P has been deployed, we know the specific H5P content type, so the icon returned is not default one. $findicon = $contenttype->get_icon($findethewords); $this->assertNotEquals($defaulticon, $findicon); $this->assertStringContainsStringIgnoringCase('find', $findicon); } /** * Tests get_download_url result. * * @covers ::get_download_url */ public function test_get_download_url(): void { global $CFG; $this->resetAfterTest(); $systemcontext = \context_system::instance(); $this->setAdminUser(); $contenttype = new contenttype($systemcontext); // Add an H5P fill the blanks file to the content bank. $filename = 'filltheblanks.h5p'; $filepath = $CFG->dirroot . '/h5p/tests/fixtures/' . $filename; $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); $contents = $generator->generate_contentbank_data('contenttype_h5p', 1, 0, $systemcontext, true, $filepath); $filltheblanks = array_shift($contents); // Check before deploying the URL is returned OK. $url1 = $contenttype->get_download_url($filltheblanks); $this->assertNotEmpty($url1); $this->assertStringContainsString($filename, $url1); // Deploy the contents though the player to create the H5P DB entries and know specific content type. $h5pplayer = new \core_h5p\player($filltheblanks->get_file_url(), new \stdClass(), true); $h5pplayer->add_assets_to_page(); $h5pplayer->output(); // Once the H5P has been deployed, the URL is still the same. $url2 = $contenttype->get_download_url($filltheblanks); $this->assertNotEmpty($url2); $this->assertEquals($url1, $url2); } } PK*\BB;wff:contenttype/h5p/tests/behat/teacher_upload_content.featurenu[@core @core_contentbank @core_h5p @contenttype_h5p @_file_upload @_switch_iframe @javascript Feature: H5P file upload to content bank for non admins In order import new H5P content to content bank As an admin I need to be able to upload a new .h5p file to content bank Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | teacher2 | Teacher | 2 | teacher2@example.com | And the following "categories" exist: | name | category | idnumber | | Cat 1 | 0 | CAT1 | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | CAT1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher2 | C1 | editingteacher | And the following "user private file" exists: | user | teacher1 | | filepath | h5p/tests/fixtures/filltheblanks.h5p | And I log in as "teacher1" Scenario: Teachers can not access system level content bank Given I turn editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present When I expand "Site pages" node Then I should not see "Content bank" Scenario: Teachers can access course level content bank Given I am on "Course 1" course homepage with editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present When I expand "Site pages" node Then I should see "Content bank" Scenario: Teachers can upload .h5p extension files to course content bank Given I log out And I log in as "admin" And I navigate to "H5P > Manage H5P content types" in site administration And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element" And I log out And I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present When I expand "Site pages" node And I click on "Content bank" "link" Then I should not see "filltheblanks.h5p" And I click on "Upload" "link" And I click on "Choose a file..." "button" And I click on "Private files" "link" in the ".fp-repo-area" "css_element" And I click on "filltheblanks.h5p" "link" And I click on "Select this file" "button" And I click on "Save changes" "button" And I wait until the page is ready And I should see "filltheblanks.h5p" Scenario: Other teachers can see uploaded H5P contents Given I log out And I log in as "admin" And I navigate to "H5P > Manage H5P content types" in site administration And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element" And I log out And I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present When I expand "Site pages" node And I click on "Content bank" "link" And I click on "Upload" "link" And I click on "Choose a file..." "button" And I click on "Private files" "link" in the ".fp-repo-area" "css_element" And I click on "filltheblanks.h5p" "link" And I click on "Select this file" "button" And I click on "Save changes" "button" And I wait until the page is ready And I should see "filltheblanks.h5p" And I log out When I log in as "teacher2" And I am on "Course 1" course homepage with editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present And I expand "Site pages" node And I click on "Content bank" "link" Then I should see "filltheblanks.h5p" Scenario: Teachers can not upload and deploy content types when libraries are not installed Given I log out And I log in as "admin" And I navigate to "H5P > Manage H5P content types" in site administration And I should not see "Fill in the Blanks" And I log out And I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present And I expand "Site pages" node And I click on "Content bank" "link" When I click on "Upload" "link" And I click on "Choose a file..." "button" And I click on "Private files" "link" in the ".fp-repo-area" "css_element" And I click on "filltheblanks.h5p" "link" And I click on "Select this file" "button" And I click on "Save changes" "button" Then I should see "Missing required library" And I should not see "filltheblanks.h5p" And I log out And I log in as "admin" And I am on "Course 1" course homepage And I expand "Site pages" node And I click on "Content bank" "link" And I should not see "filltheblanks.h5p" Scenario: Teachers can not see existing contents when libraries are not installed Given the following "user private file" exists: | user | admin | | filepath | h5p/tests/fixtures/filltheblanks.h5p | And I log in as "admin" And I navigate to "H5P > Manage H5P content types" in site administration And I should not see "Fill in the Blanks" When I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element" And I wait until the page is ready And I should see "Fill in the Blanks" And I log out And I log in as "teacher1" Given I am on "Course 1" course homepage with editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present When I expand "Site pages" node And I click on "Content bank" "link" And I click on "Upload" "link" And I click on "Choose a file..." "button" And I click on "Private files" "link" in the ".fp-repo-area" "css_element" And I click on "filltheblanks.h5p" "link" And I click on "Select this file" "button" And I click on "Save changes" "button" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe Then I should see "Of which countries" Then I should not see "missing-main-library" And I switch to the main frame Given I log out And I log in as "admin" And I navigate to "H5P > Manage H5P content types" in site administration When I click on "Delete version" "link" in the "Fill in the Blanks" "table_row" And I press "Continue" Then I should not see "Fill in the Blanks" And I log out And I log in as "teacher1" Given I am on "Course 1" course homepage When I expand "Site pages" node And I click on "Content bank" "link" Then I should not see "filltheblanks.h5p" And I log out And I log in as "admin" And I am on "Course 1" course homepage And I expand "Site pages" node And I click on "Content bank" "link" And I should see "filltheblanks.h5p" And I click on "filltheblanks.h5p" "link" And I switch to "h5p-player" class iframe And I should see "missing-main-library" PK*\i_enn;contenttype/h5p/tests/behat/teacher_replace_content.featurenu[@core @core_contentbank @core_h5p @contenttype_h5p @_file_upload @_switch_iframe @javascript Feature: Replace H5P file from an existing content requires special capabilities In order replace an H5P content from the content bank As a teacher I need to be able to replace the content only if certain capabilities are allowed Background: Given I log in as "admin" And I navigate to "H5P > Manage H5P content types" in site administration And I upload "h5p/tests/fixtures/ipsums.h5p" file to "H5P content type" filemanager And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element" And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element" And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "categories" exist: | name | category | idnumber | | Cat 1 | 0 | CAT1 | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | CAT1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | Course | C1 | contenttype_h5p | admin | admincontent | /h5p/tests/fixtures/ipsums.h5p | | Course | C1 | contenttype_h5p | teacher1 | teachercontent | /h5p/tests/fixtures/filltheblanks.h5p | And I log out And I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present And I expand "Site pages" node And I click on "Content bank" "link" # Force the content deploy And I click on "admincontent" "link" And I am on "Course 1" course homepage And I expand "Site pages" node And I click on "Content bank" "link" Scenario: Teacher can replace its own H5P files Given I click on "teachercontent" "link" When I click on "More" "button" And I click on "Replace with file" "link" And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Upload content" filemanager And I click on "Save changes" "button" Then I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I should see "Lorum ipsum" And I switch to the main frame Scenario: Teacher cannot replace another user's H5P files When I click on "admincontent" "link" And I click on "More" "button" Then I should not see "Replace with file" Scenario: Teacher cannot replace a content without having upload capability Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/contentbank:upload | Prevent | editingteacher | Course | C1 | When I click on "teachercontent" "link" And I click on "More" "button" Then I should not see "Replace with file" Scenario: Teacher cannot replace a content without having the H5P upload capability Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | contenttype/h5p:upload | Prevent | editingteacher | Course | C1 | When I click on "teachercontent" "link" And I click on "More" "button" Then I should not see "Replace with file" Scenario: Teacher cannot replace a content without having the manage own content capability Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/contentbank:manageowncontent | Prevent | editingteacher | Course | C1 | When I click on "teachercontent" "link" And I click on "More" "button" Then I should not see "Replace with file" Scenario: Replacing with invalid packages throws error Given I log in as "admin" And I navigate to "H5P > Manage H5P content types" in site administration And I upload "h5p/tests/fixtures/no-json-file.h5p" file to "H5P content type" filemanager And I upload "h5p/tests/fixtures/unzippable.h5p" file to "H5P content type" filemanager And I log out And I log in as "teacher1" And I am on "Course 1" course homepage And I expand "Site pages" node And I click on "Content bank" "link" And I click on "teachercontent" "link" When I click on "More" "button" And I click on "Replace with file" "link" And I upload "h5p/tests/fixtures/no-json-file.h5p" file to "Upload content" filemanager And I click on "Save changes" "button" And I wait until the page is ready Then I should see "A valid main h5p.json file is missing" And I should see "Only files with the following extensions are allowed" And I should not see "Sorry, this file is not valid" And I click on "More" "button" And I click on "Replace with file" "link" And I upload "h5p/tests/fixtures/unzippable.h5p" file to "Upload content" filemanager And I click on "Save changes" "button" And I wait until the page is ready And I should see "It is not possible to unzip it" And I should not see "Sorry, this file is not valid" PK*\ܪi8contenttype/h5p/tests/behat/admin_upload_content.featurenu[@core @core_contentbank @core_h5p @contenttype_h5p @_switch_iframe Feature: H5P file upload to content bank for admins In order import new H5P content to content bank As an admin I need to be able to upload a new .h5p file to content bank Background: Given the following "user private file" exists: | user | admin | | filepath | h5p/tests/fixtures/filltheblanks.h5p | And I log in as "admin" And I turn editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present And I expand "Site pages" node And I click on "Content bank" "link" @javascript Scenario: Admins can upload .h5p extension files to content bank Given I should not see "filltheblanks.h5p" When I click on "Upload" "link" And I click on "Choose a file..." "button" And I click on "Private files" "link" in the ".fp-repo-area" "css_element" And I click on "filltheblanks.h5p" "link" And I click on "Select this file" "button" And I click on "Save changes" "button" And I wait until the page is ready Then I should see "filltheblanks.h5p" @javascript Scenario: Admins can see uploaded H5P contents Given I should not see "filltheblanks.h5p" When I click on "Upload" "link" And I click on "Choose a file..." "button" And I click on "Private files" "link" in the ".fp-repo-area" "css_element" And I click on "filltheblanks.h5p" "link" And I click on "Select this file" "button" And I click on "Save changes" "button" And I wait until the page is ready And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe Then I should see "Of which countries" @javascript Scenario: Users can't see content managed by disabled plugins Given I click on "Upload" "link" And I click on "Choose a file..." "button" And I click on "Private files" "link" in the ".fp-repo-area" "css_element" And I click on "filltheblanks.h5p" "link" And I click on "Select this file" "button" And I click on "Save changes" "button" And I wait until the page is ready And I should see "filltheblanks.h5p" And I navigate to "Plugins > Content bank > Manage content types" in site administration And I click on "Disable" "icon" in the "H5P" "table_row" And I wait until the page is ready When I navigate to "Plugins > Content bank" in site administration Then I should not see "filltheblanks.h5p" @javascript Scenario: Contents in a context are not available from other contexts Given the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | When I click on "Upload" "link" And I click on "Choose a file..." "button" And I click on "Private files" "link" in the ".fp-repo-area" "css_element" And I click on "filltheblanks.h5p" "link" And I click on "Select this file" "button" And I click on "Save changes" "button" And I wait until the page is ready Then I should see "filltheblanks.h5p" And I am on "Course 1" course homepage And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present And I expand "Site pages" node And I click on "Content bank" "link" And I should not see "filltheblanks.h5p" @javascript Scenario: Admins can upload and deployed content types when libraries are not installed Given I navigate to "H5P > Manage H5P content types" in site administration And I should not see "Fill in the Blanks" And I follow "Dashboard" And I expand "Site pages" node And I click on "Content bank" "link" And I should not see "filltheblanks.h5p" When I click on "Upload" "link" And I click on "Choose a file..." "button" And I click on "Private files" "link" in the ".fp-repo-area" "css_element" And I click on "filltheblanks.h5p" "link" And I click on "Select this file" "button" And I click on "Save changes" "button" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe Then I should see "Of which countries" And I switch to the main frame And I navigate to "H5P > Manage H5P content types" in site administration And I should see "Fill in the Blanks" @javascript Scenario: Uploading invalid packages throws error Given the following "user private files" exist: | user | filepath | | admin | h5p/tests/fixtures/no-json-file.h5p | | admin | h5p/tests/fixtures/unzippable.h5p | And I follow "Dashboard" And I expand "Site pages" node And I click on "Content bank" "link" When I click on "Upload" "link" And I click on "Choose a file..." "button" And I click on "Private files" "link" in the ".fp-repo-area" "css_element" And I click on "no-json-file.h5p" "link" And I click on "Select this file" "button" And I click on "Save changes" "button" And I wait until the page is ready Then I should see "A valid main h5p.json file is missing" And I should see "Only files with the following extensions are allowed" And I should not see "Sorry, this file is not valid" And I click on "Upload" "link" And I click on "Choose a file..." "button" And I click on "Private files" "link" in the ".fp-repo-area" "css_element" And I click on "unzippable.h5p" "link" And I click on "Select this file" "button" And I click on "Save changes" "button" And I wait until the page is ready And I should see "It is not possible to unzip it" And I should not see "Sorry, this file is not valid" @_file_upload @javascript Scenario: Uploading invalid file types is not allowed Given I am on the "Content bank" page logged in as "admin" When I click on "Upload" "link" And I click on "Choose a file..." "button" And I click on "Upload a file" "link" in the ".fp-repo-area" "css_element" And I set the field "Attachment" to "#dirroot#/lib/tests/fixtures/1.jpg" And I press "Upload this file" # Confirm that jpg files cannot be added to content bank. Then "Image (JPEG) filetype cannot be accepted." "text" should exist And I click on "OK" "button" in the "Error" "dialogue" And I click on "Close" "button" in the "File picker" "dialogue" And I click on "Cancel" "button" in the "Upload" "dialogue" # Confirm that jpg file was not added to the content bank. And "No content available" "text" should exist And I should not see "1.jpg" Scenario: File upload for content bank can be disabled by admin Given I log in as "admin" And I navigate to "Plugins > Content bank > Manage content types" in site administration When I follow "Disable" And I am on the "Content bank" page # Confirm that "Upload" does not exist after admin disabled from Plugins > Content bank > Manage content types. Then "Upload" "link" should not exist PK*\w 2contenttype/h5p/tests/behat/manage_content.featurenu[@core @core_contentbank @core_h5p @contenttype_h5p @_file_upload @javascript Feature: Manage H5P content from the content bank In order to manage H5P content in the content bank As an admin I need to be able to edit any H5P content in the content bank Background: Given I log in as "admin" And I navigate to "H5P > Manage H5P content types" in site administration And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element" And I upload "h5p/tests/fixtures/ipsums.h5p" file to "H5P content type" filemanager And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element" And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | Course | C1 | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p | | Course | C1 | contenttype_h5p | teacher1 | ipsums.h5p | /h5p/tests/fixtures/ipsums.h5p | And I am on "Course 1" course homepage with editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present And I log out Scenario: Teachers can rename their own content in the content bank Given I log in as "teacher1" And I am on "Course 1" course homepage And I expand "Site pages" node And I click on "Content bank" "link" And I follow "ipsums.h5p" When I click on "More" "button" And I should see "Rename" And I click on "Rename" "link" And I set the field "Content name" to "New name" And I click on "Rename" "button" And I wait until the page is ready Then I should not see "ipsums.h5p" And I should see "New name" Scenario: Teachers can't rename content created by other users in the content bank Given I log in as "teacher1" And I am on "Course 1" course homepage And I expand "Site pages" node And I click on "Content bank" "link" When I follow "filltheblanks.h5p" And I click on "More" "button" Then I should not see "Rename" PK*\wu8contenttype/h5p/tests/behat/disable_contenttypes.featurenu[@core @core_contentbank @core_h5p @contenttype_h5p @_file_upload @javascript Feature: Disable H5P content-types from the content bank In order to disable H5P content-types As an admin I need to be able to check they are not displayed in the content bank Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "contentbank contents" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | Course | C1 | contenttype_h5p | admin | filltheblanks | /h5p/tests/fixtures/filltheblanks.h5p | | Course | C1 | contenttype_h5p | admin | accordion | /h5p/tests/fixtures/ipsums.h5p | | Course | C1 | contenttype_h5p | admin | invalidh5p | /h5p/tests/fixtures/h5ptest.zip | And I log in as "admin" And I am on "Course 1" course homepage with editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present And I log out Scenario: Teachers cannot view disabled or invalid content-types Given I log in as "teacher1" And I am on "Course 1" course homepage And I expand "Site pages" node And I click on "Content bank" "link" And I should see "accordion" And I should see "filltheblanks" And I should not see "invalidh5p" And I log out And I log in as "admin" And I navigate to "H5P > Manage H5P content types" in site administration And I click on "Disable" "link" in the "Accordion" "table_row" And I log out When I log in as "teacher1" And I am on "Course 1" course homepage And I expand "Site pages" node And I click on "Content bank" "link" Then I should not see "accordion" And I should see "filltheblanks" And I should not see "invalidh5p" Scenario: Admins cannot view disabled content-types Given I log in as "admin" And I am on "Course 1" course homepage And I expand "Site pages" node And I click on "Content bank" "link" And I should see "accordion" And I should see "filltheblanks" And I should see "invalidh5p" And I navigate to "H5P > Manage H5P content types" in site administration And I click on "Disable" "link" in the "Accordion" "table_row" When I am on "Course 1" course homepage And I expand "Site pages" node And I click on "Content bank" "link" Then I should not see "accordion" And I should see "filltheblanks" And I should see "invalidh5p" Scenario: Teachers cannot create disabled content-types Given I log in as "teacher1" And I am on "Course 1" course homepage And I expand "Site pages" node And I click on "Content bank" "link" And I click on "[data-action=Add-content]" "css_element" And I should see "Accordion" And I should see "Fill in the Blanks" And I log out And I log in as "admin" And I navigate to "H5P > Manage H5P content types" in site administration And I click on "Disable" "link" in the "Accordion" "table_row" And I log out When I log in as "teacher1" And I am on "Course 1" course homepage And I expand "Site pages" node And I click on "Content bank" "link" And I click on "[data-action=Add-content]" "css_element" Then I should not see "Accordion" And I should see "Fill in the Blanks" Scenario: Admins cannot create disabled content-types Given I log in as "admin" And I am on "Course 1" course homepage And I expand "Site pages" node And I click on "Content bank" "link" And I click on "[data-action=Add-content]" "css_element" And I should see "Accordion" And I should see "Fill in the Blanks" And I navigate to "H5P > Manage H5P content types" in site administration And I click on "Disable" "link" in the "Accordion" "table_row" When I am on "Course 1" course homepage And I expand "Site pages" node And I click on "Content bank" "link" And I click on "[data-action=Add-content]" "css_element" Then I should not see "Accordion" And I should see "Fill in the Blanks" PK*\s}6*9contenttype/h5p/tests/behat/admin_replace_content.featurenu[@core @core_contentbank @core_h5p @contenttype_h5p @_file_upload @_switch_iframe @javascript Feature: Replace H5P file from an existing content In order to replace an H5P content from the content bank As an admin I need to be able to replace the content with a new .h5p file Background: Given the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | System | | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p | And I log in as "admin" And I turn editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present And I expand "Site pages" node And I click on "Content bank" "link" Scenario: Admins can replace the original .h5p file with a new one Given I click on "filltheblanks.h5p" "link" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I should see "Of which countries" And I switch to the main frame When I click on "More" "button" And I click on "Replace with file" "link" And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Upload content" filemanager And I click on "Save changes" "button" Then I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I should see "Lorum ipsum" And I switch to the main frame PK*\!8||#tests/fixtures/testable_content.phpnu[. /** * Testable content plugin class. * * @package core_contentbank * @category test * @copyright 2020 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace contenttype_testable; use file_exception; use stored_file; /** * Testable content plugin class. * * @package core_contentbank * @copyright 2020 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class content extends \core_contentbank\content { /** * Import a file as a valid content. * * This method will thow an error if the filename is "error.*" * * @param stored_file $file File to store in the content file area. * @return stored_file|null the stored content file or null if the file is discarted. * @throws file_exception if the filename contains the word "error" */ public function import_file(stored_file $file): ?stored_file { $filename = $file->get_filename(); if (strrpos($filename, 'error') !== false) { throw new file_exception('yourerrorthanks', 'contenttype_test'); } return parent::import_file($file); } } PK*\&ah6 'tests/fixtures/testable_contenttype.phpnu[. namespace contenttype_testable; /** * Testable contenttype plugin class. * * @package core_contentbank * @copyright 2020 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class contenttype extends \core_contentbank\contenttype { /** Feature for testing */ const CAN_TEST = 'test'; /** @var array Additional features for testing */ public static $featurestotest; /** * Return an array of implemented features by this plugin. * * @return array */ protected function get_implemented_features(): array { $features = [self::CAN_TEST]; if (!empty(self::$featurestotest)) { $features = array_merge($features, self::$featurestotest); } return $features; } /** * Return an array of extensions this plugin could manage. * * @return array */ public function get_manageable_extensions(): array { return ['.txt', '.png', '.h5p']; } /** * Returns the list of different types of the given content type. * * @return array */ public function get_contenttype_types(): array { $type = new \stdClass(); $type->typename = 'testable'; return [$type]; } /** * Returns true, so the user has permission on the feature. * * @return bool True if content could be edited or created. False otherwise. */ final public function can_test2(): bool { if (!$this->is_feature_supported('test2')) { return false; } return true; } /** * This implements custom file serving. * * @param stdClass $course the course object * @param stdClass $cm the course module object * @param \context $context the context * @param string $filearea the name of the file area * @param array $args extra arguments (itemid, path) * @param bool $forcedownload whether or not force download * @param array $options additional options affecting the file serving * @return void */ public static function pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = []): void { return; } } PK*\z'tests/generator/lib.phpnu[. /** * Content bank files repository data generator * * @package repository_contentbank * @category test * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Content bank files repository data generator class * * @package repository_contentbank * @category test * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_contentbank_generator extends testing_repository_generator { } PK*\w}hh$tests/external/copy_content_test.phpnu[. namespace core_contentbank\external; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php'); require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php'); require_once($CFG->dirroot . '/webservice/tests/helpers.php'); use core_external\external_api; /** * Content bank's copy content external function tests. * * @package core_contentbank * @copyright 2023 Daniel Neis Araujo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \core_contentbank\external\copy_content */ final class copy_content_test extends \externallib_advanced_testcase { /** * Test the behaviour of copy_content() for users with permission. * * @covers ::execute */ public function test_copy_content_with_permission(): void { global $CFG, $DB; $this->resetAfterTest(); // Create users. $roleid = $DB->get_field('role', 'id', ['shortname' => 'editingteacher']); $teacher = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->role_assign($roleid, $teacher->id); $this->setUser($teacher); // Add some content to the content bank as teacher. $filename = 'filltheblanks.h5p'; $filepath = $CFG->dirroot . '/h5p/tests/fixtures/' . $filename; $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); $contents = $generator->generate_contentbank_data('contenttype_h5p', 1, $teacher->id, null, true, $filepath); $content = array_shift($contents); $oldname = $content->get_name(); $newname = 'New name'; // Call the WS and check the content is copied as expected. $result = copy_content::execute($content->get_id(), $newname); $result = external_api::clean_returnvalue(copy_content::execute_returns(), $result); $this->assertNotEmpty($result['id']); $record = $DB->get_record('contentbank_content', ['id' => $result['id']]); $this->assertEquals($newname, $record->name); $record = $DB->get_record('contentbank_content', ['id' => $content->get_id()]); $this->assertEquals($oldname, $record->name); // Call the WS using an unexisting contentid and check an error is thrown. $this->expectException(\invalid_response_exception::class); $result = copy_content::execute_returns($content->get_id() + 1, $oldname); $result = external_api::clean_returnvalue(copy_content::execute_returns(), $result); $this->assertNotEmpty($result['warnings']); } /** * Test the behaviour of copy_content() for users with and without permission. * * @covers ::execute */ public function test_copy_content(): void { global $CFG, $DB; $this->resetAfterTest(); // Create users. $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); $teacher2 = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); $manager = $this->getDataGenerator()->create_and_enrol($course, 'manager'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); // Add some content to the content bank as teacher. $coursecontext = \context_course::instance($course->id); $filename = 'filltheblanks.h5p'; $filepath = $CFG->dirroot . '/h5p/tests/fixtures/' . $filename; $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); $contents = $generator->generate_contentbank_data('contenttype_h5p', 1, $teacher->id, $coursecontext, true, $filepath); $content = array_shift($contents); $oldname = $content->get_name(); $newname = 'New name'; // Call the WS and check the teacher can copy his/her own content. $this->setUser($teacher); $result = copy_content::execute($content->get_id(), $newname); $result = external_api::clean_returnvalue(copy_content::execute_returns(), $result); $this->assertEmpty($result['warnings']); $record = $DB->get_record('contentbank_content', ['id' => $result['id']]); $this->assertEquals($newname, $record->name); // Call the WS and check the content has not been copied by the student. $this->setUser($student); $result = copy_content::execute($content->get_id(), $newname); $result = external_api::clean_returnvalue(copy_content::execute_returns(), $result); $this->assertNotEmpty($result['warnings']); $record = $DB->get_record('contentbank_content', ['id' => $content->get_id()]); $this->assertEquals($oldname, $record->name); $this->assertNotEquals($newname, $record->name); // Call the WS an check the content with empty name is not copied by the teacher. $this->setUser($teacher); $result = copy_content::execute($content->get_id(), ''); $result = external_api::clean_returnvalue(copy_content::execute_returns(), $result); $this->assertNotEmpty($result['warnings']); // Call the WS and check a teacher cannot copy content from another teacher by default. $this->setUser($teacher2); $result = copy_content::execute($content->get_id(), 'New name 2'); $result = external_api::clean_returnvalue(copy_content::execute_returns(), $result); $this->assertNotEmpty($result['warnings']); // Call the WS and check a manager can copy content from a teacher by default. $this->setUser($manager); $result = copy_content::execute($content->get_id(), $newname); $result = external_api::clean_returnvalue(copy_content::execute_returns(), $result); $this->assertEmpty($result['warnings']); $record = $DB->get_record('contentbank_content', ['id' => $result['id']]); $this->assertEquals($newname, $record->name); } } PK*\{vww&tests/external/delete_content_test.phpnu[. /** * External function test for delete_content. * * @package core_contentbank * @category external * @since Moodle 3.9 * @copyright 2020 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_contentbank\external; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php'); use dml_missing_record_exception; use core_external\external_api; use externallib_advanced_testcase; /** * External function test for delete_content. * * @package core_contentbank * @copyright 2020 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class delete_content_test extends externallib_advanced_testcase { /** * Test the behaviour of delete_content(). */ public function test_delete_content(): void { global $DB; $this->resetAfterTest(); $records = []; // Create users. $user = $this->getDataGenerator()->create_user(); $roleid = $DB->get_field('role', 'id', array('shortname' => 'manager')); $manager = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->role_assign($roleid, $manager->id); // Add some content to the content bank. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); $records[$manager->id] = $generator->generate_contentbank_data('contenttype_testable', 4, $manager->id, null, false); $records[$user->id] = $generator->generate_contentbank_data('contenttype_testable', 2, $user->id, null, false); // Check the content has been created as expected. $this->assertEquals(6, $DB->count_records('contentbank_content')); // Check the content is deleted as expected by the user when the content has been created by herself. $this->setUser($user); $userrecord = array_shift($records[$user->id]); $result = delete_content::execute([$userrecord->id]); $result = external_api::clean_returnvalue(delete_content::execute_returns(), $result); $this->assertTrue($result['result']); $this->assertCount(0, $result['warnings']); $this->assertEquals(5, $DB->count_records('contentbank_content')); // Check the content is not deleted if the user hasn't created it and has only permission to delete her own content. $userrecord = array_shift($records[$user->id]); $managerrecord1 = array_shift($records[$manager->id]); $result = delete_content::execute([$managerrecord1->id, $userrecord->id]); $result = external_api::clean_returnvalue(delete_content::execute_returns(), $result); $this->assertFalse($result['result']); $this->assertCount(1, $result['warnings']); $warning = array_shift($result['warnings']); $this->assertEquals('nopermissiontodelete', $warning['warningcode']); $this->assertEquals($managerrecord1->id, $warning['item']); $this->assertEquals(4, $DB->count_records('contentbank_content')); // Check the content is deleted as expected by the manager. $this->setUser($manager); $managerrecord2 = array_shift($records[$manager->id]); $result = delete_content::execute([$managerrecord1->id, $managerrecord2->id]); $result = external_api::clean_returnvalue(delete_content::execute_returns(), $result); $this->assertTrue($result['result']); $this->assertCount(0, $result['warnings']); $this->assertEquals(2, $DB->count_records('contentbank_content')); // Check an exception warning is returned if an unexisting contentid is deleted. // Check also the other content is deleted (so the process continues after the exception is thrown). $managerrecord3 = array_shift($records[$manager->id]); $result = delete_content::execute([$managerrecord1->id, $managerrecord3->id]); $result = external_api::clean_returnvalue(delete_content::execute_returns(), $result); $this->assertFalse($result['result']); $this->assertCount(1, $result['warnings']); $warning = array_shift($result['warnings']); $this->assertEquals('exception', $warning['warningcode']); $this->assertEquals($managerrecord1->id, $warning['item']); $this->assertEquals(1, $DB->count_records('contentbank_content')); } } PK*\!.&tests/external/rename_content_test.phpnu[. /** * Core content bank external functions tests. * * @package core_contentbank * @category external * @copyright 2020 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.9 */ namespace core_contentbank\external; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php'); require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php'); require_once($CFG->dirroot . '/webservice/tests/helpers.php'); use core_external\external_api; /** * Core content bank external functions tests. * * @package core_contentbank * @copyright 2020 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \core_contentbank\external\rename_content */ final class rename_content_test extends \externallib_advanced_testcase { /** * Data provider for test_rename_content. * * @return array */ public static function rename_content_provider(): array { return [ 'Standard name' => ['New name', 'New name', true], 'Name with digits' => ['Today is 17/04/2017', 'Today is 17/04/2017', true], 'Name with symbols' => ['Follow us: @moodle', 'Follow us: @moodle', true], 'Name with tags' => ['This is bold', 'This is bold', true], 'Long name' => [str_repeat('a', 100), str_repeat('a', 100), true], 'Too long name' => [str_repeat('a', 300), str_repeat('a', 255), true], 'Empty name' => ['', 'Test content ', false], 'Blanks only' => [' ', 'Test content ', false], 'Zero name' => ['0', '0', true], ]; } /** * Test the behaviour of rename_content() for users with permission. * * @dataProvider rename_content_provider * @param string $newname The name to set * @param string $expectedname The name result * @param bool $expectedresult The bolean result expected when renaming * * @covers ::execute */ public function test_rename_content_with_permission(string $newname, string $expectedname, bool $expectedresult): void { global $DB; $this->resetAfterTest(); // Create users. $roleid = $DB->get_field('role', 'id', ['shortname' => 'editingteacher']); $teacher = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->role_assign($roleid, $teacher->id); $this->setUser($teacher); // Add some content to the content bank as teacher. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); $contents = $generator->generate_contentbank_data('contenttype_testable', 1, $teacher->id); $content = array_shift($contents); $oldname = $content->get_name(); // Call the WS and check the content is renamed as expected. $result = rename_content::execute($content->get_id(), $newname); $result = external_api::clean_returnvalue(rename_content::execute_returns(), $result); $this->assertEquals($expectedresult, $result['result']); $record = $DB->get_record('contentbank_content', ['id' => $content->get_id()]); $this->assertEquals($expectedname, $record->name); // Call the WS using an unexisting contentid and check an error is thrown. $this->expectException(\invalid_response_exception::class); $result = rename_content::execute_returns($content->get_id() + 1, $oldname); $result = external_api::clean_returnvalue(rename_content::execute_returns(), $result); $this->assertFalse($result['result']); } /** * Test the behaviour of rename_content() for users with permission. * * @covers ::execute */ public function test_rename_content_without_permission(): void { global $DB; $this->resetAfterTest(); // Create users. $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); // Add some content to the content bank as teacher. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); $contents = $generator->generate_contentbank_data('contenttype_testable', 1, $teacher->id); $content = array_shift($contents); $oldname = $content->get_name(); $newname = 'New name'; // Call the WS and check the content has not been renamed by the student. $this->setUser($student); $result = rename_content::execute($content->get_id(), $newname); $result = external_api::clean_returnvalue(rename_content::execute_returns(), $result); $this->assertFalse($result['result']); $record = $DB->get_record('contentbank_content', ['id' => $content->get_id()]); $this->assertEquals($oldname, $record->name); $this->assertNotEquals($newname, $record->name); } } PK*\. eetests/contenttype_test.phpnu[. namespace core_contentbank; use stdClass; use context_system; use context_user; use Exception; use contenttype_testable\contenttype as contenttype; /** * Test for content bank contenttype class. * * @package core_contentbank * @category test * @copyright 2020 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \core_contentbank\contenttype * */ final class contenttype_test extends \advanced_testcase { /** @var int Identifier for the manager role. */ protected $managerroleid; /** @var stdClass Manager user. */ protected $manager1; /** @var stdClass Manager user. */ protected $manager2; /** @var stdClass User. */ protected $user; /** @var array List of contents created (every user has a key with contents created by her). */ protected $contents = []; /** @var contenttype The contenttype instance. */ protected $contenttype; /** * Setup to ensure that fixtures are loaded. */ public static function setupBeforeClass(): void { global $CFG; require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php'); require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php'); } /** * Tests get_contenttype_name result. * * @covers ::get_contenttype_name */ public function test_get_contenttype_name(): void { $this->resetAfterTest(); $systemcontext = \context_system::instance(); $testable = new contenttype($systemcontext); $this->assertEquals('contenttype_testable', $testable->get_contenttype_name()); } /** * Tests get_plugin_name result. * * @covers ::get_plugin_name */ public function test_get_plugin_name(): void { $this->resetAfterTest(); $systemcontext = \context_system::instance(); $testable = new contenttype($systemcontext); $this->assertEquals('testable', $testable->get_plugin_name()); } /** * Tests get_icon result. * * @covers ::get_icon */ public function test_get_icon(): void { global $CFG; $this->resetAfterTest(); $systemcontext = \context_system::instance(); $testable = new contenttype($systemcontext); $record = new stdClass(); $record->name = 'New content'; $content = $testable->create_content($record); $this->assertEquals( "{$CFG->wwwroot}/theme/image.php/boost/core/1/f/unknown", $testable->get_icon($content), ); } /** * Tests is_feature_supported behavior . * * @covers ::is_feature_supported */ public function test_is_feature_supported(): void { $this->resetAfterTest(); $systemcontext = \context_system::instance(); $testable = new contenttype($systemcontext); $this->assertTrue($testable->is_feature_supported(contenttype::CAN_TEST)); $this->assertFalse($testable->is_feature_supported(contenttype::CAN_UPLOAD)); } /** * Tests can_upload behavior with no implemented upload feature. * * @covers ::can_upload */ public function test_no_upload_feature_supported(): void { $this->resetAfterTest(); $systemcontext = \context_system::instance(); $testable = new contenttype($systemcontext); $this->setAdminUser(); $this->assertFalse($testable->is_feature_supported(contenttype::CAN_UPLOAD)); $this->assertFalse($testable->can_upload()); } /** * Test create_content() with empty data. * * @covers ::create_content */ public function test_create_empty_content(): void { $this->resetAfterTest(); // Create empty content. $record = new stdClass(); $contenttype = new contenttype(context_system::instance()); $content = $contenttype->create_content($record); $this->assertEquals('contenttype_testable', $content->get_content_type()); $this->assertInstanceOf('\\contenttype_testable\\content', $content); } /** * Tests for behaviour of create_content() with data. * * @covers ::create_content */ public function test_create_content(): void { $this->resetAfterTest(); // Create content. $record = new stdClass(); $record->name = 'Test content'; $record->configdata = ''; $record->contenttype = ''; $contenttype = new contenttype(context_system::instance()); $content = $contenttype->create_content($record); $this->assertEquals('contenttype_testable', $content->get_content_type()); $this->assertInstanceOf('\\contenttype_testable\\content', $content); } /** * Tests for behaviour of upload_content() with a file and a record. * * @dataProvider upload_content_provider * @param bool $userecord if a predefined record has to be used. * * @covers ::upload_content */ public function test_upload_content(bool $userecord): void { global $USER; $this->resetAfterTest(); $this->setAdminUser(); $dummy = [ 'contextid' => context_user::instance($USER->id)->id, 'component' => 'user', 'filearea' => 'draft', 'itemid' => 1, 'filepath' => '/', 'filename' => 'file.h5p', 'userid' => $USER->id, ]; $fs = get_file_storage(); $dummyfile = $fs->create_file_from_string($dummy, 'Dummy content'); // Create content. if ($userecord) { $record = new stdClass(); $record->name = 'Test content'; $record->configdata = ''; $record->contenttype = ''; $checkname = $record->name; } else { $record = null; $checkname = $dummyfile->get_filename(); } $contenttype = new contenttype(context_system::instance()); $content = $contenttype->upload_content($dummyfile, $record); $this->assertEquals('contenttype_testable', $content->get_content_type()); $this->assertEquals($checkname, $content->get_name()); $this->assertInstanceOf('\\contenttype_testable\\content', $content); $file = $content->get_file(); $this->assertEquals($dummyfile->get_filename(), $file->get_filename()); $this->assertEquals($dummyfile->get_userid(), $file->get_userid()); $this->assertEquals($dummyfile->get_mimetype(), $file->get_mimetype()); $this->assertEquals($dummyfile->get_contenthash(), $file->get_contenthash()); $this->assertEquals('contentbank', $file->get_component()); $this->assertEquals('public', $file->get_filearea()); $this->assertEquals('/', $file->get_filepath()); } /** * Data provider for test_rename_content. * * @return array */ public static function upload_content_provider(): array { return [ 'With record' => [true], 'Without record' => [false], ]; } /** * Tests for behaviour of upload_content() with a file wrong file. * * @covers ::upload_content */ public function test_upload_content_exception(): void { global $USER, $DB; $this->resetAfterTest(); $this->setAdminUser(); // The testing contenttype thows exception if filename is "error.*". $dummy = [ 'contextid' => context_user::instance($USER->id)->id, 'component' => 'user', 'filearea' => 'draft', 'itemid' => 1, 'filepath' => '/', 'filename' => 'error.txt', 'userid' => $USER->id, ]; $fs = get_file_storage(); $dummyfile = $fs->create_file_from_string($dummy, 'Dummy content'); $contenttype = new contenttype(context_system::instance()); $cbcontents = $DB->count_records('contentbank_content'); // We need to capture the exception to check no content is created. try { $content = $contenttype->upload_content($dummyfile); $this->assertTrue(false); } catch (Exception $e) { $this->assertTrue(true); } $this->assertEquals($cbcontents, $DB->count_records('contentbank_content')); $this->assertEquals(1, $DB->count_records('files', ['contenthash' => $dummyfile->get_contenthash()])); } /** * Tests for behaviour of replace_content() using a dummy file. * * @covers ::replace_content */ public function test_replace_content(): void { global $USER; $this->resetAfterTest(); $this->setAdminUser(); $context = context_system::instance(); // Add some content to the content bank. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); $contents = $generator->generate_contentbank_data('contenttype_testable', 3, 0, $context); $content = reset($contents); $dummy = [ 'contextid' => context_user::instance($USER->id)->id, 'component' => 'user', 'filearea' => 'draft', 'itemid' => 1, 'filepath' => '/', 'filename' => 'file.h5p', 'userid' => $USER->id, ]; $fs = get_file_storage(); $dummyfile = $fs->create_file_from_string($dummy, 'Dummy content'); $contenttype = new contenttype(context_system::instance()); $content = $contenttype->replace_content($dummyfile, $content); $this->assertEquals('contenttype_testable', $content->get_content_type()); $this->assertInstanceOf('\\contenttype_testable\\content', $content); $file = $content->get_file(); $this->assertEquals($dummyfile->get_userid(), $file->get_userid()); $this->assertEquals($dummyfile->get_contenthash(), $file->get_contenthash()); $this->assertEquals('contentbank', $file->get_component()); $this->assertEquals('public', $file->get_filearea()); $this->assertEquals('/', $file->get_filepath()); } /** * Tests for behaviour of replace_content() using an error file. * * @covers ::replace_content */ public function test_replace_content_exception(): void { global $USER; $this->resetAfterTest(); $this->setAdminUser(); $context = context_system::instance(); // Add some content to the content bank. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); $contents = $generator->generate_contentbank_data('contenttype_testable', 3, 0, $context); $content = reset($contents); $dummy = [ 'contextid' => context_user::instance($USER->id)->id, 'component' => 'user', 'filearea' => 'draft', 'itemid' => 1, 'filepath' => '/', 'filename' => 'error.txt', 'userid' => $USER->id, ]; $fs = get_file_storage(); $dummyfile = $fs->create_file_from_string($dummy, 'Dummy content'); $contenttype = new contenttype(context_system::instance()); $this->expectException(Exception::class); $content = $contenttype->replace_content($dummyfile, $content); } /** * Test the behaviour of can_delete(). */ public function test_can_delete(): void { global $DB; $this->resetAfterTest(); $this->contenttype_setup_scenario_data(); $managercontent = array_shift($this->contents[$this->manager1->id]); $usercontent = array_shift($this->contents[$this->user->id]); // Check the content has been created as expected. $records = $DB->count_records('contentbank_content'); $this->assertEquals(4, $records); // Check user can only delete records created by her. $this->setUser($this->user); $this->assertFalse($this->contenttype->can_delete($managercontent)); $this->assertTrue($this->contenttype->can_delete($usercontent)); // Check manager can delete records all the records created. $this->setUser($this->manager1); $this->assertTrue($this->contenttype->can_delete($managercontent)); $this->assertTrue($this->contenttype->can_delete($usercontent)); // Unassign capability to manager role and check not can only delete their own records. unassign_capability('moodle/contentbank:deleteanycontent', $this->managerroleid); $this->assertTrue($this->contenttype->can_delete($managercontent)); $this->assertFalse($this->contenttype->can_delete($usercontent)); $this->setUser($this->manager2); $this->assertFalse($this->contenttype->can_delete($managercontent)); $this->assertFalse($this->contenttype->can_delete($usercontent)); } /** * Test the behaviour of delete_content(). */ public function test_delete_content(): void { global $DB; $this->resetAfterTest(); $this->contenttype_setup_scenario_data(); // Check the content has been created as expected. $this->assertEquals(4, $DB->count_records('contentbank_content')); // Check the content is deleted as expected. $this->setUser($this->manager1); $content = array_shift($this->contents[$this->manager1->id]); $deleted = $this->contenttype->delete_content($content); $this->assertTrue($deleted); $this->assertEquals(3, $DB->count_records('contentbank_content')); } /** * Helper function to setup 3 users (manager1, manager2 and user) and 4 contents (3 created by manager1 and 1 by user). */ protected function contenttype_setup_scenario_data(string $contenttype = 'contenttype_testable'): void { global $DB; $systemcontext = context_system::instance(); // Create users. $this->manager1 = $this->getDataGenerator()->create_user(); $this->manager2 = $this->getDataGenerator()->create_user(); $this->managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager')); $this->getDataGenerator()->role_assign($this->managerroleid, $this->manager1->id); $this->getDataGenerator()->role_assign($this->managerroleid, $this->manager2->id); $editingteacherrolerid = $DB->get_field('role', 'id', ['shortname' => 'editingteacher']); $this->user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->role_assign($editingteacherrolerid, $this->user->id); // Add some content to the content bank. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); $this->contents[$this->manager1->id] = $generator->generate_contentbank_data($contenttype, 3, $this->manager1->id); $this->contents[$this->user->id] = $generator->generate_contentbank_data($contenttype, 1, $this->user->id); $contenttypeclass = "\\$contenttype\\contenttype"; $this->contenttype = new $contenttypeclass($systemcontext); } /** * Data provider for test_rename_content. * * @return array */ public static function rename_content_provider(): array { return [ 'Standard name' => ['New name', 'New name', true], 'Name with digits' => ['Today is 17/04/2017', 'Today is 17/04/2017', true], 'Name with symbols' => ['Follow us: @moodle', 'Follow us: @moodle', true], 'Name with tags' => ['This is bold', 'This is bold', true], 'Long name' => [str_repeat('a', 100), str_repeat('a', 100), true], 'Too long name' => [str_repeat('a', 300), str_repeat('a', 255), true], 'Empty name' => ['', 'Test content ', false], 'Blanks only' => [' ', 'Test content ', false], 'Zero name' => ['0', '0', true], ]; } /** * Test the behaviour of rename_content(). * * @dataProvider rename_content_provider * @param string $newname The name to set * @param string $expected The name result * @param bool $result The bolean result expected when renaming * * @covers ::rename_content */ public function test_rename_content(string $newname, string $expected, bool $result): void { global $DB; $this->resetAfterTest(); // Create course and teacher user. $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); $coursecontext = \context_course::instance($course->id); $contenttype = new contenttype($coursecontext); // Add some content to the content bank as teacher. $this->setUser($teacher); $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); $contents = $generator->generate_contentbank_data('contenttype_testable', 1, $teacher->id); $content = array_shift($contents); $oldname = $content->get_name(); // Check the content is renamed as expected by a user with permission. $renamed = $contenttype->rename_content($content, $newname); $this->assertEquals($result, $renamed); $record = $DB->get_record('contentbank_content', ['id' => $content->get_id()]); $this->assertEquals($expected, $record->name); } /** * Test the behaviour of move_content(). */ public function test_move_content(): void { global $DB; $this->resetAfterTest(); $systemcontext = context_system::instance(); $course = $this->getDataGenerator()->create_course(); $coursecontext = \context_course::instance($course->id); // Add some content to the content bank. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); $systemcontents = $generator->generate_contentbank_data('contenttype_testable', 3, 0, $systemcontext); $generator->generate_contentbank_data('contenttype_testable', 3, 0, $coursecontext); $systemcontent = reset($systemcontents); // Check the content has been created as expected. $this->assertEquals(6, $DB->count_records('contentbank_content')); $this->assertEquals(3, $DB->count_records('contentbank_content', ['contextid' => $systemcontext->id])); $this->assertEquals(3, $DB->count_records('contentbank_content', ['contextid' => $coursecontext->id])); // Check the content files has been created as expected. $this->assertEquals(12, $DB->count_records('files', ['component' => 'contentbank'])); $this->assertEquals(6, $DB->count_records('files', ['component' => 'contentbank', 'contextid' => $systemcontext->id])); $this->assertEquals(6, $DB->count_records('files', ['component' => 'contentbank', 'contextid' => $coursecontext->id])); // Check the content is moved as expected. $contenttype = new contenttype($systemcontext); $this->assertTrue($contenttype->move_content($systemcontent, $coursecontext)); $this->assertEquals(6, $DB->count_records('contentbank_content')); $this->assertEquals(2, $DB->count_records('contentbank_content', ['contextid' => $systemcontext->id])); $this->assertEquals(4, $DB->count_records('contentbank_content', ['contextid' => $coursecontext->id])); // Check the content files were moved as expected. $this->assertEquals(12, $DB->count_records('files', ['component' => 'contentbank'])); $this->assertEquals(4, $DB->count_records('files', ['component' => 'contentbank', 'contextid' => $systemcontext->id])); $this->assertEquals(8, $DB->count_records('files', ['component' => 'contentbank', 'contextid' => $coursecontext->id])); } /** * Test the behaviour of can_manage(). * * @covers ::can_manage */ public function test_can_manage(): void { global $DB, $USER; $this->resetAfterTest(); $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); // Create course and teacher user. $teacherroleid = $DB->get_field('role', 'id', ['shortname' => 'editingteacher']); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); $manager = $this->getDataGenerator()->create_and_enrol($course, 'manager'); $coursecontext = \context_course::instance($course->id); $contenttype = new contenttype($coursecontext); // Add some content to the content bank as admin. $this->setAdminUser(); $contentsbyadmin = $generator->generate_contentbank_data('contenttype_testable', 1, $USER->id, $coursecontext); $contentbyadmin = array_shift($contentsbyadmin); // Add some content to the content bank as teacher. $contentsbyteacher = $generator->generate_contentbank_data('contenttype_testable', 1, $teacher->id, $coursecontext); $contentbyteacher = array_shift($contentsbyteacher); // Check the content has been created as expected. $records = $DB->count_records('contentbank_content'); $this->assertEquals(2, $records); // Check manager can manage by default all the contents created. $this->setUser($manager); $this->assertTrue($contenttype->can_manage($contentbyteacher)); $this->assertTrue($contenttype->can_manage($contentbyadmin)); // Check teacher can only edit their own content. $this->setUser($teacher); $this->assertTrue($contenttype->can_manage($contentbyteacher)); $this->assertFalse($contenttype->can_manage($contentbyadmin)); // Unassign capability to teacher role and check they not can not edit any content. unassign_capability('moodle/contentbank:manageowncontent', $teacherroleid); $this->assertFalse($contenttype->can_manage($contentbyteacher)); $this->assertFalse($contenttype->can_manage($contentbyadmin)); } /** * Test the behaviour of can_download(). * * @covers ::can_download */ public function test_can_download(): void { global $DB; $this->resetAfterTest(); $this->contenttype_setup_scenario_data('contenttype_h5p'); $managercontent = array_shift($this->contents[$this->manager1->id]); $usercontent = array_shift($this->contents[$this->user->id]); // Check the content has been created as expected. $records = $DB->count_records('contentbank_content'); $this->assertEquals(4, $records); // Check user can download content created by anybody. $this->setUser($this->user); $this->assertTrue($this->contenttype->can_download($usercontent)); $this->assertTrue($this->contenttype->can_download($managercontent)); // Check manager can download all the content too. $this->setUser($this->manager1); $this->assertTrue($this->contenttype->can_download($managercontent)); $this->assertTrue($this->contenttype->can_download($usercontent)); // Unassign capability to manager role and check she cannot download content anymore. unassign_capability('moodle/contentbank:downloadcontent', $this->managerroleid); $this->assertFalse($this->contenttype->can_download($managercontent)); $this->assertFalse($this->contenttype->can_download($usercontent)); } /** * Tests get_download_url result. * * @covers ::get_download_url */ public function test_get_download_url(): void { global $CFG; $this->resetAfterTest(); $this->setAdminUser(); $systemcontext = context_system::instance(); // Add some content to the content bank. $filename = 'filltheblanks.h5p'; $filepath = $CFG->dirroot . '/h5p/tests/fixtures/' . $filename; $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); $contents = $generator->generate_contentbank_data('contenttype_testable', 1, 0, $systemcontext, true, $filepath); $content = array_shift($contents); // Check the URL is returned OK for a content with file. $contenttype = new contenttype($systemcontext); $url = $contenttype->get_download_url($content); $this->assertNotEmpty($url); $this->assertStringContainsString($filename, $url); // Check the URL is empty when the content hasn't any file. $record = new stdClass(); $content = $contenttype->create_content($record); $url = $contenttype->get_download_url($content); $this->assertEmpty($url); } /** * Tests pluginfile result. * * @covers ::__construct */ public function test_pluginfile(): void { $this->resetAfterTest(); $this->setAdminUser(); $systemcontext = context_system::instance(); $contenttype = new contenttype($systemcontext); $this->assertIsCallable([$contenttype, 'pluginfile']); } } PK*\f`v E Etests/privacy/provider_test.phpnu[. /** * Base class for unit tests for core_contentbank. * * @package core_contentbank * @category test * @copyright 2020 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_contentbank\privacy; use stdClass; use context_system; use context_coursecat; use context_course; use context_user; use core_contentbank\privacy\provider; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\writer; use core_privacy\tests\provider_testcase; use core_privacy\local\request\userlist; use core_privacy\local\request\approved_userlist; /** * Unit tests for contentbank\classes\privacy\provider.php * * @copyright 2020 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class provider_test extends provider_testcase { /** * Setup to ensure that fixtures are loaded. */ public static function setupBeforeClass(): void { global $CFG; require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php'); } /** * Test for provider::get_contexts_for_userid(). */ public function test_get_contexts_for_userid(): void { $this->resetAfterTest(); // Setup scenario. $scenario = $this->setup_scenario(); // Testing againts Manager who has content in the three contexts. $contextlist = provider::get_contexts_for_userid($scenario->manager->id); // There are three contexts in the list. $contextlistids = $contextlist->get_contextids(); $this->assertCount(3, $contextlistids); // Check the list against the expected list of contexts. $this->assertContainsEquals($scenario->systemcontext->id, $contextlistids); $this->assertContainsEquals($scenario->coursecategorycontext->id, $contextlistids); $this->assertContainsEquals($scenario->coursecontext->id, $contextlistids); // Testing againts Teacher who has content in the one context. $contextlist = provider::get_contexts_for_userid($scenario->teacher->id); // There are only one context in the list. $contextlistids = $contextlist->get_contextids(); $this->assertCount(1, $contextlistids); // Check the againts Course Context. $this->assertContainsEquals($scenario->coursecontext->id, $contextlistids); // And there is not a System and Course Category Context. $this->assertNotContainsEquals($scenario->systemcontext->id, $contextlistids); $this->assertNotContainsEquals($scenario->coursecategorycontext->id, $contextlistids); } /** * Test for provider::get_users_in_context(). */ public function test_get_users_in_context(): void { $this->resetAfterTest(); // Setup scenario. $scenario = $this->setup_scenario(); // Get the userlist to Context System, only Manager will be there. $userlist = new userlist($scenario->systemcontext, 'core_contentbank'); provider::get_users_in_context($userlist); $this->assertEquals([$scenario->manager->id], $userlist->get_userids()); // Teacher will not be there. $this->assertNotEquals([$scenario->teacher->id], $userlist->get_userids()); // Get the userlist to Context Course, Manager and Teacher will be there. $userlist = new userlist($scenario->coursecontext, 'core_contentbank'); provider::get_users_in_context($userlist); $expected = [$scenario->manager->id, $scenario->teacher->id]; sort($expected); $actual = $userlist->get_userids(); sort($actual); $this->assertEquals($expected, $actual); } /** * Test for provider::test_export_user_data(). */ public function test_export_user_data(): void { $this->resetAfterTest(); // Setup scenario. $scenario = $this->setup_scenario(); $subcontexts = [ get_string('name', 'core_contentbank') ]; // Get the data for the System Context. $writer = writer::with_context($scenario->systemcontext); $this->assertFalse($writer->has_any_data()); // Export data for Manager. $this->export_context_data_for_user($scenario->manager->id, $scenario->systemcontext, 'core_contentbank'); $data = $writer->get_data($subcontexts); $this->assertCount(3, (array) $data); $this->assertCount(3, $writer->get_files($subcontexts)); // Get the data for the Course Categoy Context. $writer = writer::with_context($scenario->coursecategorycontext); // Export data for Manager. $this->export_context_data_for_user($scenario->manager->id, $scenario->coursecategorycontext, 'core_contentbank'); $data = $writer->get_data($subcontexts); $this->assertCount(2, (array) $data); $this->assertCount(2, $writer->get_files($subcontexts)); // Get the data for the Course Context. $writer = writer::with_context($scenario->coursecontext); // Export data for Manager. $this->export_context_data_for_user($scenario->manager->id, $scenario->coursecontext, 'core_contentbank'); $data = $writer->get_data($subcontexts); $this->assertCount(2, (array) $data); $this->assertCount(2, $writer->get_files($subcontexts)); // Export data for Teacher. $writer = writer::reset(); $writer = writer::with_context($scenario->coursecontext); $this->export_context_data_for_user($scenario->teacher->id, $scenario->coursecontext, 'core_contentbank'); $data = $writer->get_data($subcontexts); $this->assertCount(3, (array) $data); $this->assertCount(3, $writer->get_files($subcontexts)); } /** * 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(); // Setup scenario. $scenario = $this->setup_scenario(); // Before delete data, we have 4 contents. // - 3 in a system context. // - 2 in a course category context. // - 5 in a course context (2 by manager and 3 by teacher). // Delete data based on system context. provider::delete_data_for_all_users_in_context($scenario->systemcontext); $count = $DB->count_records('contentbank_content'); // 3 content should be deleted. // 7 contents should be remain. $this->assertEquals(7, $count); // Delete data based on course category context. provider::delete_data_for_all_users_in_context($scenario->coursecategorycontext); $count = $DB->count_records('contentbank_content'); // 2 contents should be deleted. // 5 content should be remain. $this->assertEquals(5, $count); // Delete data based on course context. provider::delete_data_for_all_users_in_context($scenario->coursecontext); $count = $DB->count_records('contentbank_content'); // 5 content should be deleted. // 0 content should be remain. $this->assertEquals(0, $count); } /** * Test for provider::test_delete_data_for_users(). */ public function test_delete_data_for_users(): void { global $DB; $this->resetAfterTest(); // Setup scenario. $scenario = $this->setup_scenario(); // Before delete data, we have 4 contents. // - 3 in a system context. // - 2 in a course category context. // - 5 in a course context (2 by manager and 3 by teacher). // A list of users who has created content in Course Category Context. $userlist1 = new userlist($scenario->coursecategorycontext, 'core_contentbank'); provider::get_users_in_context($userlist1); $this->assertCount(1, $userlist1); // Only Manager should be. $this->assertEquals([$scenario->manager->id], $userlist1->get_userids()); // A list of users who has created content in Course Context. $userlist2 = new userlist($scenario->coursecontext, 'core_contentbank'); provider::get_users_in_context($userlist2); $this->assertCount(2, $userlist2); // Manager and Teacher should be. $expected = [$scenario->manager->id, $scenario->teacher->id]; sort($expected); $actual = $userlist2->get_userids(); sort($actual); $this->assertEquals($expected, $actual); // Convert $userlist1 into an approved_contextlist. $approvedlist1 = new approved_userlist($scenario->coursecategorycontext, 'core_contentbank', $userlist1->get_userids()); // Delete data for users in course category context. provider::delete_data_for_users($approvedlist1); // Re-fetch users in course category context. $userlist1 = new userlist($scenario->coursecategorycontext, 'core_contentbank'); provider::get_users_in_context($userlist1); // The user data in course category context should be deleted. $this->assertCount(0, $userlist1); // Re-fetch users in course category context. $userlist2 = new userlist($scenario->coursecontext, 'core_contentbank'); provider::get_users_in_context($userlist2); // The user data in course context should be still present. $this->assertCount(2, $userlist2); // Convert $userlist2 into an approved_contextlist. $approvedlist2 = new approved_userlist($scenario->coursecontext, 'core_contentbank', $userlist2->get_userids()); // Delete data for users in course context. provider::delete_data_for_users($approvedlist2); $userlist2 = new userlist($scenario->coursecontext, 'core_contentbank'); provider::get_users_in_context($userlist2); // The user data in course context should be deleted. $this->assertCount(0, $userlist2); } /** * Test for provider::delete_data_for_user(). */ public function test_delete_data_for_user(): void { global $DB; $this->resetAfterTest(); // Setup scenario. $scenario = $this->setup_scenario(); // Before delete data, we have 4 contents. // - 3 in a system context. // - 2 in a course category context. // - 5 in a course context (2 by manager and 3 by teacher). // Get all the context for Manager. $contextlist = provider::get_contexts_for_userid($scenario->manager->id); $approvedcontextlist = new approved_contextlist($scenario->manager, 'core_contentbank', $contextlist->get_contextids()); // Delete all the data created by the Manager in all the contexts. provider::delete_data_for_user($approvedcontextlist); // After deletion, only 3 content for teacher should be present. $count = $DB->count_records('contentbank_content'); $this->assertEquals(3, $count); // Confirm that the remaining content was created by the teacher. $count = $DB->count_records('contentbank_content', ['usercreated' => $scenario->teacher->id]); $this->assertEquals(3, $count); // Get all the context for Teacher. $contextlist = provider::get_contexts_for_userid($scenario->teacher->id); $approvedcontextlist = new approved_contextlist($scenario->teacher, 'core_contentbank', $contextlist->get_contextids()); // Delete all the data created by the Teacher in all the contexts. provider::delete_data_for_user($approvedcontextlist); // After deletion, no content should be present. $count = $DB->count_records('contentbank_content'); $this->assertEquals(0, $count); } /** * Create a complex scenario to use into the tests. * * @return stdClass $scenario */ protected function setup_scenario() { global $DB; $systemcontext = context_system::instance(); $manager = $this->getDataGenerator()->create_user(); $managerroleid = $DB->get_field('role', 'id', ['shortname' => 'manager']); $this->getDataGenerator()->role_assign($managerroleid, $manager->id); $coursecategory = $this->getDataGenerator()->create_category(); $coursecategorycontext = context_coursecat::instance($coursecategory->id); $course = $this->getDataGenerator()->create_course(); $coursecontext = context_course::instance($course->id); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); // Add some content to the content bank. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); // Add contents by Manager in Context System. $records = $generator->generate_contentbank_data('contenttype_testable', 1, $manager->id, $systemcontext, false, 'systemtestfile1.h5p'); $records = $generator->generate_contentbank_data('contenttype_testable', 1, $manager->id, $systemcontext, false, 'systemtestfile2.h5p'); $records = $generator->generate_contentbank_data('contenttype_testable', 1, $manager->id, $systemcontext, false, 'systemtestfile3.h5p'); // Add contents by Manager in Context Course Category. $records = $generator->generate_contentbank_data('contenttype_testable', 1, $manager->id, $coursecategorycontext, false, 'coursecattestfile1.h5p'); $records = $generator->generate_contentbank_data('contenttype_testable', 1, $manager->id, $coursecategorycontext, false, 'coursecattestfile2.h5p'); // Add contents by Manager in Context Course. $records = $generator->generate_contentbank_data('contenttype_testable', 1, $manager->id, $coursecontext, false, 'coursetestfile1.h5p'); $records = $generator->generate_contentbank_data('contenttype_testable', 1, $manager->id, $coursecontext, false, 'coursetestfile2.h5p'); // Add contents by Teacher. $records = $generator->generate_contentbank_data('contenttype_testable', 1, $teacher->id, $coursecontext, false, 'courseteacherfile1.h5p'); $records = $generator->generate_contentbank_data('contenttype_testable', 1, $teacher->id, $coursecontext, false, 'courseteacherfile2.h5p'); $records = $generator->generate_contentbank_data('contenttype_testable', 1, $teacher->id, $coursecontext, false, 'courseteacherfile3.h5p'); $scenario = new stdClass(); $scenario->systemcontext = $systemcontext; $scenario->coursecategorycontext = $coursecategorycontext; $scenario->coursecontext = $coursecontext; $scenario->manager = $manager; $scenario->teacher = $teacher; return $scenario; } /** * Ensure that export_user_preferences returns no data if the user has not visited any content bank. */ public function test_export_user_preferences_no_pref(): void { global $DB; $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $managerroleid = $DB->get_field('role', 'id', ['shortname' => 'manager']); $this->getDataGenerator()->role_assign($managerroleid, $user->id); provider::export_user_preferences($user->id); $writer = writer::with_context(context_system::instance()); $this->assertFalse($writer->has_any_data()); } /** * Test for provider::test_export_user_preferences(). */ public function test_export_user_preferences(): void { global $DB; // Test setup. $this->resetAfterTest(true); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); set_user_preference('core_contentbank_view_list', 1); // Test the user preferences export contains 1 user preference record for the User. provider::export_user_preferences($user->id); $contextuser = context_user::instance($user->id); $writer = writer::with_context($contextuser); $this->assertTrue($writer->has_any_data()); $prefs = $writer->get_user_preferences('core_contentbank'); $this->assertCount(1, (array) $prefs); $this->assertEquals(1, $prefs->core_contentbank_view_list->value); $this->assertEquals( get_string('privacy:request:preference:set', 'core_contentbank', (object) [ 'name' => 'core_contentbank_view_list', 'value' => $prefs->core_contentbank_view_list->value, ]), $prefs->core_contentbank_view_list->description ); } } PK*\*i${f{ftests/contentbank_test.phpnu[. /** * Test for extensions manager. * * @package core_contentbank * @category test * @copyright 2020 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_contentbank; defined('MOODLE_INTERNAL') || die(); use advanced_testcase; use context_block; use context_course; use context_coursecat; use context_module; use context_system; use context_user; use Exception; global $CFG; require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php'); require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php'); /** * Test for extensions manager. * * @package core_contentbank * @category test * @copyright 2020 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \core_contentbank\contentbank */ final class contentbank_test extends advanced_testcase { /** * Setup to ensure that fixtures are loaded. */ public static function setupBeforeClass(): void { global $CFG; require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php'); } /** * Data provider for test_get_extension_supporter. * * @return array */ public static function get_extension_provider(): array { return [ 'H5P file' => ['something.h5p', '.h5p'], 'PDF file' => ['something.pdf', '.pdf'] ]; } /** * Tests for get_extension() function. * * @dataProvider get_extension_provider * @param string $filename The filename given * @param string $expected The extension of the file * * @covers ::get_extension */ public function test_get_extension(string $filename, string $expected): void { $this->resetAfterTest(); $cb = new contentbank(); $extension = $cb->get_extension($filename); $this->assertEquals($expected, $extension); } /** * Data provider for test_load_context_supported_extensions. * * @return array */ public static function get_extension_supporters_provider(): array { return [ 'H5P first' => [['.h5p' => ['h5p', 'testable']], '.h5p', 'h5p'], 'Testable first (but upload not implemented)' => [['.h5p' => ['testable', 'h5p']], '.h5p', 'h5p'], ]; } /** * Tests for get_extension_supporter() function with admin permissions. * * @dataProvider get_extension_supporters_provider * @param array $supporters The content type plugin supporters for each extension * @param string $extension The extension of the file given * @param string $expected The supporter contenttype of the file * * @covers ::load_context_supported_extensions */ public function test_get_extension_supporter_for_admins(array $supporters, string $extension, string $expected): void { $this->resetAfterTest(); $cb = new contentbank(); $systemcontext = context_system::instance(); // All contexts allowed for admins. $this->setAdminUser(); $contextsupporters = $cb->load_context_supported_extensions($systemcontext); $this->assertArrayHasKey($extension, $contextsupporters); $this->assertEquals($expected, $contextsupporters[$extension]); } /** * Tests for get_extension_supporter() function with user default permissions. * * @dataProvider get_extension_supporters_provider * @param array $supporters The content type plugin supporters for each extension * @param string $extension The extension of the file given * @param string $expected The supporter contenttype of the file * * @covers ::load_context_supported_extensions */ public function test_get_extension_supporter_for_users(array $supporters, string $extension, string $expected): void { $this->resetAfterTest(); $cb = new contentbank(); $systemcontext = context_system::instance(); // Set a user with no permissions. $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Users with no capabilities can't upload content. $contextsupporters = $cb->load_context_supported_extensions($systemcontext); $this->assertEquals([], $contextsupporters); } /** * Tests for get_extension_supporter() function with teacher defaul permissions. * * @dataProvider get_extension_supporters_provider * @param array $supporters The content type plugin supporters for each extension * @param string $extension The extension of the file given * @param string $expected The supporter contenttype of the file * * @covers ::load_context_supported_extensions */ public function test_get_extension_supporter_for_teachers(array $supporters, string $extension, string $expected): void { $this->resetAfterTest(); $cb = new contentbank(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); $this->setUser($teacher); $coursecontext = context_course::instance($course->id); // Teachers has permission in their context to upload supported by H5P content type. $contextsupporters = $cb->load_context_supported_extensions($coursecontext); $this->assertArrayHasKey($extension, $contextsupporters); $this->assertEquals($expected, $contextsupporters[$extension]); } /** * Tests for get_extension_supporter() function. * * @dataProvider get_extension_supporters_provider * @param array $supporters The content type plugin supporters for each extension * @param string $extension The extension of the file given * @param string $expected The supporter contenttype of the file * * @covers ::get_extension_supporter */ public function test_get_extension_supporter(array $supporters, string $extension, string $expected): void { $this->resetAfterTest(); $cb = new contentbank(); $systemcontext = context_system::instance(); $this->setAdminUser(); $supporter = $cb->get_extension_supporter($extension, $systemcontext); $this->assertEquals($expected, $supporter); } /** * Test the behaviour of search_contents(). * * @dataProvider search_contents_provider * @param string $search String to search. * @param string $where Context where to search. * @param int $expectedresult Expected result. * @param array $contexts List of contexts where to create content. */ public function test_search_contents(?string $search, string $where, int $expectedresult, array $contexts = [], ?array $contenttypes = null): void { global $DB, $CFG; $this->resetAfterTest(); $this->setAdminUser(); // Create users. $managerroleid = $DB->get_field('role', 'id', ['shortname' => 'manager']); $manager = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->role_assign($managerroleid, $manager->id); // Create a category and a course. $coursecat = $this->getDataGenerator()->create_category(); $course = $this->getDataGenerator()->create_course(); $existingcontexts = []; $existingcontexts['system'] = \context_system::instance(); $existingcontexts['category'] = \context_coursecat::instance($coursecat->id); $existingcontexts['course'] = \context_course::instance($course->id); if (empty($where)) { $contextid = 0; } else { $contextid = $existingcontexts[$where]->id; } // Add some content to the content bank. $filepath = $CFG->dirroot . '/h5p/tests/fixtures/filltheblanks.h5p'; $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); foreach ($contexts as $context) { $contextinstance = $existingcontexts[$context]; $records = $generator->generate_contentbank_data('contenttype_h5p', 3, $manager->id, $contextinstance, false, $filepath); } // Search for some content. $cb = new contentbank(); $contents = $cb->search_contents($search, $contextid, $contenttypes); $this->assertCount($expectedresult, $contents); if (!empty($contents) && !empty($search)) { foreach ($contents as $content) { $this->assertStringContainsString($search, $content->get_name()); } } } /** * Data provider for test_search_contents(). * * @return array */ public static function search_contents_provider(): array { return [ 'Search all content in all contexts' => [ null, '', 9, ['system', 'category', 'course'] ], 'Search in all contexts for existing string in all contents' => [ 'content', '', 9, ['system', 'category', 'course'] ], 'Search in all contexts for unexisting string in all contents' => [ 'chocolate', '', 0, ['system', 'category', 'course'] ], 'Search in all contexts for existing string in some contents' => [ '1', '', 3, ['system', 'category', 'course'] ], 'Search in all contexts for existing string in some contents (create only 1 context)' => [ '1', '', 1, ['system'] ], 'Search in system context for existing string in all contents' => [ 'content', 'system', 3, ['system', 'category', 'course'] ], 'Search in category context for unexisting string in all contents' => [ 'chocolate', 'category', 0, ['system', 'category', 'course'] ], 'Search in course context for existing string in some contents' => [ '1', 'course', 1, ['system', 'category', 'course'] ], 'Search in system context' => [ null, 'system', 3, ['system', 'category', 'course'] ], 'Search in course context with existing content' => [ null, 'course', 3, ['system', 'category', 'course'] ], 'Search in course context without existing content' => [ null, 'course', 0, ['system', 'category'] ], 'Search in an empty contentbank' => [ null, '', 0, [] ], 'Search in a context in an empty contentbank' => [ null, 'system', 0, [] ], 'Search for a string in an empty contentbank' => [ 'content', '', 0, [] ], 'Search with unexisting content-type' => [ null, 'course', 0, ['system', 'category', 'course'], ['contenttype_unexisting'], ], ]; } /** * Test create_content_from_file function. * * @covers ::create_content_from_file */ public function test_create_content_from_file(): void { global $USER, $CFG; $this->resetAfterTest(); $this->setAdminUser(); $systemcontext = \context_system::instance(); $name = 'greeting-card.h5p'; // Create a dummy H5P file. $dummyh5p = array( 'contextid' => $systemcontext->id, 'component' => 'contentbank', 'filearea' => 'public', 'itemid' => 1, 'filepath' => '/', 'filename' => $name, 'userid' => $USER->id ); $path = $CFG->dirroot . '/h5p/tests/fixtures/' . $name; $dummyh5pfile = \core_h5p\helper::create_fake_stored_file_from_path($path); $cb = new contentbank(); $content = $cb->create_content_from_file($systemcontext, $USER->id, $dummyh5pfile); $this->assertEquals('contenttype_h5p', $content->get_content_type()); $this->assertInstanceOf('\\contenttype_h5p\\content', $content); $this->assertEquals($name, $content->get_name()); } /** * Test the behaviour of delete_contents(). * * @covers ::delete_contents */ public function test_delete_contents(): void { global $DB; $this->resetAfterTest(); $cb = new \core_contentbank\contentbank(); // Create a category and two courses. $systemcontext = context_system::instance(); $coursecat = $this->getDataGenerator()->create_category(); $coursecatcontext = context_coursecat::instance($coursecat->id); $course1 = $this->getDataGenerator()->create_course(); $course1context = context_course::instance($course1->id); $course2 = $this->getDataGenerator()->create_course(); $course2context = context_course::instance($course2->id); // Add some content to the content bank. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); $systemcontent = $generator->generate_contentbank_data(null, 3, 0, $systemcontext); $categorycontent = $generator->generate_contentbank_data(null, 3, 0, $coursecatcontext); $course1content = $generator->generate_contentbank_data(null, 3, 0, $course1context); $course2content = $generator->generate_contentbank_data(null, 3, 0, $course2context); // Check the content has been created as expected. $this->assertEquals(12, $DB->count_records('contentbank_content')); // Check the system content is deleted as expected and the rest of the content is not. $this->assertTrue($cb->delete_contents($systemcontext)); $this->assertEquals(0, $DB->count_records('contentbank_content', ['contextid' => $systemcontext->id])); // And the rest of the context content exists. $this->assertEquals(9, $DB->count_records('contentbank_content')); // Check the course category content is deleted as expected and the rest of the content is not. $this->assertTrue($cb->delete_contents($coursecatcontext)); $this->assertEquals(0, $DB->count_records('contentbank_content', ['contextid' => $coursecatcontext->id])); // And the rest of the context content exists. $this->assertEquals(6, $DB->count_records('contentbank_content')); // Check the course content is deleted as expected and the rest of the content is not. $this->assertTrue($cb->delete_contents($course1context)); $this->assertEquals(0, $DB->count_records('contentbank_content', ['contextid' => $course1context->id])); // And the rest of the context content exists. $this->assertEquals(3, $DB->count_records('contentbank_content')); } /** * Test the behaviour of delete_contents() for empty content bank. * * @covers ::delete_contents */ public function test_delete_contents_for_empty_contentbank(): void { $this->resetAfterTest(); $cb = new \core_contentbank\contentbank(); // Create a category and two courses. $systemcontext = \context_system::instance(); $coursecat = $this->getDataGenerator()->create_category(); $coursecatcontext = \context_coursecat::instance($coursecat->id); $course = $this->getDataGenerator()->create_course(); $coursecontext = \context_course::instance($course->id); // Check there's no error when trying to delete content from an empty content bank. $this->assertTrue($cb->delete_contents($systemcontext)); $this->assertTrue($cb->delete_contents($coursecatcontext)); $this->assertTrue($cb->delete_contents($coursecontext)); } /** * Test the behaviour of move_contents(). * * @covers ::move_contents */ public function test_move_contents(): void { global $DB; $this->resetAfterTest(); $cb = new \core_contentbank\contentbank(); // Create a category and two courses. $course1 = $this->getDataGenerator()->create_course(); $course1context = context_course::instance($course1->id); $course2 = $this->getDataGenerator()->create_course(); $course2context = context_course::instance($course2->id); // Add some content to the content bank. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); $course1content = $generator->generate_contentbank_data(null, 3, 0, $course1context); $course2content = $generator->generate_contentbank_data(null, 3, 0, $course2context); // Check the content has been created as expected. $this->assertEquals(6, $DB->count_records('contentbank_content')); $this->assertEquals(3, $DB->count_records('contentbank_content', ['contextid' => $course1context->id])); // Check the content is moved to another context as expected and the rest of the content is not. $this->assertTrue($cb->move_contents($course1context, $course2context)); $this->assertEquals(6, $DB->count_records('contentbank_content')); $this->assertEquals(0, $DB->count_records('contentbank_content', ['contextid' => $course1context->id])); $this->assertEquals(6, $DB->count_records('contentbank_content', ['contextid' => $course2context->id])); } /** * Test the behaviour of move_contents() for empty content bank. * * @covers ::move_contents */ public function test_move_contents_for_empty_contentbank(): void { $this->resetAfterTest(); $cb = new \core_contentbank\contentbank(); // Create a category and two courses. $systemcontext = \context_system::instance(); $course = $this->getDataGenerator()->create_course(); $coursecontext = \context_course::instance($course->id); // Check there's no error when trying to move content context from an empty content bank. $this->assertTrue($cb->delete_contents($systemcontext, $coursecontext)); } /** * Data provider for get_contenttypes_with_capability_feature. * * @return array */ public static function get_contenttypes_with_capability_feature_provider(): array { return [ 'no-contenttypes_enabled' => [ 'contenttypesenabled' => [], 'contenttypescanfeature' => [], ], 'contenttype_enabled_noeditable' => [ 'contenttypesenabled' => ['testable'], 'contenttypescanfeature' => [], ], 'contenttype_enabled_editable' => [ 'contenttypesenabled' => ['testable'], 'contenttypescanfeature' => ['testable'], ], 'no-contenttype_enabled_editable' => [ 'contenttypesenabled' => [], 'contenttypescanfeature' => ['testable'], ], ]; } /** * Tests for get_contenttypes_with_capability_feature() function. * * @dataProvider get_contenttypes_with_capability_feature_provider * @param array $contenttypesenabled Content types enabled. * @param array $contenttypescanfeature Content types the user has the permission to use the feature. * * @covers ::get_contenttypes_with_capability_feature */ public function test_get_contenttypes_with_capability_feature(array $contenttypesenabled, array $contenttypescanfeature): void { $this->resetAfterTest(); $cb = new contentbank(); $plugins = []; // Content types not enabled where the user has permission to use a feature. if (empty($contenttypesenabled) && !empty($contenttypescanfeature)) { $enabled = false; // Mock core_plugin_manager class and the method get_plugins_of_type. $pluginmanager = $this->getMockBuilder(\core_plugin_manager::class) ->disableOriginalConstructor() ->onlyMethods(['get_plugins_of_type']) ->getMock(); // Replace protected singletoninstance reference (core_plugin_manager property) with mock object. $ref = new \ReflectionProperty(\core_plugin_manager::class, 'singletoninstance'); $ref->setValue(null, $pluginmanager); // Return values of get_plugins_of_type method. foreach ($contenttypescanfeature as $contenttypepluginname) { $contenttypeplugin = new \stdClass(); $contenttypeplugin->name = $contenttypepluginname; $contenttypeplugin->type = 'contenttype'; // Add the feature to the fake content type. $classname = "\\contenttype_$contenttypepluginname\\contenttype"; $classname::$featurestotest = ['test2']; $plugins[] = $contenttypeplugin; } // Set expectations and return values. $pluginmanager->expects($this->once()) ->method('get_plugins_of_type') ->with('contenttype') ->willReturn($plugins); } else { $enabled = true; // Get access to private property enabledcontenttypes. $rc = new \ReflectionClass(\core_contentbank\contentbank::class); $rcp = $rc->getProperty('enabledcontenttypes'); foreach ($contenttypesenabled as $contenttypename) { $plugins["\\contenttype_$contenttypename\\contenttype"] = $contenttypename; // Add to the testable contenttype the feature to test. if (in_array($contenttypename, $contenttypescanfeature)) { $classname = "\\contenttype_$contenttypename\\contenttype"; $classname::$featurestotest = ['test2']; } } // Set as enabled content types only those in the test. $rcp->setValue($cb, $plugins); } $actual = $cb->get_contenttypes_with_capability_feature('test2', null, $enabled); $this->assertEquals($contenttypescanfeature, array_values($actual)); } /** * Test the behaviour of get_content_from_id() * * @covers ::get_content_from_id */ public function test_get_content_from_id(): void { $this->resetAfterTest(); $cb = new \core_contentbank\contentbank(); // Create a category and two courses. $systemcontext = context_system::instance(); // Add some content to the content bank. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); $contents = $generator->generate_contentbank_data(null, 3, 0, $systemcontext); $content = reset($contents); // Get the content instance form id. $newinstance = $cb->get_content_from_id($content->get_id()); $this->assertEquals($content->get_id(), $newinstance->get_id()); // Now produce and exception with an innexistent id. $this->expectException(Exception::class); $cb->get_content_from_id(0); } /** * Test the behaviour of is_context_allowed(). * * @covers ::is_context_allowed */ public function test_is_context_allowed(): void { $this->resetAfterTest(); $cb = new contentbank(); // System context. $this->assertTrue($cb->is_context_allowed(context_system::instance())); // User context. $user = $this->getDataGenerator()->create_user(); $this->assertFalse($cb->is_context_allowed(context_user::instance($user->id))); // Category context. $category = $this->getDataGenerator()->create_category(); $this->assertTrue($cb->is_context_allowed(context_coursecat::instance($category->id))); // Course context. $course = $this->getDataGenerator()->create_course(['category' => $category->id]); $coursecontext = context_course::instance($course->id); $this->assertTrue($cb->is_context_allowed($coursecontext)); // Module context. $module = $this->getDataGenerator()->create_module('page', ['course' => $course->id]); $this->assertFalse($cb->is_context_allowed(context_module::instance($module->cmid))); // Block context. $block = $this->getDataGenerator()->create_block('online_users', ['parentcontextid' => $coursecontext->id]); $this->assertFalse($cb->is_context_allowed(context_block::instance($block->id))); } } PK*\S`»P9P9tests/content_test.phpnu[. /** * Test for content bank contenttype class. * * @package core_contentbank * @category test * @copyright 2020 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_contentbank; use stdClass; use context_system; use contenttype_testable\contenttype as contenttype; /** * Test for content bank contenttype class. * * @package core_contentbank * @category test * @copyright 2020 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \core_contentbank\content * */ final class content_test extends \advanced_testcase { /** * Setup to ensure that fixtures are loaded. */ public static function setupBeforeClass(): void { global $CFG; require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php'); require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php'); } /** * Tests for behaviour of get_name(). * * @covers ::get_name */ public function test_get_name(): void { $this->resetAfterTest(); // Create content. $record = new stdClass(); $record->name = 'Test content'; $record->configdata = ''; $contenttype = new contenttype(context_system::instance()); $content = $contenttype->create_content($record); $this->assertEquals($record->name, $content->get_name()); } /** * Data provider for test_set_name. * * @return array */ public static function set_name_provider(): array { return [ 'Standard name' => ['New name', 'New name'], 'Name with digits' => ['Today is 17/04/2017', 'Today is 17/04/2017'], 'Name with symbols' => ['Follow us: @moodle', 'Follow us: @moodle'], 'Name with tags' => ['This is bold', 'This is bold'], 'Long name' => [str_repeat('a', 100), str_repeat('a', 100)], 'Too long name' => [str_repeat('a', 300), str_repeat('a', 255)], 'Empty name' => ['', 'Old name'], 'Blanks only' => [' ', 'Old name'], ]; } /** * Tests for 'set_name' behaviour. * * @dataProvider set_name_provider * @param string $newname The name to set * @param string $expected The name result * * @covers ::set_name */ public function test_set_name(string $newname, string $expected): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $oldname = "Old name"; $context = context_system::instance(); // Create content. $record = new stdClass(); $record->name = $oldname; $contenttype = new contenttype($context); $content = $contenttype->create_content($record); $this->assertEquals($oldname, $content->get_name()); $content->set_name($newname); $this->assertEquals($expected, $content->get_name()); $record = $DB->get_record('contentbank_content', ['id' => $content->get_id()]); $this->assertEquals($expected, $record->name); } /** * Tests for behaviour of get_content_type(). * * @covers ::get_content_type */ public function test_get_content_type(): void { $this->resetAfterTest(); // Create content. $record = new stdClass(); $record->name = 'Test content'; $record->configdata = ''; $contenttype = new contenttype(context_system::instance()); $content = $contenttype->create_content($record); $this->assertEquals('contenttype_testable', $content->get_content_type()); } /** * Tests for 'configdata' behaviour. * * @covers ::set_configdata */ public function test_configdata_changes(): void { $this->resetAfterTest(); $configdata = "{img: 'icon.svg'}"; // Create content. $record = new stdClass(); $record->configdata = $configdata; $contenttype = new contenttype(context_system::instance()); $content = $contenttype->create_content($record); $this->assertEquals($configdata, $content->get_configdata()); $configdata = "{alt: 'Name'}"; $content->set_configdata($configdata); $this->assertEquals($configdata, $content->get_configdata()); } /** * Tests for 'set_contextid' behaviour. * * @covers ::set_contextid */ public function test_set_contextid(): void { $this->resetAfterTest(); $this->setAdminUser(); $context = context_system::instance(); $course = $this->getDataGenerator()->create_course(); $newcontext = \context_course::instance($course->id); // Add some content to the content bank. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); $contents = $generator->generate_contentbank_data('contenttype_testable', 3, 0, $context); $content = reset($contents); $oldcontextid = $content->get_contextid(); $file = $content->get_file(); $this->assertEquals($oldcontextid, $file->get_contextid()); $this->assertEquals($context->id, $oldcontextid); $this->assertNotEquals($newcontext->id, $oldcontextid); $content->set_contextid($newcontext->id); $file = $content->get_file(); $this->assertEquals($newcontext->id, $content->get_contextid()); $this->assertEquals($newcontext->id, $file->get_contextid()); } /** * Tests for set_visibility behaviour * * @covers ::set_visibility */ public function test_set_visibility(): void { $this->resetAfterTest(); $this->setAdminUser(); $context = context_system::instance(); $oldvisibility = content::VISIBILITY_PUBLIC; $newvisibility = content::VISIBILITY_UNLISTED; $illegalvisibility = -1; $record = new stdClass(); $record->visibility = $oldvisibility; $contenttype = new contenttype($context); $content = $contenttype->create_content($record); $this->assertEquals($oldvisibility, $content->get_visibility()); $content->set_visibility($newvisibility); $this->assertEquals($newvisibility, $content->get_visibility()); $content->set_visibility($illegalvisibility); $this->assertEquals($newvisibility, $content->get_visibility()); } /** * Tests for 'import_file' behaviour when replacing a file. * * @covers ::import_file */ public function test_import_file_replace(): void { global $USER; $this->resetAfterTest(); $this->setAdminUser(); $context = context_system::instance(); // Add some content to the content bank. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); $contents = $generator->generate_contentbank_data('contenttype_testable', 3, 0, $context); $content = reset($contents); $originalfile = $content->get_file(); // Create a dummy file. $filerecord = array( 'contextid' => $context->id, 'component' => 'contentbank', 'filearea' => 'draft', 'itemid' => $content->get_id(), 'filepath' => '/', 'filename' => 'example.txt' ); $fs = get_file_storage(); $file = $fs->create_file_from_string($filerecord, 'Dummy content '); $importedfile = $content->import_file($file); $this->assertEquals($originalfile->get_filename(), $importedfile->get_filename()); $this->assertEquals($originalfile->get_filearea(), $importedfile->get_filearea()); $this->assertEquals($originalfile->get_filepath(), $importedfile->get_filepath()); $this->assertEquals($originalfile->get_mimetype(), $importedfile->get_mimetype()); $this->assertEquals($file->get_userid(), $importedfile->get_userid()); $this->assertEquals($file->get_contenthash(), $importedfile->get_contenthash()); } /** * Tests for 'import_file' behaviour when uploading a new file. * * @covers ::import_file */ public function test_import_file_upload(): void { global $USER; $this->resetAfterTest(); $this->setAdminUser(); $context = context_system::instance(); $type = new contenttype($context); $record = (object)[ 'name' => 'content name', 'usercreated' => $USER->id, ]; $content = $type->create_content($record); // Create a dummy file. $filerecord = array( 'contextid' => $context->id, 'component' => 'contentbank', 'filearea' => 'draft', 'itemid' => $content->get_id(), 'filepath' => '/', 'filename' => 'example.txt' ); $fs = get_file_storage(); $file = $fs->create_file_from_string($filerecord, 'Dummy content '); $importedfile = $content->import_file($file); $this->assertEquals($file->get_filename(), $importedfile->get_filename()); $this->assertEquals($file->get_userid(), $importedfile->get_userid()); $this->assertEquals($file->get_mimetype(), $importedfile->get_mimetype()); $this->assertEquals($file->get_contenthash(), $importedfile->get_contenthash()); $this->assertEquals('public', $importedfile->get_filearea()); $this->assertEquals('/', $importedfile->get_filepath()); $contentfile = $content->get_file($file); $this->assertEquals($importedfile->get_id(), $contentfile->get_id()); } /** * Tests for 'get_content_type_instance' * * @covers ::get_content_type_instance */ public function test_get_content_type_instance(): void { global $USER; $this->resetAfterTest(); $this->setAdminUser(); $context = context_system::instance(); $type = new contenttype($context); $record = (object)[ 'name' => 'content name', 'usercreated' => $USER->id, ]; $content = $type->create_content($record); $contenttype = $content->get_content_type_instance(); $this->assertInstanceOf(get_class($type), $contenttype); } /** * Tests for 'is_view_allowed'. * * @covers ::is_view_allowed */ public function test_is_view_allowed(): void { $this->resetAfterTest(); $this->setAdminUser(); $context = context_system::instance(); $userauthor = $this->getDataGenerator()->create_user(); $userother = $this->getDataGenerator()->create_user(); $contenttype = new contenttype($context); $unlistedrecord = new stdClass(); $unlistedrecord->visibility = content::VISIBILITY_UNLISTED; $unlistedrecord->usercreated = $userauthor->id; $unlistedcontent = $contenttype->create_content($unlistedrecord); $publicrecord = new stdClass(); $publicrecord->visibility = content::VISIBILITY_PUBLIC; $publicrecord->usercreated = $userauthor->id; $publiccontent = $contenttype->create_content($publicrecord); $this->setUser($userother); $this->assertFalse($unlistedcontent->is_view_allowed()); $this->assertTrue($publiccontent->is_view_allowed()); $this->setUser($userauthor); $this->assertTrue($unlistedcontent->is_view_allowed()); $this->assertTrue($publiccontent->is_view_allowed()); $this->setAdminUser(); $this->assertTrue($unlistedcontent->is_view_allowed()); $this->assertTrue($publiccontent->is_view_allowed()); } /** * Tests for 'get_uses' behaviour. * * @covers ::get_uses */ public function test_get_uses(): void { $this->resetAfterTest(); $this->setAdminUser(); $context = context_system::instance(); // Add some content to the content bank. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); $contents = $generator->generate_contentbank_data('contenttype_testable', 3, 0, $context); $content1 = array_shift($contents); // Check content has no references for now. $this->assertCount(0, $content1->get_uses()); // Add a link to the previous content. $cbfile = $content1->get_file(); $cbrecord = array( 'contextid' => $cbfile->get_contextid(), 'component' => $cbfile->get_component(), 'filearea' => $cbfile->get_filearea(), 'itemid' => $cbfile->get_itemid(), 'filepath' => $cbfile->get_filepath(), 'filename' => $cbfile->get_filename(), ); $fs = get_file_storage(); $ref = $fs->pack_reference($cbrecord); $aliasrecord = new stdClass(); $aliasrecord->contextid = $context->id; $aliasrecord->component = 'core'; $aliasrecord->filearea = 'phpunit'; $aliasrecord->filepath = '/foo/'; $aliasrecord->filename = 'one.txt'; $aliasrecord->itemid = 0; $repos = \repository::get_instances(['type' => 'contentbank']); $cbrepo = reset($repos); $this->assertInstanceOf('repository', $cbrepo); $alias = $fs->create_file_from_reference($aliasrecord, $cbrepo->id, $ref); // Check content now has one reference (the previous alias). $contentuses1 = $content1->get_uses(); $this->assertCount(1, $contentuses1); $reffile = reset($contentuses1); $this->assertEquals($alias, $reffile); // Check a different content hasn't any reference. $content2 = array_shift($contents); $this->assertCount(0, $content2->get_uses()); } } PK*\O` tests/behat/sort_content.featurenu[@core @core_contentbank @core_h5p @contentbank_h5p @_file_upload @javascript Feature: Sort content in the content bank In order to temporarily organise the content of the content bank As an admin I need to be able to sort the content bank in various ways Background: Given I log in as "admin" And I navigate to "H5P > Manage H5P content types" in site administration And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element" And the following "users" exist: | username | firstname | lastname | email | | manager | Max | Manager | man@example.com | And the following "role assigns" exist: | user | role | contextlevel | reference | | manager | manager | System | | And the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | System | | contenttype_h5p | admin | Dragon_santjordi.h5p | /h5p/tests/fixtures/filltheblanks.h5p | | System | | contenttype_h5p | admin | mathsbook.h5p | /h5p/tests/fixtures/filltheblanks.h5p | | System | | contenttype_h5p | manager | historybook.h5p | /h5p/tests/fixtures/filltheblanks.h5p | | System | | contenttype_h5p | admin | santjordi.h5p | /h5p/tests/fixtures/filltheblanks.h5p | | System | | contenttype_h5p | admin | santjordi_rose.h5p | /h5p/tests/fixtures/filltheblanks.h5p | | System | | contenttype_h5p | admin | SantJordi_book | /h5p/tests/fixtures/filltheblanks.h5p | Scenario: Admins can order content in the content bank Given I am on site homepage And I turn editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present And I expand "Site pages" node And I click on "Content bank" "link" When I click on "Display content bank with file details" "button" And I click on "Sort by Content name ascending" "button" And "Dragon_santjordi.h5p" "text" should appear before "historybook.h5p" "text" And "historybook.h5p" "text" should appear before "mathsbook.h5p" "text" And "SantJordi_book" "text" should appear before "santjordi_rose.h5p" "text" And I click on "Sort by Content name descending" "button" And "historybook.h5p" "text" should appear before "Dragon_santjordi.h5p" "text" And "mathsbook.h5p" "text" should appear before "historybook.h5p" "text" Then "santjordi_rose.h5p" "text" should appear before "SantJordi_book" "text" Scenario: Admins can order content depending on the author Given I am on site homepage And I turn editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present And I expand "Site pages" node And I click on "Content bank" "link" When I click on "Display content bank with file details" "button" Then I click on "Sort by Author ascending" "button" And "Dragon_santjordi.h5p" "text" should appear before "historybook.h5p" "text" And "santjordi_rose.h5p" "text" should appear before "historybook" "text" And I click on "Sort by Author descending" "button" And "historybook.h5p" "text" should appear before "Dragon_santjordi.h5p" "text" And "historybook.h5p" "text" should appear before "santjordi_rose" "text" PK*\J)) tests/behat/edit_content.featurenu[@core @core_contentbank @core_h5p @contentbank_h5p @_file_upload @javascript Feature: Content bank use editor feature In order to add/edit content As a user I need to be able to access the edition options Background: Given I log in as "admin" And I am on site homepage And I turn editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present And I configure the "Navigation" block And I set the following fields to these values: | Page contexts | Display throughout the entire site | And I press "Save changes" Scenario: Users see the Add button disabled if there is no content type available for creation Given I click on "Site pages" "list_item" in the "Navigation" "block" When I click on "Content bank" "link" Then the "[data-action=Add-content]" "css_element" should be disabled Scenario: Users can see the Add button if there is content type available for creation Given the following "user private file" exists: | user | admin | | filepath | h5p/tests/fixtures/filltheblanks.h5p | And I click on "Site pages" "list_item" in the "Navigation" "block" And I click on "Content bank" "link" in the "Navigation" "block" And I click on "Upload" "link" And I click on "Choose a file..." "button" And I click on "Private files" "link" in the ".fp-repo-area" "css_element" And I click on "filltheblanks.h5p" "link" And I click on "Select this file" "button" And I click on "Save changes" "button" When I click on "Content bank" "link" And I click on "filltheblanks.h5p" "link" And I click on "Exit" "link" Then I click on "[data-action=Add-content]" "css_element" And I should see "Fill in the Blanks" Scenario: Users can edit content if they have the required permission Given the following "user private file" exists: | user | admin | | filepath | h5p/tests/fixtures/filltheblanks.h5p | And I click on "Site pages" "list_item" in the "Navigation" "block" And I click on "Content bank" "link" in the "Navigation" "block" And I click on "Upload" "link" And I click on "Choose a file..." "button" And I click on "Private files" "link" in the ".fp-repo-area" "css_element" And I click on "filltheblanks.h5p" "link" And I click on "Select this file" "button" And I click on "Save changes" "button" When I click on "Content bank" "link" And I click on "filltheblanks.h5p" "link" Then I click on "Edit" "link" And I switch to "h5p-editor-iframe" class iframe And I switch to the main frame And I change viewport size to "800x1400" And I click on "Cancel" "button" And "filltheblanks.h5p" "heading" should exist Scenario: Users can create new content if they have the required permission Given I navigate to "H5P > Manage H5P content types" in site administration And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element" And I should see "H5P content types uploaded successfully" And I click on "Site pages" "list_item" in the "Navigation" "block" When I click on "Content bank" "link" in the "Navigation" "block" And I click on "[data-action=Add-content]" "css_element" Then I click on "Fill in the Blanks" "link" And I switch to "h5p-editor-iframe" class iframe And I switch to the main frame And I click on "Cancel" "button" Scenario: Users can't edit content if they don't have the required permission Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | user1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And I navigate to "H5P > Manage H5P content types" in site administration And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element" And I should see "H5P content types uploaded successfully" And I log out And I log in as "teacher1" And I am on "Course 1" course homepage And I click on "Site pages" "list_item" in the "Navigation" "block" And I click on "Content bank" "link" And "[data-action=Add-content]" "css_element" should exist When the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/contentbank:useeditor | Prohibit | editingteacher | System | | And I reload the page Then "[data-action=Add-content]" "css_element" should not exist Scenario: Users can edit content and save changes Given the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | System | | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p | And I click on "Site pages" "list_item" in the "Navigation" "block" And I click on "Content bank" "link" in the "Navigation" "block" And I click on "filltheblanks.h5p" "link" And I click on "Edit" "link" And I switch to "h5p-editor-iframe" class iframe And the field "Title" matches value "Geography" And I set the field "Title" to "New title" And I switch to the main frame When I click on "Save" "button" And "filltheblanks.h5p" "heading" should exist And I click on "Edit" "link" And I switch to "h5p-editor-iframe" class iframe Then the field "Title" matches value "New title" Scenario: Teachers can edit their own content in the content bank Given I navigate to "H5P > Manage H5P content types" in site administration And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element" And I upload "h5p/tests/fixtures/ipsums.h5p" file to "H5P content type" filemanager And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element" And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | Course | C1 | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p | | Course | C1 | contenttype_h5p | teacher1 | ipsums.h5p | /h5p/tests/fixtures/ipsums.h5p | When I log out And I log in as "teacher1" And I am on "Course 1" course homepage And I expand "Site pages" node And I click on "Content bank" "link" And I follow "ipsums.h5p" Then "Edit" "link" should exist in the "region-main" "region" Scenario: Teachers can't edit content created by other users in the content bank Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | Course | C1 | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p | | Course | C1 | contenttype_h5p | teacher1 | ipsums.h5p | /h5p/tests/fixtures/ipsums.h5p | When I log out And I log in as "teacher1" And I am on "Course 1" course homepage And I expand "Site pages" node And I click on "Content bank" "link" And I follow "filltheblanks.h5p" Then "Edit" "link" should not exist in the "region-main" "region" Scenario: Teachers keep their content authoring in copied courses Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | Course | C1 | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p | | Course | C1 | contenttype_h5p | teacher1 | ipsums.h5p | /h5p/tests/fixtures/ipsums.h5p | And I am on the "Course 1" "course copy" page And I set the following fields to these values: | Course full name | Copy | | Course short name | Copy | | Teacher | 1 | When I press "Copy and view" And I trigger cron And I am on homepage And I log out And I log in as "teacher1" And I am on "Copy" course homepage And I expand "Site pages" node And I click on "Content bank" "link" And I follow "ipsums.h5p" Then "Edit" "link" should exist in the "region-main" "region" And I navigate to "Content bank" in current page administration And I follow "filltheblanks.h5p" Then "Edit" "link" should not exist in the "region-main" "region" PK*\˟#tests/behat/restore_content.featurenu[@core @core_contentbank @core_h5p @contentbank_h5p Feature: Content bank contents are retained when course is restored In order to restore content bank contents As a manager I need to be able to restore course containing the content bank Background: Given the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "course enrolments" exist: | user | course | role | | admin | C1 | editingteacher | And the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | Course | C1 | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p | And I log in as "admin" And I am on "Course 1" course homepage with editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| # TODO MDL-57120 site "Content bank" link not accessible without navigation block. And I add the "Navigation" block if not present @javascript Scenario: Deleted courses with content banks can be restored Given I navigate to "Courses > Manage courses and categories" in site administration And I click on "delete" action for "Course 1" in management course listing And I click on "Delete" "button" in the "Confirm" "dialogue" And I press "Continue" And I navigate to "Recycle bin" in current page administration And I click on "Restore" "link" in the "Course 1" "table_row" And I am on "Course 1" course homepage And I expand "Site pages" node When I click on "Content bank" "link" And I click on "filltheblanks.h5p" "link" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe Then I should see "Of which countries are Berlin, Washington, Beijing, Canberra and Brasilia the capitals?" PK*\)l $tests/behat/download_content.featurenu[@core @core_contentbank @core_h5p @contentbank_h5p @_file_upload @javascript Feature: Download H5P content from the content bank In order export H5P content from the content bank As an admin I need to be able to download any H5P content from the content bank Background: Given the following "users" exist: | username | firstname | lastname | email | | manager | Max | Manager | man@example.com | And the following "role assigns" exist: | user | role | contextlevel | reference | | manager | manager | System | | And the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | System | | contenttype_h5p | admin | filltheblanksadmin.h5p | /h5p/tests/fixtures/filltheblanks.h5p | | System | | contenttype_h5p | manager | filltheblanksmanager.h5p | /h5p/tests/fixtures/filltheblanks.h5p | And I log in as "admin" And I am on site homepage And I turn editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present And I configure the "Navigation" block And I set the following fields to these values: | Page contexts | Display throughout the entire site | And I press "Save changes" Scenario: Admins can download content from the content bank Given I click on "Site pages" "list_item" in the "Navigation" "block" And I click on "Content bank" "link" in the "Navigation" "block" And I follow "filltheblanksmanager.h5p" And I click on "More" "button" And I should see "Download" When I click on "Download" "link" Then I should see "filltheblanksmanager.h5p" Scenario: Users can download content created by different users Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/contentbank:manageanycontent | Prohibit | manager | System | | And I log out And I log in as "manager" When I click on "Site pages" "list_item" in the "Navigation" "block" And I click on "Content bank" "link" in the "Navigation" "block" And I should see "filltheblanksadmin.h5p" And I follow "filltheblanksadmin.h5p" And I click on "More" "button" Then I should see "Download" And I should not see "Rename" Scenario: Users without the required capability cannot download content Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/contentbank:downloadcontent | Prohibit | manager | System | | And I log out And I log in as "manager" When I click on "Site pages" "list_item" in the "Navigation" "block" And I click on "Content bank" "link" in the "Navigation" "block" And I should see "filltheblanksmanager.h5p" And I follow "filltheblanksmanager.h5p" And I click on "More" "button" Then I should not see "Download" PK*\wr  $tests/behat/view_preferences.featurenu[@core @core_contentbank @core_h5p @contentbank_h5p @javascript Feature: Store the content bank view preference In order to consistantly view the content bank in icons or details view As an admin I need to be able to store my view preference Background: Given the following "user private files" exist: | user | filepath | | admin | h5p/tests/fixtures/filltheblanks.h5p | | admin | h5p/tests/fixtures/greeting-card.h5p | And I log in as "admin" And I am on site homepage And I turn editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present And I configure the "Navigation" block And I set the following fields to these values: | Page contexts | Display throughout the entire site | And I press "Save changes" And I expand "Site pages" node And I click on "Content bank" "link" And I click on "Upload" "link" And I click on "Choose a file..." "button" And I click on "Private files" "link" in the ".fp-repo-area" "css_element" And I click on "filltheblanks.h5p" "link" And I set the field "Save as" to "fib.h5p" And I click on "Select this file" "button" And I click on "Save changes" "button" And I click on "Content bank" "link" And I click on "Upload" "link" And I click on "Choose a file..." "button" And I click on "Private files" "link" in the ".fp-repo-area" "css_element" And I click on "greeting-card.h5p" "link" And I set the field "Save as" to "greetingcard.h5p" And I click on "Select this file" "button" And I click on "Save changes" "button" Scenario: There are several views for displaying contents into the content bank Given I am on site homepage And I expand "Site pages" node And I click on "Content bank" "link" When I click on "Display content bank with file details" "button" Then I should see "Last modified" And I follow "greetingcard.h5p" And I click on "Content bank" "link" And I should see "Last modified" And I click on "Display content bank with icons" "button" And I follow "greetingcard.h5p" And I click on "Content bank" "link" And I should not see "Last modified" Scenario: Display the number of times a content is used in file details view Given I am on the "My private files" page And I click on "Add..." "button" And I select "Content bank" repository in file picker And I click on "fib.h5p" "file" in repository content area And I click on "Link to the file" "radio" And I click on "Select this file" "button" And I click on "Save changes" "button" When I am on site homepage And I expand "Site pages" node And I click on "Content bank" "link" And I click on "Display content bank with file details" "button" Then I should see "1" in the "[data-file='fib.h5p'] .cb-uses" "css_element" And I should see "0" in the "[data-file='greetingcard.h5p'] .cb-uses" "css_element" PK*\b#F T T tests/behat/events.featurenu[@core @core_contentbank @core_h5p @contentbank_h5p @_switch_iframe @javascript Feature: Confirm content bank events are triggered In order to log content bank actions As an admin I need to be able to check triggered events Background: Given the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | Course | C1 | contenttype_h5p | admin | Existing | /h5p/tests/fixtures/filltheblanks.h5p | And the following "user private file" exists: | user | admin | | filepath | h5p/tests/fixtures/filltheblanks.h5p | And the following config values are set as admin: | unaddableblocks | | theme_boost| And I log in as "admin" And I am on "Course 1" course homepage with editing mode on And I add the "Navigation" block if not present Scenario: Content created and uploaded events when uploading a content file Given I navigate to "Reports > Live logs" in site administration And I should not see "Content uploaded" And I should not see "Content created" And I am on "Course 1" course homepage And I expand "Site pages" node And I click on "Content bank" "link" When I click on "Upload" "link" And I click on "Choose a file..." "button" And I click on "Private files" "link" in the ".fp-repo-area" "css_element" And I click on "filltheblanks.h5p" "link" And I click on "Select this file" "button" And I click on "Save changes" "button" And I navigate to "Reports > Live logs" in site administration Then I should see "Content uploaded" And I should see "Content created" Scenario: Content viewed event Given I navigate to "Reports > Live logs" in site administration And I should not see "Content viewed" And I am on "Course 1" course homepage And I expand "Site pages" node And I click on "Content bank" "link" When I click on "Existing" "link" And I navigate to "Reports > Live logs" in site administration Then I should see "Content viewed" Scenario: Content deleted event Given I navigate to "Reports > Live logs" in site administration And I should not see "Content deleted" And I am on "Course 1" course homepage And I expand "Site pages" node And I click on "Content bank" "link" And I click on "Existing" "link" And I click on "More" "button" When I click on "Delete" "link" And I click on "Delete" "button" in the "Delete content" "dialogue" And I navigate to "Reports > Live logs" in site administration Then I should see "Content deleted" Scenario: Content updated event when renaming Given I navigate to "Reports > Live logs" in site administration And I should not see "Content updated" And I am on "Course 1" course homepage And I expand "Site pages" node And I click on "Content bank" "link" And I click on "Existing" "link" And I click on "More" "button" When I click on "Rename" "link" And I set the field "Content name" to "New name" And I click on "Rename" "button" And I navigate to "Reports > Live logs" in site administration Then I should see "Content updated" PK*\*O<<"tests/behat/search_content.featurenu[@repository @repository_contentbank @javascript Feature: Search content bank files using the content bank files repository In order to find the content I need to select in the file picker As a user I need to be able to search in the content bank files repository by content name Background: Given the following "users" exist: | username | firstname | lastname | email | | student | Student | 1 | student@example.com | | teacher | Teacher | 1 | teacher1@example.com | And the following "categories" exist: | name | category | idnumber | | Category1 | 0 | CAT1 | | Category2 | 0 | CAT2 | And the following "courses" exist: | fullname | shortname | category | | Course1 | C1 | CAT1 | | Course2 | C2 | CAT2 | And the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | Course | C1 | contenttype_h5p | admin | coursecontent1.h5p | /h5p/tests/fixtures/filltheblanks.h5p | | Course | C2 | contenttype_h5p | admin | coursecontent2.h5p | /h5p/tests/fixtures/find-the-words.h5p | | Category | CAT1 | contenttype_h5p | admin | categorycontent1.h5p | /h5p/tests/fixtures/ipsums.h5p | | Category | CAT2 | contenttype_h5p | admin | categorycontent2.h5p | /h5p/tests/fixtures/multiple-choice-2-6.h5p | | System | | contenttype_h5p | admin | systemcontent.h5p | /h5p/tests/fixtures/greeting-card.h5p | And the following "activities" exist: | activity | name | intro | introformat | course | idnumber | | folder | Folder | FolderDesc | 1 | C1 | folder | And the following "course enrolments" exist: | user | course | role | | teacher | C1 | editingteacher | Scenario: User can see a search field and reset search button in the content bank files repository Given I am on the Folder "Folder activity" page logged in as admin And I click on "Edit" "button" And I click on "Add..." "button" And I should see "Content bank" in the ".fp-repo-area" "css_element" When I select "Content bank" repository in file picker Then "Search repository" "field" should be visible And "Refresh" "link" should be visible Scenario: User can see search results when there is content that matches the search criteria Given I am on the Folder "Folder activity" page logged in as admin And I click on "Edit" "button" And I click on "Add..." "button" And I should see "Content bank" in the ".fp-repo-area" "css_element" And I select "Content bank" repository in file picker And I set the field "Search repository" to "content" When I press enter Then I should see "5" elements in repository content area And I should see "systemcontent.h5p" "file" in repository content area And I should see "categorycontent1.h5p" "file" in repository content area And I should see "categorycontent2.h5p" "file" in repository content area And I should see "coursecontent1.h5p" "file" in repository content area And I should see "coursecontent2.h5p" "file" in repository content area Scenario: User can see search results when there is content that matches the search criteria ignoring case sensitivity Given I am on the Folder "Folder activity" page logged in as admin And I click on "Edit" "button" And I click on "Add..." "button" And I should see "Content bank" in the ".fp-repo-area" "css_element" And I select "Content bank" repository in file picker And I set the field "Search repository" to "COURSE" When I press enter Then I should see "2" elements in repository content area And I should see "coursecontent1.h5p" "file" in repository content area And I should see "coursecontent2.h5p" "file" in repository content area Scenario: User can not see any search results when there is not a content that matches the search criteria Given I am on the Folder "Folder activity" page logged in as admin And I click on "Edit" "button" And I click on "Add..." "button" And I should see "Content bank" in the ".fp-repo-area" "css_element" And I select "Content bank" repository in file picker And I set the field "Search repository" to "somecontent" When I press enter Then I should see "0" elements in repository content area And I should see "No files available" in the ".filepicker .fp-content" "css_element" Scenario: User can reset search criteria and see all content displayed prior the search action Given I am on the Folder "Folder activity" page logged in as admin And I click on "Edit" "button" And I click on "Add..." "button" And I should see "Content bank" in the ".fp-repo-area" "css_element" And I select "Content bank" repository in file picker And I should see "1" elements in repository content area And I should see "coursecontent1.h5p" "file" in repository content area And I set the field "Search repository" to "category" And I press enter And I should see "2" elements in repository content area And I should see "categorycontent1.h5p" "file" in repository content area And I should see "categorycontent2.h5p" "file" in repository content area When I click on "Refresh" "link" Then I should see "1" elements in repository content area And I should see "coursecontent1.h5p" "file" in repository content area Scenario: Editing teacher can see search results when the content is available to him and matches the search criteria Given I am on the Folder "Folder activity" page logged in as teacher And I click on "Edit" "button" And I click on "Add..." "button" And I should see "Content bank" in the ".fp-repo-area" "css_element" And I select "Content bank" repository in file picker And I set the field "Search repository" to "content" When I press enter Then I should see "3" elements in repository content area And I should see "coursecontent1.h5p" "file" in repository content area And I should see "categorycontent1.h5p" "file" in repository content area And I should see "systemcontent.h5p" "file" in repository content area PK*\(%% tests/behat/copy_content.featurenu[@core @core_contentbank @core_h5p @contentbank_h5p @_file_upload @javascript Feature: Copy content from the content bank In order copy content from the content bank As an admin I need to be able to copy any content from the content bank Background: Given the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | System | | contenttype_h5p | admin | content2copy.h5p | /h5p/tests/fixtures/filltheblanks.h5p | And I log in as "admin" And I am on site homepage And I turn editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present And I configure the "Navigation" block And I set the following fields to these values: | Page contexts | Display throughout the entire site | And I press "Save changes" Scenario: Admins can copy content from the content bank Given I log in as "admin" And I am on site homepage And I click on "Site pages" "list_item" in the "Navigation" "block" And I click on "Content bank" "link" And I click on "content2copy.h5p" "link" And I click on "More" "button" And I click on "Copy content" "link" in the ".cb-toolbar-container" "css_element" And I set the following fields to these values: | Content name | | And I click on "Save changes" "button" Then I should see "Empty name is not allowed" And I click on "OK" "button" in the "Error" "dialogue" And I set the following fields to these values: | Content name | Fill the blanks copy 1 | And I click on "Save changes" "button" Then I should see "Fill the blanks copy 1" And I click on "Edit" "link" And I switch to "h5p-editor-iframe" class iframe Then the field "Title" matches value "Geography" Scenario: Users without the required capability cannot copy content Given the following "users" exist: | username | firstname | lastname | email | | manager | Max | Manager | man@example.com | And the following "role assigns" exist: | user | role | contextlevel | reference | | manager | manager | System | | And the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/contentbank:copycontent | Prohibit | manager | System | | | moodle/contentbank:copyanycontent | Prohibit | manager | System | | And I log out And I log in as "manager" And I click on "Site pages" "list_item" in the "Navigation" "block" And I click on "Content bank" "link" in the "Navigation" "block" And I should see "content2copy.h5p" And I follow "content2copy.h5p" And I click on "More" "button" Then I should not see "Copy content" Scenario: Users can't copy content if they don't have the required permission Given I log in as "admin" And I am on site homepage Given I navigate to "H5P > Manage H5P content types" in site administration And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element" And I upload "h5p/tests/fixtures/ipsums.h5p" file to "H5P content type" filemanager And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element" Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | user1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | Course | C1 | contenttype_h5p | teacher1 | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p | When I log out And I log in as "teacher1" And I am on "Course 1" course homepage And I click on "Site pages" "list_item" in the "Navigation" "block" And I click on "Content bank" "link" in the "Navigation" "block" And I follow "filltheblanks.h5p" And I click on "More" "button" Then I should see "Copy content" And the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/contentbank:copycontent | Prohibit | editingteacher | System | | And I reload the page And I click on "More" "button" Then I should not see "Copy content" Scenario: Teachers can copy their own content in the content bank Given I log in as "admin" And I am on site homepage Given I navigate to "H5P > Manage H5P content types" in site administration And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element" And I upload "h5p/tests/fixtures/ipsums.h5p" file to "H5P content type" filemanager And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element" And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | Course | C1 | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p | | Course | C1 | contenttype_h5p | teacher1 | ipsums.h5p | /h5p/tests/fixtures/ipsums.h5p | When I log out And I log in as "teacher1" And I am on "Course 1" course homepage And I click on "Site pages" "list_item" in the "Navigation" "block" And I click on "Content bank" "link" in the "Navigation" "block" And I follow "ipsums.h5p" And I click on "More" "button" Then I should see "Copy content" Scenario: Teachers can't copy content created by other users in the content bank Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | Course | C1 | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p | | Course | C1 | contenttype_h5p | teacher1 | ipsums.h5p | /h5p/tests/fixtures/ipsums.h5p | When I log out And I log in as "teacher1" And I am on "Course 1" course homepage And I click on "Site pages" "list_item" in the "Navigation" "block" And I click on "Content bank" "link" in the "Navigation" "block" And I follow "filltheblanks.h5p" And I click on "More" "button" Then I should not see "Copy content" Scenario: Teachers can copy any content created by other users in the content bank if allowed Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | Course | C1 | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p | | Course | C1 | contenttype_h5p | teacher1 | ipsums.h5p | /h5p/tests/fixtures/ipsums.h5p | And the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/contentbank:copyanycontent | Allow | editingteacher | System | | When I log out And I log in as "teacher1" And I am on "Course 1" course homepage And I click on "Site pages" "list_item" in the "Navigation" "block" And I click on "Content bank" "link" And I follow "filltheblanks.h5p" And I click on "More" "button" Then I should see "Copy content" PK*\#H$tests/behat/navigate_content.featurenu[@core @core_contentbank @core_h5p @contentbank_h5p @_file_upload @javascript Feature: Navigate to different contexts in the content bank In order to navigate easily in the content bank I need to be able to view dropdown with all allowed contexts in the content bank Background: Given I log in as "admin" And the following "categories" exist: | name | category | idnumber | | Cat 1 | 0 | CAT1 | | Cat 2 | 0 | CAT2 | And the following "courses" exist: | fullname | shortname | category | | Course 0 | C0 | | | Course 1 | C1 | CAT1 | | Course 2 | C2 | CAT2 | And I navigate to "H5P > Manage H5P content types" in site administration And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element" And the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | System | | contenttype_h5p | admin | santjordi.h5p | /h5p/tests/fixtures/filltheblanks.h5p | | Category | CAT1 | contenttype_h5p | admin | santjordi_rose.h5p | /h5p/tests/fixtures/filltheblanks.h5p | | Category | CAT2 | contenttype_h5p | admin | SantJordi_book | /h5p/tests/fixtures/filltheblanks.h5p | | Course | C0 | contenttype_h5p | admin | Dragon.h5p | /h5p/tests/fixtures/filltheblanks.h5p | | Course | C1 | contenttype_h5p | admin | princess.h5p | /h5p/tests/fixtures/filltheblanks.h5p | | Course | C2 | contenttype_h5p | admin | mathsbook.h5p | /h5p/tests/fixtures/filltheblanks.h5p | Scenario: Admins can view and navigate to all the contexts in the content bank Given I am on site homepage And I turn editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present And I expand "Site pages" node When I click on "Content bank" "link" And the "Choose course or category" select box should contain "System" And the "Choose course or category" select box should contain "Cat 1" And the "Choose course or category" select box should contain "Cat 2" And the "Choose course or category" select box should contain "Course 0" And the "Choose course or category" select box should contain "Course 1" And the "Choose course or category" select box should contain "Course 2" And I should see "santjordi.h5p" And I should not see "santjordi_rose.h5p" And I should not see "Dragon.h5p" And I set the field "Choose course or category" to "Cat 1" Then I should not see "santjordi.h5p" And I should see "santjordi_rose.h5p" And I should not see "Dragon.h5p" And I set the field "Choose course or category" to "Course 0" And I should not see "santjordi.h5p" And I should not see "santjordi_rose.h5p" And I should see "Dragon.h5p" Scenario: Teachers can view and navigate to contexts in the content bank based on their permissions Given the following "users" exist: | username | firstname | lastname | | teacher | Joseba | Cilarte | And the following "course enrolments" exist: | user | course | role | | teacher | C0 | editingteacher | | teacher | C1 | editingteacher | And I log out And I am on the "C0" "Course" page logged in as "teacher" And I turn editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present And I expand "Site pages" node When I click on "Content bank" "link" And the "Choose course or category" select box should contain "Course 0" And the "Choose course or category" select box should contain "Course 1" And the "Choose course or category" select box should not contain "System" And the "Choose course or category" select box should not contain "Cat 1" And the "Choose course or category" select box should not contain "Cat 2" And the "Choose course or category" select box should not contain "Course 2" And I should see "Dragon.h5p" And I should not see "princess.h5p" And I should not see "santjordi.h5p" And I should not see "santjordi_rose.h5p" And I set the field "Choose course or category" to "Course 1" Then I should not see "Dragon.h5p" And I should see "princess.h5p" And I should not see "santjordi.h5p" And I should not see "santjordi_rose.h5p" And the following "role assigns" exist: | user | role | contextlevel | reference | | teacher | manager | Category | CAT1 | And I am on the "C0" "Course" page logged in as "teacher" And I expand "Site pages" node When I click on "Content bank" "link" And the "Choose course or category" select box should contain "Course 0" And the "Choose course or category" select box should contain "Course 1" And the "Choose course or category" select box should contain "Cat 1" And the "Choose course or category" select box should not contain "System" And the "Choose course or category" select box should not contain "Cat 2" And the "Choose course or category" select box should not contain "Course 2" And I should see "Dragon.h5p" And I set the field "Choose course or category" to "Cat 1" And I should not see "Dragon.h5p" And I should see "santjordi_rose.h5p" PK*\%-ss&tests/behat/access_permissions.featurenu[@core @core_contentbank Feature: Access permission to content Bank In order to control access to content bank As an admin I need to be able to configure users' permissions Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | user1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | Scenario: Admins access content bank Given I log in as "admin" And I am on "Course 1" course homepage Then "Content bank" "link" should exist Scenario: Editing teachers can access content bank at course level Given I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on Then "Content bank" "link" should exist Scenario: Editing teachers can't access content bank at system level Given I log in as "teacher1" Then "Content bank" "link" should not exist PK*\YϘ tests/behat/customfields.featurenu[@core @core_contentbank @core_h5p @contentbank_h5p @_file_upload @javascript Feature: Content bank custom fields In order to add/edit custom fields for content As a user I need to be able to access the custom fields Background: Given the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | private_files | System | 1 | my-index | side-post | And the following "custom field categories" exist: | name | component | area | itemid | | Category for test | core_contentbank | content | 0 | And the following "custom fields" exist: | name | category | type | shortname | | Test field | Category for test | text | testfield | And the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | System | | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p | And I log in as "admin" And I am on site homepage And I turn editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present And I configure the "Navigation" block And I set the following fields to these values: | Page contexts | Display throughout the entire site | And I press "Save changes" Scenario: Users can edit customfields Given I click on "Site pages" "list_item" in the "Navigation" "block" And I click on "Content bank" "link" in the "Navigation" "block" And I follow "filltheblanks.h5p" And I click on "Edit" "link" And I set the following fields to these values: | Test field | My test value | When I click on "Save" "button" Then I should see "Test field: My test value" PK*\ ;;;"tests/behat/delete_content.featurenu[@core @core_contentbank @core_h5p @contentbank_h5p @javascript Feature: Delete H5P file from the content bank In order remove H5P content from the content bank As an admin I need to be able to delete any H5P content from the content bank Background: Given the following "user private file" exists: | user | admin | | filepath | h5p/tests/fixtures/filltheblanks.h5p | And I am on the "Content bank" page logged in as "admin" And I click on "Upload" "link" And I click on "Choose a file..." "button" And I click on "Private files" "link" in the ".fp-repo-area" "css_element" And I click on "filltheblanks.h5p" "link" And I set the field "Save as" to "content2delete.h5p" And I click on "Select this file" "button" And I click on "Save changes" "button" Scenario: Admins can delete content from the content bank Given I wait "2" seconds And I click on "More" "button" And I should see "Delete" And I click on "Delete" "link" And I should see "Are you sure you want to delete the content 'content2delete.h5p'" And I should not see "The content will only be deleted from the content bank" And I click on "Cancel" "button" in the "Delete content" "dialogue" Then I should see "content2delete.h5p" And I wait "2" seconds And I click on "More" "button" And I click on "Delete" "link" in the ".cb-toolbar-container" "css_element" And I click on "Delete" "button" in the "Delete content" "dialogue" And I wait until the page is ready And I should see "Content deleted." And I should not see "content2delete.h5p" Scenario: Users without the required capability can only delete their own content Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/contentbank:deleteanycontent | Prohibit | manager | System | | And the following "users" exist: | username | firstname | lastname | email | | manager | Max | Manager | man@example.com | And the following "role assigns" exist: | user | role | contextlevel | reference | | manager | manager | System | | And the following "user private file" exists: | user | manager | | filepath | h5p/tests/fixtures/find-the-words.h5p | When I am on the "Content bank" page logged in as "manager" And I should see "content2delete.h5p" And I follow "content2delete.h5p" And I wait "2" seconds And I click on "More" "button" Then I should not see "Delete" And I am on the "Content bank" page And I click on "Upload" "link" And I click on "Choose a file..." "button" And I click on "Private files" "link" in the ".fp-repo-area" "css_element" And I click on "find-the-words.h5p" "link" And I click on "Select this file" "button" And I click on "Save changes" "button" And I wait "2" seconds And I click on "More" "button" And I should see "Delete" Scenario: The number of times a content is used is displayed before removing it Given I am on the "My private files" page And I click on "Add..." "button" And I select "Content bank" repository in file picker And I click on "content2delete.h5p" "file" in repository content area And I click on "Link to the file" "radio" And I click on "Select this file" "button" And I click on "Save changes" "button" And I am on the "Content bank" page And I follow "content2delete.h5p" And I wait "2" seconds And I click on "More" "button" And I click on "Delete" "link" in the ".cb-toolbar-container" "css_element" Then I should see "Are you sure you want to delete the content 'content2delete.h5p'" And I should see "The content will only be deleted from the content bank" And I click on "Delete" "button" in the "Delete content" "dialogue" And I should see "Content deleted." And I should not see "content2delete.h5p" PK*\v##tests/behat/visibility.featurenu[@core @core_contentbank @core_h5p @contentbank_h5p @_file_upload @javascript Feature: Make content public or unlisted In order to make content public or unlisted As a user I need to be able to access the edition options Background: Given I log in as "admin" And I am on site homepage And I turn editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present And I configure the "Navigation" block And I set the following fields to these values: | Page contexts | Display throughout the entire site | And I press "Save changes" And I navigate to "H5P > Manage H5P content types" in site administration And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element" Scenario: Users can make their content public or unlisted Given the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | visibility | | System | | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p | 1 | And I click on "Site pages" "list_item" in the "Navigation" "block" And I click on "Content bank" "link" in the "Navigation" "block" And I click on "filltheblanks.h5p" "link" And I wait until the page is ready And "filltheblanks.h5p (Unlisted)" "heading" should not exist And I click on "More" "button" And I should see "Make unlisted" And I click on "Make unlisted" "link" And I wait until the page is ready Then "filltheblanks.h5p (Unlisted)" "heading" should exist And I click on "More" "button" And I should see "Make public" Scenario: Unlisted content can be hidden by user preference Given the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | visibility | | System | | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p | 1 | | System | | contenttype_h5p | admin | find-the-words.h5p | /h5p/tests/fixtures/find-the-words.h5p | 2 | And I am on the "Content bank" page logged in as "admin" And I should see "filltheblanks.h5p" And I should see "find-the-words.h5p (Unlisted)" When I set the field "Show unlisted content" to "0" And I wait until the page is ready Then I should see "filltheblanks.h5p" And I should not see "find-the-words.h5p (Unlisted)" And I set the field "Show unlisted content" to "1" And I wait until the page is ready And I should see "filltheblanks.h5p" And I should see "find-the-words.h5p (Unlisted)" Scenario: Unlisted content cannot be seen by other users Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | teacher2 | Teacher | 2 | teacher2@example.com | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher2 | C1 | editingteacher | And the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | visibility | | Course | C1 | contenttype_h5p | teacher1 | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p | 2 | And I log out And I log in as "teacher1" And I am on "Course 1" course homepage And I click on "Site pages" "list_item" in the "Navigation" "block" And I click on "Content bank" "link" in the "Navigation" "block" Then I should see "filltheblanks.h5p (Unlisted)" And I log out And I log in as "teacher2" And I am on "Course 1" course homepage And I click on "Site pages" "list_item" in the "Navigation" "block" And I click on "Content bank" "link" in the "Navigation" "block" Then I should not see "filltheblanks.h5p" Scenario: Unlisted content is not found through search by other users Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | teacher2 | Teacher | 2 | teacher2@example.com | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher2 | C1 | editingteacher | And the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | visibility | | Course | C1 | contenttype_h5p | teacher1 | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p | 2 | And I log out And I log in as "teacher1" And I am on "Course 1" course homepage And I click on "Site pages" "list_item" in the "Navigation" "block" And I click on "Content bank" "link" in the "Navigation" "block" And I set the field "Search" to "filltheblanks.h5p" And I should see "filltheblanks.h5p" And I log out And I log in as "teacher2" And I am on "Course 1" course homepage And I click on "Site pages" "list_item" in the "Navigation" "block" And I click on "Content bank" "link" in the "Navigation" "block" When I set the field "Search" to "filltheblanks.h5p" Then I should not see "filltheblanks.h5p" Scenario: Managers can see other users' unlisted content Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | manager1 | Manager | 1 | manager1@example.com | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | manager1 | C1 | manager | And the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | visibility | | Course | C1 | contenttype_h5p | teacher1 | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p | 2 | And I log out And I log in as "manager1" And I am on "Course 1" course homepage And I click on "Site pages" "list_item" in the "Navigation" "block" And I click on "Content bank" "link" in the "Navigation" "block" And I should see "filltheblanks.h5p (Unlisted)" And I set the field "Search" to "filltheblanks.h5p" And I should see "filltheblanks.h5p (Unlisted)" @_file_upload Scenario: Default content visibility can be set to unlisted Given the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And I set the following administration settings values: | Default content visibility | 2 | And I am on "Course 1" course homepage And I click on "Site pages" "list_item" in the "Navigation" "block" And I click on "Content bank" "link" in the "Navigation" "block" And I click on "Upload" "link" And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "Upload content" filemanager And I click on "Save changes" "button" Then "filltheblanks.h5p (Unlisted)" "heading" should exist @_file_upload Scenario: User preference concerning content visibility overrides site-wide default content visibility Given the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And I set the following administration settings values: | Default content visibility | 2 | And the following "user preferences" exist: | user | preference | value | | admin | core_contentbank_visibility | 1 | And I am on "Course 1" course homepage And I click on "Site pages" "list_item" in the "Navigation" "block" And I click on "Content bank" "link" in the "Navigation" "block" And I click on "Upload" "link" And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "Upload content" filemanager And I click on "Save changes" "button" Then "filltheblanks.h5p" "heading" should exist And "filltheblanks.h5p (Unlisted)" "heading" should not exist PK/\iidb/install.phpnu[. /** * Installation file for the content bank repository * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Create a default instance of the content bank repository * * @return bool A status indicating success or failure */ function xmldb_repository_contentbank_install() { global $CFG; $result = true; require_once($CFG->dirroot.'/repository/lib.php'); $userplugin = new repository_type('contentbank', [], true); if (!$id = $userplugin->create(true)) { $result = false; } return $result; } PK/\> db/access.phpnu[. /** * Plugin capabilities. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @author Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = [ 'repository/contentbank:view' => [ 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'coursecreator' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ] ], 'repository/contentbank:accesscoursecontent' => [ 'captype' => 'read', 'contextlevel' => CONTEXT_COURSE, 'archetypes' => [ 'coursecreator' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ] ], 'repository/contentbank:accesscoursecategorycontent' => [ 'captype' => 'read', 'contextlevel' => CONTEXT_COURSECAT, 'archetypes' => [ 'coursecreator' => CAP_ALLOW, 'manager' => CAP_ALLOW ] ], 'repository/contentbank:accessgeneralcontent' => [ 'captype' => 'read', 'contextlevel' => CONTEXT_COURSECAT, 'archetypes' => [ 'user' => CAP_ALLOW ] ] ]; PK/\Ǚ[TT"lang/en/repository_contentbank.phpnu[. /** * Strings for component 'repository_contentbank', language 'en'. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @author Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['configplugin'] = 'Configuration for content bank repository'; $string['pluginname_help'] = 'Files in content bank'; $string['pluginname'] = 'Content bank'; $string['emptyfilelist'] = 'There are no files to show'; $string['contentbank:view'] = 'View content bank repository'; $string['contentbank:accesscoursecontent'] = 'Access course content bank files'; $string['contentbank:accesscoursecategorycontent'] = 'Access course category content bank files'; $string['contentbank:accessgeneralcontent'] = 'Access system content bank files'; $string['privacy:metadata'] = 'The Content bank repository plugin does not store or transmit any personal data.'; PK/\2% pix/icon.svgnu[ PK/\$ pix/icon.pngnu[PNG  IHDR^`>\sRGB, pHYs9]9]C! {IDATx \U;2*BH K - tw [B(AE$,ʠ1#K$ԭNqT@epAQD1l# ,A!p;NѦutCuys.紴B!B!B!B!B!B!BMƕJ2rrC! m,D#ז_#/#@] !y$HsW3cq1_B܍Q7K=B(h[PǏgWC#pkלz~τbHo:Ü W ?DYu |;2=͜qkLr*ri5"0)EJHNdX7tjҶwKBP(9:Eސiᾂ$#! i"$+2>oCXKK3Z .V۰pe&s[rӅGa,{}<@!c"LCMrtB\(`,Ds%H!D9ts>~UKXWXH!DӦM 3@"K˃ta̕&GQ7˜8uHLMbfv$8JkkƘ.?pc9U] !޾1biY%![8~ytB1g |9mMܠ,.?3"ۜ3f)ASȏ}ֵ"gc۟#8 i}7>@y~>dIkLKB%0pI} `p̂2%e[46AO: !h+!$Z"_C^ ޘE+Qb߯cѱt!n "/H Vh !$$'O~OX<e4瑙} 0x*1m 2ABP(^S 6FI!BH /ryޘJ}U,lZ_{UZn[.\27sT%!dC[ޕ-2n<YiQ0Jls-yq|>_FZL}P(/ݿ! ̃K ;;!D ʇ},g!I=)+-N6H5BRY+ܒYqsu;$>ko y}'?I;BR;3QD2~HH*0ޚ]x1v`3򹮵uc~IH"ikkAvfw(0|t%$1ԅk}Ł8rt%D5c1 Ԍ\ w Qn҃j~L rܦ0Y-=pD2 dL6mrc2jЧIq))7Uzȅ5߻V.߯dƚ w;׿nu/+JOs{2ͦI_B!S,hXy߲Io7Nd/ݵJ9@@3 utR.Ê,҃IOJ`f!~l{IJ#r%<~;l3擺N麦<>.#5{獝15^-9a0;?cRI.[?:Ee|qmٸIJ YH8i ccڲͦawiw%L%P߹ers%,(=fV$H9 ½M\lb.T*$90ÝNUM0]vA斫jfiߕD; 掎Iz ya|2=>+ZY3AV 1r||S`4K3_ATSxlS`{<:3҃aȜ=7F9>/<]xmsUa1x(eю|=Y瀄Ҝ{zDvG0GM 9àɤ–MJK9ӕ)FCtttl ~Y'=0V39g`z/--Eì/ޛ]<fFt.d`)k{o6q꡷<*/f8?-1k H|Ǔq4kp\lL{gY͎B`l'>SnxI/j谷1xYt⃚QGbSDW5t2/4&:~]_;nq/ P*SZ7#UD##2 aN6vwLrОӷp`1iOq@'ǟ_,i0aIo7# K 3t?-X[h Ә> g#yQ\ -fubp3\`.z͢8 g{N=^?3kTlOl7rvaq90T5|1ɵ{2vąČw`FZ{Bp''+.Xt3ԝxEՎn6\~p9>{/b*^-tQ07F($[^X21eb݂}ۃ9{ yf%0tdӧo.]рZ#>bKzGYL]vz sf- vUvc~~{,l .>b`Ӄܽ:v+H^><;Z?=Xzn\5WnD/CtW[raZ;H{3C74[SH7Tc^[oӷ V-*)ߞug >cb jЕ-](1GbI%tNǬPEhY{1GK7x"u==[kmkk5UE4 7eVyxBtmkuA}WcZJO;)tQJҺv ) e9ߐ jrjk5ԁuAΩUφ. rÌw)I!]cԪE⵵-?o{'k-lWo~;EOe e3!5fjn"eSڋY"tQ U >mN+(]cٖxMi,S gYS$}d. ̢1ҵ:xOkl|Hs]@6K%]_'^ck񮑮Mjs[kc dAi85EJ&IsD 5- 3?*u[vZMxt}m*DbxqjI'kkl`. 7-J/v;AśȧU?$|ƛ셎B׬D6 X>ϛpV6{Nƶ9[6Uklm5/^ԲHHH9{ks5d;|m%9R[,${ƛtz+Ip 1xJ^3@ wa†v9x rPzxBvf7nS@ '+uXn (^Pzx[[,?f-DxBmy m!ūW/oKFMn%~Ɛ ūԋsj3Ŭ#v(^PzIx!}cchXxB%-7+|>K m+^(^^f{fś;(^Pzxe͊T*$^(^PzIx!ӋC7qkRzxBJW/chXxB-٬xmHK ^B&.AKŋ0;.A Z1KlPzxzB]a QCūW//DzzHCūW//dzrHCūW//DzDȻ,AK[,w)DKK[(2!O5CūW/B}9|&B w7!f/XnXxBm~;xYnXxBmvF lY ūOB>6$V(^PzxP] C zjC @.ۮ#v(^PzxjB,^(^P(J&;B[@ ;pD"PzxBzv6^(^Pr=kq;A Ż"8ÈA W/^(!0{>^xƗM(!(m`{@m ūW/ׇ@W/^( ~eQšxBlQšxBRt`]"PzxByXb="PzxBپ(Ω (^Pzx3 u_6x<5 ֍#yaxBmGE ūI ¾ kE ū7Cȓ0^(^P!tO4[EPzxB~k͑0^(^P!)"QA(^PzxSūW/oʡxBM9^(^P) ū7PzxBW/^(ޔC śr(^PzxSūW/oʡxBM9^(^P) ū7PzxBW/^(ޔC śr(^PzxSūW/oʡxBM9^(^P) ū7PzxBW/^(ޔC śr(^PzxSūW/oʡxBM9^(^P) ū7PzxBW/^(ޔC śr(^PzxSūW/oʡxBM9^(^P) ū7PzxBW/^(ޔC śr(^PzxSūW/oʡxBM9^(^P) ū7PzxBW/^(ޔC śrz| ڤV.vVgH&xJV sK!B!B!B!B!B!B!B!B!B!B哏&IENDB`PK/\%  version.phpnu[. /** * Version details * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @author Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'repository_contentbank'; // Full name of the plugin (used for diagnostics). PK/\ 6classes/browser/contentbank_browser_context_course.phpnu[. /** * Utility class for browsing of content bank files in the course context. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_contentbank\browser; /** * Represents the content bank browser in the course context. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class contentbank_browser_context_course extends contentbank_browser { /** * Constructor. * * @param \context_course $context The current context */ public function __construct(\context_course $context) { $this->context = $context; } /** * Define the allowed child context levels. * * @return int[] The array containing the relevant child context levels */ protected function allowed_child_context_levels(): array { // The course context is the last relevant context level, therefore child context levels are not being returned. return []; } /** * The required condition to enable the user to view/access the content bank content in this context. * * @return bool Whether the user can view/access the content bank content in the context */ public function can_access_content(): bool { // When the following conditions are met, the user would be able to share the content created in the course // context level all over the site. // The content from the course context level should be available to: // * Every user which has capability to access the content of a given course. return has_capability('repository/contentbank:accesscoursecontent', $this->context); } } PK/\`8v* * 9classes/browser/contentbank_browser_context_coursecat.phpnu[. /** * Utility class for browsing of content bank files in the course category context. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_contentbank\browser; /** * Represents the content bank browser in the course category context. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class contentbank_browser_context_coursecat extends contentbank_browser { /** * Constructor. * * @param \context_coursecat $context The current context */ public function __construct(\context_coursecat $context) { $this->context = $context; } /** * Define the allowed child context levels. * * @return int[] The array containing the relevant child context levels */ protected function allowed_child_context_levels(): array { // The expected child contexts in the course category context level are the course category context // (ex. subcategories) and the course context. return [\CONTEXT_COURSECAT, \CONTEXT_COURSE]; } /** * The required condition to enable the user to view/access the content bank content in this context. * * @return bool Whether the user can view/access the content bank content in the context */ public function can_access_content(): bool { // When the following conditions are met, the user would be able to share the content created in the course // category context level all over the site. // The content from the course category context level should be available to either: // * Every user which has a capability to access the 'general' content and has capability to access the // content of any child course of the given course category. // * Users that have capability to access content at a course category context level. if (has_capability('repository/contentbank:accesscoursecategorycontent', $this->context)) { return true; } $canaccesschildcontent = false; foreach ($this->get_child_contexts() as $childcontext) { $browser = \repository_contentbank\helper::get_contentbank_browser($childcontext); if ($canaccesschildcontent = $browser->can_access_content()) { break; } } return $canaccesschildcontent && has_capability('repository/contentbank:accessgeneralcontent', $this->context); } } PK/\P'classes/browser/contentbank_browser.phpnu[. /** * Utility class for browsing of content bank files. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_contentbank\browser; /** * Base class for the content bank browsers. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class contentbank_browser { /** @var \context The current context. */ protected $context; /** * Get all content nodes in the current context which can be viewed/accessed by the user. * * @return array[] The array containing all nodes which can be viewed/accessed by the user in the current context */ public function get_content(): array { return array_merge($this->get_context_folders(), $this->get_contentbank_content()); } /** * Generate the full navigation to the current node. * * @return array[] The array containing the path to each node in the navigation. * Each navigation node is an array with keys: name, path. */ public function get_navigation(): array { // Get the current navigation node. $currentnavigationnode = \repository_contentbank\helper::create_navigation_node($this->context); $navigationnodes = [$currentnavigationnode]; // Get the parent content bank browser. $parent = $this->get_parent(); // Prepend parent navigation node in the navigation nodes array until there is no existing parent. while ($parent !== null) { $parentnavigationnode = \repository_contentbank\helper::create_navigation_node($parent->context); array_unshift($navigationnodes, $parentnavigationnode); $parent = $parent->get_parent(); } return $navigationnodes; } /** * The required condition to enable the user to view/access the content bank content in this context. * * @return bool Whether the user can view/access the content bank content in the context */ abstract public function can_access_content(): bool; /** * Define the allowed child context levels. * * @return int[] The array containing the relevant child context levels */ abstract protected function allowed_child_context_levels(): array; /** * Get the relevant child contexts. * * @return \context[] The array containing the relevant, next-level children contexts */ protected function get_child_contexts(): array { global $DB; if (empty($allowedcontextlevels = $this->allowed_child_context_levels())) { // Early return if there aren't any defined child context levels. return []; } list($contextlevelsql, $params) = $DB->get_in_or_equal($allowedcontextlevels, SQL_PARAMS_NAMED); $pathsql = $DB->sql_like('path', ':path', false, false); $select = "contextlevel {$contextlevelsql} AND {$pathsql} AND depth = :depth"; $params['path'] = "{$this->context->path}/%"; $params['depth'] = $this->context->depth + 1; $childcontexts = $DB->get_records_select('context', $select, $params); return array_map(function($childcontext) { return \context::instance_by_id($childcontext->id); }, $childcontexts); } /** * Get the content bank browser class of the parent context. Currently used to generate the navigation path. * * @return contentbank_browser|null The content bank browser of the parent context */ private function get_parent(): ?self { if ($parentcontext = $this->context->get_parent_context()) { return \repository_contentbank\helper::get_contentbank_browser($parentcontext); } return null; } /** * Generate folder nodes for the relevant child contexts which can be accessed/viewed by the user. * * @return array[] The array containing the context folder nodes where each folder node is an array with keys: * title, datemodified, datecreated, path, thumbnail, children. */ private function get_context_folders(): array { // Get all relevant child contexts. $children = $this->get_child_contexts(); // Return all child context folder nodes which can be accessed by the user following the defined conditions // in can_access_content(). return array_reduce($children, function ($list, $child) { $browser = \repository_contentbank\helper::get_contentbank_browser($child); if ($browser->can_access_content()) { $name = $child->get_context_name(false); $path = base64_encode(json_encode(['contextid' => $child->id])); $list[] = \repository_contentbank\helper::create_context_folder_node($name, $path); } return $list; }, []); } /** * Generate nodes for the content bank content in the current context which can be accessed/viewed by the user. * * @return array[] The array containing the content nodes where each content node is an array with keys: * shorttitle, title, datemodified, datecreated, author, license, isref, source, icon, thumbnail. */ private function get_contentbank_content(): array { $cb = new \core_contentbank\contentbank(); // Get all content bank files in the current context. $contents = $cb->search_contents(null, $this->context->id); // Return all content bank content nodes from the current context which can be accessed by the user following // the defined conditions in can_access_content(). return array_reduce($contents, function($list, $content) { if ($this->can_access_content() && $contentnode = \repository_contentbank\helper::create_contentbank_content_node($content)) { $list[] = $contentnode; } return $list; }, []); } } PK/\^> 6classes/browser/contentbank_browser_context_system.phpnu[. /** * Utility class for browsing of content bank files in the system context. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_contentbank\browser; /** * Represents the content bank browser in the system context. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class contentbank_browser_context_system extends contentbank_browser { /** * Constructor. * * @param \context_system $context The current context */ public function __construct(\context_system $context) { $this->context = $context; } /** * Define the allowed child context levels. * * @return int[] The array containing the relevant child context levels */ protected function allowed_child_context_levels(): array { // The expected child context in the system context level is the course category context. return [\CONTEXT_COURSECAT]; } /** * The required condition to enable the user to view/access the content bank content in this context. * * @return bool Whether the user can view/access the content bank content in the context */ public function can_access_content(): bool { // When the following conditions are met, the user would be able to share the content created in the system // context level all over the site. // The content from the system context level should be available to: // * Every user that has a capability to access the 'general' content. return has_capability('repository/contentbank:accessgeneralcontent', $this->context); } } PK/\oQ؃ classes/contentbank_search.phpnu[. /** * Utility class for searching of content bank files. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_contentbank; /** * Represents the content bank search related functionality. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class contentbank_search { /** * Generate and return content nodes for all content bank files that match the search criteria * and can be viewed/accessed by the user. * * @param string $search The search string * @return array[] The array containing all content file nodes that match the search criteria. Each content node is * an array with keys: shorttitle, title, datemodified, datecreated, author, license, isref, source, * icon, thumbnail. */ public static function get_search_contents(string $search): array { $contentbank = new \core_contentbank\contentbank(); // Return all content bank content that matches the search criteria and can be viewed/accessed by the user. $contents = $contentbank->search_contents($search); return array_reduce($contents, function($list, $content) { $contentcontext = \context::instance_by_id($content->get_content()->contextid); $browser = \repository_contentbank\helper::get_contentbank_browser($contentcontext); // If the user can access the content and content node can be created, add the node into the // search results list. if ($browser->can_access_content() && $contentnode = \repository_contentbank\helper::create_contentbank_content_node($content)) { $list[] = $contentnode; } return $list; }, []); } } PK/\ eetests/browser_test.phpnu[. namespace repository_contentbank; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once("$CFG->dirroot/repository/lib.php"); /** * Tests for the content bank browser class. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class browser_test extends \advanced_testcase { /** * Test get_content() in the system context with users that have capability to access/view content bank content * within the system context. By default, every authenticated user should be able to access/view the content in * the system context. */ public function test_get_content_system_context_user_has_capabilities(): void { global $DB, $CFG; $this->resetAfterTest(true); $systemcontext = \context_system::instance(); // Create a course category $coursecategory. $coursecategory = $this->getDataGenerator()->create_category(['name' => 'Category']); $coursecatcontext = \context_coursecat::instance($coursecategory->id); // Get the default course category. $defaultcat = \core_course_category::get(1); $defaultcatcontext = \context_coursecat::instance($defaultcat->id); // Create course. $course = $this->getDataGenerator()->create_course(['category' => $coursecategory->id]); $admin = get_admin(); // Create a user (not enrolled in a course). $user = $this->getDataGenerator()->create_user(); // Add some content to the content bank. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); // Add some content bank files in the system context. $filepath = $CFG->dirroot . '/h5p/tests/fixtures/filltheblanks.h5p'; $contentbankcontents = $generator->generate_contentbank_data('contenttype_h5p', 3, $admin->id, $systemcontext, true, $filepath); // Log in as admin. $this->setUser($admin); // Get the content bank nodes displayed to the admin in the system context. $browser = new \repository_contentbank\browser\contentbank_browser_context_system($systemcontext); $repositorycontentnodes = $browser->get_content(); // All content nodes should be available to the admin user. // There should be a total of 5 nodes, 3 file nodes representing the existing content bank files in the // system context and 2 folder nodes representing the default course category and 'Category'. $this->assertCount(5, $repositorycontentnodes); $contextfolders = [ [ 'name' => get_string('defaultcategoryname'), 'contextid' => $defaultcatcontext->id ], [ 'name' => 'Category', 'contextid' => $coursecatcontext->id ] ]; $expected = $this->generate_expected_content($contextfolders, $contentbankcontents); $this->assertEqualsCanonicalizing($expected, $repositorycontentnodes); // Log in as a user. $this->setUser($user); // Get the content bank nodes displayed to an authenticated user in the system context. $browser = new \repository_contentbank\browser\contentbank_browser_context_system($systemcontext); $repositorycontentnodes = $browser->get_content(); // There should be 3 nodes representing the existing content bank files in the system context. // The course category context folder node should be ignored as the user does not have an access to // the content of the category's courses. $this->assertCount(3, $repositorycontentnodes); $expected = $this->generate_expected_content([], $contentbankcontents); $this->assertEqualsCanonicalizing($expected, $repositorycontentnodes); // Enrol the user as an editing teacher in the course. $editingteacherrole = $DB->get_field('role', 'id', ['shortname' => 'editingteacher']); $this->getDataGenerator()->enrol_user($user->id, $course->id, $editingteacherrole); // Get the content bank nodes displayed to the editing teacher in the system context. $browser = new \repository_contentbank\browser\contentbank_browser_context_system($systemcontext); $repositorycontentnodes = $browser->get_content(); // All content nodes should now be available to the editing teacher. // There should be a total of 4 nodes, 3 file nodes representing the existing content bank files in the // system context and 1 folder node representing the course category 'Category' (The editing teacher is now // enrolled in a course from the category). $this->assertCount(4, $repositorycontentnodes); $contextfolders = [ [ 'name' => 'Category', 'contextid' => $coursecatcontext->id ] ]; $expected = $this->generate_expected_content($contextfolders, $contentbankcontents); $this->assertEqualsCanonicalizing($expected, $repositorycontentnodes); } /** * Test get_content() in the system context with users that do not have a capability to access/view content bank * content within the system context. By default, every non-authenticated user should not be able to access/view * the content in the system context. */ public function test_get_content_system_context_user_missing_capabilities(): void { $this->resetAfterTest(true); $systemcontext = \context_system::instance(); $admin = get_admin(); // Add some content to the content bank. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); // Add some content bank files in the system context. $generator->generate_contentbank_data('contenttype_h5p', 3, $admin->id, $systemcontext, true); // Log out. $this->setUser(); // Get the content bank nodes displayed to a non-authenticated user in the system context. $browser = new \repository_contentbank\browser\contentbank_browser_context_system($systemcontext); $repositorycontents = $browser->get_content(); // Content nodes should not be available to the non-authenticated user in the system context. $this->assertCount(0, $repositorycontents); } /** * Test get_content() in the course category context with users that have capability to access/view content * bank content within the course category context. By default, every authenticated user that has access to * any category course should be able to access/view the content in the course category context. */ public function test_get_content_course_category_context_user_has_capabilities(): void { global $CFG; $this->resetAfterTest(true); // Create a course category. $category = $this->getDataGenerator()->create_category(['name' => 'Category']); $coursecatcontext = \context_coursecat::instance($category->id); // Create course1. $course1 = $this->getDataGenerator()->create_course(['fullname' => 'Course1', 'category' => $category->id]); $course1context = \context_course::instance($course1->id); // Create course2. $course2 = $this->getDataGenerator()->create_course(['fullname' => 'Course2', 'category' => $category->id]); $course2context = \context_course::instance($course2->id); $admin = get_admin(); // Create editing teacher enrolled in course1. $editingteacher = $this->getDataGenerator()->create_and_enrol($course1, 'editingteacher'); // Add some content to the content bank. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); // Add some content bank files in the course category context. $filepath = $CFG->dirroot . '/h5p/tests/fixtures/filltheblanks.h5p'; $contentbankcontents = $generator->generate_contentbank_data('contenttype_h5p', 3, $admin->id, $coursecatcontext, true, $filepath); $this->setUser($admin); // Get the content bank nodes displayed to the admin in the course category context. $browser = new \repository_contentbank\browser\contentbank_browser_context_coursecat($coursecatcontext); $repositorycontents = $browser->get_content(); // All content nodes should be available to the admin user. // There should be a total of 5 nodes, 3 file nodes representing the existing content bank files in the // course category context and 2 folder nodes representing the courses 'Course1' and 'Course2'. $this->assertCount(5, $repositorycontents); $contextfolders = [ [ 'name' => 'Course1', 'contextid' => $course1context->id ], [ 'name' => 'Course2', 'contextid' => $course2context->id ] ]; $expected = $this->generate_expected_content($contextfolders, $contentbankcontents); $this->assertEqualsCanonicalizing($expected, $repositorycontents); // Log in as an editing teacher enrolled in a child course. $this->setUser($editingteacher); // Get the content bank nodes displayed to the editing teacher in the course category context. $browser = new \repository_contentbank\browser\contentbank_browser_context_coursecat($coursecatcontext); $repositorycontents = $browser->get_content(); // There should be a total of 4 nodes, 3 file nodes representing the existing content bank files in the // course category context and 1 folder node representing the course 'Course1' (The editing teacher is only // enrolled in course1). $this->assertCount(4, $repositorycontents); $contextfolders = [ [ 'name' => 'Course1', 'contextid' => $course1context->id ] ]; $expected = $this->generate_expected_content($contextfolders, $contentbankcontents); $this->assertEqualsCanonicalizing($expected, $repositorycontents); } /** * Test get_content() in the course category context with users that do not have capability to access/view content * bank content within the course category context. By default, every non-authenticated user or authenticated users * that cannot access/view course content from the course category should not be able to access/view the * content in the course category context. */ public function test_get_content_course_category_context_user_missing_capabilities(): void { $this->resetAfterTest(true); // Create a course category 'Category'. $category = $this->getDataGenerator()->create_category(['name' => 'Category']); // Create course1 in 'Category'. $course1 = $this->getDataGenerator()->create_course(['fullname' => 'Course1', 'category' => $category->id]); // Create course2 in default category by default. $course2 = $this->getDataGenerator()->create_course(['fullname' => 'Course2']); // Create a teacher enrolled in course1. $teacher = $this->getDataGenerator()->create_and_enrol($course1, 'teacher'); // Create an editing teacher enrolled in course2. $editingteacher = $this->getDataGenerator()->create_and_enrol($course2, 'editingteacher'); $admin = get_admin(); // Add some content to the content bank. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); // Add some content bank files in the 'Category' context. $coursecatcontext = \context_coursecat::instance($category->id); $generator->generate_contentbank_data('contenttype_h5p', 3, $admin->id, $coursecatcontext, true); // Log in as a non-editing teacher. $this->setUser($teacher); // Get the content bank nodes displayed to a non-editing teacher in the 'Category' context. $browser = new \repository_contentbank\browser\contentbank_browser_context_coursecat($coursecatcontext); $repositorycontents = $browser->get_content(); // Content nodes should not be available to a non-editing teacher in the 'Category' context. $this->assertCount(0, $repositorycontents); // Log in as an editing teacher. $this->setUser($editingteacher); // Get the content bank nodes displayed to a an editing teacher in the 'Category' context. $browser = new \repository_contentbank\browser\contentbank_browser_context_coursecat($coursecatcontext); $repositorycontents = $browser->get_content(); // Content nodes should not be available to an editing teacher in the 'Category' context. $this->assertCount(0, $repositorycontents); // Log out. $this->setUser(); // Get the content bank nodes displayed to a non-authenticated user in the course category context. $browser = new \repository_contentbank\browser\contentbank_browser_context_coursecat($coursecatcontext); $repositorycontents = $browser->get_content(); // Content nodes should not be available to the non-authenticated user in the course category context. $this->assertCount(0, $repositorycontents); } /** * Test get_content() in the course context with users that have capability to access/view content * bank content within the course context. By default, admin, managers, course creators, editing teachers enrolled * in the course should be able to access/view the content. */ public function test_get_content_course_context_user_has_capabilities(): void { global $CFG; $this->resetAfterTest(true); // Create course1. $course = $this->getDataGenerator()->create_course(['fullname' => 'Course']); $coursecontext = \context_course::instance($course->id); $admin = get_admin(); // Create editing teacher enrolled in course. $editingteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); // Add some content to the content bank. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); // Add some content bank files in the course context. $filepath = $CFG->dirroot . '/h5p/tests/fixtures/filltheblanks.h5p'; $contentbankcontents = $generator->generate_contentbank_data('contenttype_h5p', 3, $admin->id, $coursecontext, true, $filepath); $this->setUser($admin); // Get the content bank nodes displayed to the admin in the course context. $browser = new \repository_contentbank\browser\contentbank_browser_context_course($coursecontext); $repositorycontents = $browser->get_content(); // All content nodes should be available to the admin user. // There should be 3 file nodes representing the existing content bank files in the // course context. $this->assertCount(3, $repositorycontents); $expected = $this->generate_expected_content([], $contentbankcontents); $this->assertEqualsCanonicalizing($expected, $repositorycontents); // Log in as an editing teacher. $this->setUser($editingteacher); // All content nodes should also be available to the editing teacher. // Get the content bank nodes displayed to the editing teacher in the course context. $browser = new \repository_contentbank\browser\contentbank_browser_context_course($coursecontext); $repositorycontents = $browser->get_content(); // There should be 3 file nodes representing the existing content bank files in the // course context. $this->assertCount(3, $repositorycontents); $expected = $this->generate_expected_content([], $contentbankcontents); $this->assertEqualsCanonicalizing($expected, $repositorycontents); } /** * Test get_content() in the course context with users that do not have capability to access/view content * bank content within the course context. By default, every user which is not an admin, manager, course creator, * editing teacher enrolled in the course should not be able to access/view the content. */ public function test_get_content_course_context_user_missing_capabilities(): void { $this->resetAfterTest(true); // Create course1. $course1 = $this->getDataGenerator()->create_course(['fullname' => 'Course1']); $course1context = \context_course::instance($course1->id); // Create course2. $course2 = $this->getDataGenerator()->create_course(['fullname' => 'Course2']); $course2context = \context_course::instance($course2->id); $admin = get_admin(); // Create non-editing teacher enrolled in course1. $teacher = $this->getDataGenerator()->create_and_enrol($course1, 'teacher'); // Create editing teacher enrolled in course1. $editingteacher = $this->getDataGenerator()->create_and_enrol($course1, 'editingteacher'); // Add some content to the content bank. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); // Add some content bank files in the course1 context. $generator->generate_contentbank_data('contenttype_h5p', 2, $admin->id, $course1context, true); // Add some content bank files in the course2 context. $generator->generate_contentbank_data('contenttype_h5p', 3, $admin->id, $course2context, true); // Log in as a non-editing teacher. $this->setUser($teacher); // Get the content bank nodes displayed to the non-editing teacher in the course1 context. $browser = new \repository_contentbank\browser\contentbank_browser_context_course($course1context); $repositorycontents = $browser->get_content(); // Content nodes should not be available to the teacher in the course1 context. $this->assertCount(0, $repositorycontents); // Log in as editing teacher. $this->setUser($editingteacher); // Get the content bank nodes displayed to the editing teacher in the course2 context. $browser = new \repository_contentbank\browser\contentbank_browser_context_course($course2context); $repositorycontents = $browser->get_content(); // Content nodes should not be available to the teacher in the course2 context. The editing teacher is not // enrolled in this course. $this->assertCount(0, $repositorycontents); } /** * Test get_navigation() in the system context. */ public function test_get_navigation_system_context(): void { $this->resetAfterTest(true); $systemcontext = \context_system::instance(); $browser = new \repository_contentbank\browser\contentbank_browser_context_system($systemcontext); $navigation = $browser->get_navigation(); // The navigation array should contain only 1 element, representing the system navigation node. $this->assertCount(1, $navigation); $expected = [ \repository_contentbank\helper::create_navigation_node($systemcontext) ]; $this->assertEquals($expected, $navigation); } /** * Test get_navigation() in the course category context. */ public function test_get_navigation_course_category_context(): void { $this->resetAfterTest(true); $systemcontext = \context_system::instance(); // Create a course category. $category = $this->getDataGenerator()->create_category(['name' => 'category']); $categorycontext = \context_coursecat::instance($category->id); // Create a course subcategory. $subcategory = $this->getDataGenerator()->create_category(['name' => 'subcategory', 'parent' => $category->id]); $subcategorytcontext = \context_coursecat::instance($subcategory->id); // Get navigation nodes in the category context. $browser = new \repository_contentbank\browser\contentbank_browser_context_coursecat($categorycontext); $navigation = $browser->get_navigation(); // The navigation array should contain 2 elements, representing the system and course category // navigation nodes. $this->assertCount(2, $navigation); $expected = [ \repository_contentbank\helper::create_navigation_node($systemcontext), \repository_contentbank\helper::create_navigation_node($categorycontext) ]; $this->assertEquals($expected, $navigation); // Get navigation nodes in the subcategory context. $browser = new \repository_contentbank\browser\contentbank_browser_context_coursecat($subcategorytcontext); $navigation = $browser->get_navigation(); // The navigation array should contain 3 elements, representing the system, category and subcategory // navigation nodes. $this->assertCount(3, $navigation); $expected = [ \repository_contentbank\helper::create_navigation_node($systemcontext), \repository_contentbank\helper::create_navigation_node($categorycontext), \repository_contentbank\helper::create_navigation_node($subcategorytcontext) ]; $this->assertEquals($expected, $navigation); } /** * Test get_navigation() in the course context. */ public function test_get_navigation_course_context(): void { $this->resetAfterTest(true); $systemcontext = \context_system::instance(); // Create a category. $category = $this->getDataGenerator()->create_category(['name' => 'category']); $categorycontext = \context_coursecat::instance($category->id); // Create a subcategory. $subcategory = $this->getDataGenerator()->create_category(['name' => 'category', 'parent' => $category->id]); $subcategorycontext = \context_coursecat::instance($subcategory->id); // Create a course in category. $categorycourse = $this->getDataGenerator()->create_course(['category' => $category->id]); $categorycoursecontext = \context_course::instance($categorycourse->id); // Create a course in subcategory. $subcategorycourse = $this->getDataGenerator()->create_course(['category' => $subcategory->id]); $subcategorycoursecontext = \context_course::instance($subcategorycourse->id); // Get navigation nodes in the category course context. $browser = new \repository_contentbank\browser\contentbank_browser_context_course($categorycoursecontext); $navigation = $browser->get_navigation(); // The navigation array should contain 3 elements, representing the system, category and course // navigation nodes. $this->assertCount(3, $navigation); $expected = [ \repository_contentbank\helper::create_navigation_node($systemcontext), \repository_contentbank\helper::create_navigation_node($categorycontext), \repository_contentbank\helper::create_navigation_node($categorycoursecontext) ]; $this->assertEquals($expected, $navigation); // Get navigation nodes in the subcategory course context. $browser = new \repository_contentbank\browser\contentbank_browser_context_course($subcategorycoursecontext); $navigation = $browser->get_navigation(); // The navigation array should contain 4 elements, representing the system, category, subcategory and // subcategory course navigation nodes. $this->assertCount(4, $navigation); $expected = [ \repository_contentbank\helper::create_navigation_node($systemcontext), \repository_contentbank\helper::create_navigation_node($categorycontext), \repository_contentbank\helper::create_navigation_node($subcategorycontext), \repository_contentbank\helper::create_navigation_node($subcategorycoursecontext) ]; $this->assertEquals($expected, $navigation); } /** * Generate the expected array of content bank nodes. * * @param array $contextfolders The array containing the expected folder nodes * @param array $contentbankcontents The array containing the expected contents * @return array[] The expected array of content bank nodes */ private function generate_expected_content(array $contextfolders = [], array $contentbankcontents = []): array { $expected = []; if (!empty($contextfolders)) { foreach ($contextfolders as $contextfolder) { $expected[] = \repository_contentbank\helper::create_context_folder_node($contextfolder['name'], base64_encode(json_encode(['contextid' => $contextfolder['contextid']]))); } } if (!empty($contentbankcontents)) { foreach ($contentbankcontents as $content) { $expected[] = \repository_contentbank\helper::create_contentbank_content_node($content); } } return $expected; } } PK/\"{n+n+tests/search/search_test.phpnu[. /** * Content bank repository search unit tests. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_contentbank\search; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once("$CFG->dirroot/repository/lib.php"); /** * Tests for the content bank search class. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class search_test extends \advanced_testcase { /** * Test get_search_contents() by searching through some existing content using different search terms. * * @dataProvider get_search_contents_provider * @param array $contentnames The array containing the names of the content that needs to be generated * @param string $search The search string * @param array $expected The array containing the expected content names that should be returned by the search */ public function test_get_search_contents(array $contentnames, string $search, array $expected): void { $this->resetAfterTest(); $admin = get_admin(); $systemcontext = \context_system::instance(); // Add some content to the content bank. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); foreach ($contentnames as $contentname) { $generator->generate_contentbank_data('contenttype_h5p', 1, $admin->id, $systemcontext, true, 'file.h5p', $contentname); } // Log in as admin. $this->setUser($admin); // Search for content bank content items which have the search pattern within the name. $searchcontentnodes = \repository_contentbank\contentbank_search::get_search_contents($search); // Get the content name of the each content returned after performing the search. $actual = array_map(function($searchcontentnode) { return $searchcontentnode['shorttitle']; }, $searchcontentnodes); $this->assertEqualsCanonicalizing($expected, $actual); } /** * Data provider for test_get_search_contents(). * * @return array */ public static function get_search_contents_provider(): array { return [ 'Search for existing pattern found within the name of content items' => [ [ 'systemcontentfile1', 'systemcontentfile2', 'somesystemfile' ], 'content', [ 'systemcontentfile1', 'systemcontentfile2' ] ], 'Search for existing pattern found at the beginning of the name of content items' => [ [ 'systemcontentfile1', 'systemcontentfile2', 'somesystemfile' ], 'some', [ 'somesystemfile', ] ], 'Search for existing pattern found at the end of the name of content items' => [ [ 'systemcontentfile1', 'systemcontentfile2', 'somesystemfile' ], 'file2', [ 'systemcontentfile2', ] ], 'Search for a pattern which does not exist within the name of any content item' => [ [ 'systemcontentfile1', 'somesystemfile' ], 'somefile', [] ], 'Case-insensitive search for a pattern which exists within the name of content items' => [ [ 'systemcontentfile1', 'systemcontentfile2', 'somesystemfile' ], 'CONTENT', [ 'systemcontentfile1', 'systemcontentfile2' ] ] ]; } /** * Test get_search_contents() by searching for content with users that have capability to access/view * all existing content bank content. By default, admins, managers should be able to view every existing content * that matches the search criteria. */ public function test_get_search_contents_user_can_access_all_content(): void { $this->resetAfterTest(true); // Create a course in default category by default. $course = $this->getDataGenerator()->create_course(); $coursecontext = \context_course::instance($course->id); // Create a course category without a course. $category = $this->getDataGenerator()->create_category(); $categorycontext = \context_coursecat::instance($category->id); $admin = get_admin(); // Add some content to the content bank in different contexts. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); // Add a content bank file in the category context. $categorycontents = $generator->generate_contentbank_data('contenttype_h5p', 1, $admin->id, $categorycontext, true, 'file.h5p', 'categorycontentfile'); $categorycontent = reset($categorycontents); // Add a content bank file in the course context. $coursecontents = $generator->generate_contentbank_data('contenttype_h5p', 1, $admin->id, $coursecontext, true, 'file.h5p', 'coursecontentfile'); $coursecontent = reset($coursecontents); // Log in as admin. $this->setUser($admin); // Search for content bank content items which have the pattern 'contentfile' within the name. $search = 'contentfile'; $searchcontentnodes = \repository_contentbank\contentbank_search::get_search_contents($search); // All content files which name matches the search criteria should be available to the admin user. // The search should return 2 file nodes. $this->assertCount(2, $searchcontentnodes); $expected = [ \repository_contentbank\helper::create_contentbank_content_node($categorycontent), \repository_contentbank\helper::create_contentbank_content_node($coursecontent), ]; $this->assertEqualsCanonicalizing($expected, $searchcontentnodes); } /** * Test get_search_contents() by searching for content with users that have capability to access/view only * certain existing content bank content. By default, editing teachers should be able to view content that matches * the search criteria AND is in the courses they are enrolled, course categories of the enrolled courses * and system content. Other authenticated users should be able to access only the system content. */ public function test_get_search_contents_user_can_access_certain_content(): void { global $CFG; $this->resetAfterTest(true); $systemcontext = \context_system::instance(); // Create course1. $course1 = $this->getDataGenerator()->create_course(); $course1context = \context_course::instance($course1->id); // Create course2. $course2 = $this->getDataGenerator()->create_course(); $course2context = \context_course::instance($course2->id); $admin = get_admin(); // Create and enrol an editing teacher in course1. $editingteacher = $this->getDataGenerator()->create_and_enrol($course1, 'editingteacher'); // Create and enrol a teacher in course2. $teacher = $this->getDataGenerator()->create_and_enrol($course2, 'teacher'); // Add some content to the content bank in different contexts. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); // Add a content bank file in the system context. $filepath = $CFG->dirroot . '/h5p/tests/fixtures/ipsums.h5p'; $systemcontents = $generator->generate_contentbank_data('contenttype_h5p', 1, $admin->id, $systemcontext, true, $filepath, 'systemcontentfile'); $systemcontent = reset($systemcontents); // Add a content bank file in the course1 context. $course1contents = $generator->generate_contentbank_data('contenttype_h5p', 1, $admin->id, $course1context, true, $filepath, 'coursecontentfile1'); $course1content = reset($course1contents); // Add a content bank file in the course2 context. $generator->generate_contentbank_data('contenttype_h5p', 1, $admin->id, $course2context, true, $filepath, 'coursecontentfile2'); // Log in as an editing teacher. $this->setUser($editingteacher); // Search for content bank content items which have the pattern 'contentfile' within the name. $search = 'contentfile'; $searchcontentnodes = \repository_contentbank\contentbank_search::get_search_contents($search); // The search should return 2 file nodes. The editing teacher does not have access to the content of course2 // and therefore, the course2 content should be skipped. $this->assertCount(2, $searchcontentnodes); $expected = [ \repository_contentbank\helper::create_contentbank_content_node($systemcontent), \repository_contentbank\helper::create_contentbank_content_node($course1content), ]; $this->assertEqualsCanonicalizing($expected, $searchcontentnodes); // Log in as a teacher. $this->setUser($teacher); // Search for content bank content items which have the pattern 'contentfile' within the name. $search = 'contentfile'; $searchcontentnodes = \repository_contentbank\contentbank_search::get_search_contents($search); // The search should return 1 file node. The teacher should only be able to view system content. $this->assertCount(1, $searchcontentnodes); $expected = [ \repository_contentbank\helper::create_contentbank_content_node($systemcontent), ]; $this->assertEqualsCanonicalizing($expected, $searchcontentnodes); } } PK/\"ntests/behat/file_update.featurenu[@repository @repository_contentbank @javascript @core_h5p Feature: Updating a file in the content bank after using in a course In order to use file alias As a user Updated files must update references when is an alias Background: Given the following "categories" exist: | name | category | idnumber | | Category1 | 0 | CAT1 | And the following "courses" exist: | fullname | shortname | category | | Course1 | C1 | CAT1 | And the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | Course | C1 | contenttype_h5p | admin | package.h5p | /h5p/tests/fixtures/guess-the-answer.h5p | Scenario: Referenced files is the default option and updates alias as well When I log in as "admin" And I add an h5pactivity activity to course "Course1" section "1" And I set the following fields to these values: | Name | guessFile | And I click on "Add..." "link" in the ".fp-toolbar" "css_element" And I select "Content bank" repository in file picker And I click on "package.h5p" "file" in repository content area And I click on "Select this file" "button" And I click on "Save and display" "button" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe Then I should see "Press here to reveal answer" And I switch to the main frame # Now edit the content in the content bank. When I am on "Course1" course homepage with editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present And I expand "Site pages" node And I click on "Content bank" "link" And I click on "package.h5p" "link" And I click on "Edit" "link" And I wait until the page is ready And I switch to "h5p-editor-iframe" class iframe And I set the field "Title" to "Required title" And I set the field "Descriptive solution label" to "This is a new text" And I switch to the main frame And I click on "Save" "button" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe Then I should see "This is a new text" And I switch to the main frame # Check the course page is updated. When I am on "Course1" course homepage with editing mode on And I click on "guessFile" "link" in the "page-content" "region" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe Then I should see "This is a new text" And I switch to the main frame Scenario: Copied files should not be updated if the original is edited When I log in as "admin" And I add an h5pactivity activity to course "Course1" section "1" And I set the following fields to these values: | Name | guessFile | And I click on "Add..." "link" in the ".fp-toolbar" "css_element" And I select "Content bank" repository in file picker And I click on "package.h5p" "file" in repository content area And I click on "Make a copy of the file" "radio" And I click on "Select this file" "button" And I click on "Save and display" "button" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe Then I should see "Press here to reveal answer" And I switch to the main frame # Now edit the content in the content bank. When I am on "Course1" course homepage with editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present And I expand "Site pages" node And I click on "Content bank" "link" And I click on "package.h5p" "link" And I click on "Edit" "link" And I wait until the page is ready And I switch to "h5p-editor-iframe" class iframe And I set the field "Title" to "Required title" And I set the field "Descriptive solution label" to "This is a new text" And I switch to the main frame And I click on "Save" "button" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe Then I should see "This is a new text" And I switch to the main frame # Check the course page is not updated. When I am on "Course1" course homepage with editing mode on And I click on "guessFile" "link" in the "page-content" "region" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe Then I should see "Press here to reveal answer" And I switch to the main frame PK/\׆N''"tests/behat/select_content.featurenu[@repository @repository_contentbank @javascript Feature: Select content bank files using the content bank files repository In order to re-use content bank files As a user I need to be able to view and select content bank files using the content bank repository Background: Given the following "users" exist: | username | firstname | lastname | email | | student | Student | 1 | student@example.com | | teacher1 | Teacher 1 | 1 | teacher1@example.com | | teacher2 | Teacher 2 | 1 | teacher2@example.com | And the following "categories" exist: | name | category | idnumber | | Category1 | 0 | CAT1 | | SubCategory1 | CAT1 | SUBCAT1 | And the following "courses" exist: | fullname | shortname | category | | MiscellaneousCourse1 | mscC1 | 0 | | MiscellaneousCourse2 | mscC2 | 0 | | Category1Course1 | cat1C1 | CAT1 | | SubCategory1Course1 | subcat1C1 | SUBCAT1 | And the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | Course | mscC1 | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p | | Course | mscC2 | contenttype_h5p | admin | find-the-words.h5p | /h5p/tests/fixtures/find-the-words.h5p | | Course | subcat1C1 | contenttype_h5p | admin | greeting-card.h5p | /h5p/tests/fixtures/greeting-card.h5p | | Category | CAT1 | contenttype_h5p | admin | ipsums.h5p | /h5p/tests/fixtures/ipsums.h5p | | Category | SUBCAT1 | contenttype_h5p | admin | multiple-choice-2-6.h5p | /h5p/tests/fixtures/multiple-choice-2-6.h5p | | System | | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p | And the following "activities" exist: | activity | name | intro | introformat | course | idnumber | | forum | Forum | | 1 | mscC1 | forum1 | | folder | Folder | FolderDesc | 1 | mscC1 | folder1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | mscC1 | editingteacher | | teacher2 | mscC1 | teacher | | student | mscC1 | student | Scenario: Admin can navigate and see all existing content bank files using the content bank repository Given I am on the Folder "Folder activity" page logged in as admin And I click on "Edit" "button" And I click on "Add..." "button" And I should see "Content bank" in the ".fp-repo-area" "css_element" When I select "Content bank" repository in file picker Then I should see "System > Category 1 > MiscellaneousCourse1" breadcrumb navigation in repository And I should see "1" elements in repository content area And I should see "filltheblanks.h5p" "file" in repository content area And I click on "Category 1" "link" in the ".file-picker .fp-pathbar" "css_element" And I should see "System > Category 1" breadcrumb navigation in repository And I should see "2" elements in repository content area And I should see "MiscellaneousCourse1" "folder" in repository content area And I should see "MiscellaneousCourse2" "folder" in repository content area And I click on "MiscellaneousCourse2" "folder" in repository content area And I should see "System > Category 1 > MiscellaneousCourse2" breadcrumb navigation in repository And I should see "1" elements in repository content area And I should see "find-the-words.h5p" "file" in repository content area And I click on "System" "link" in the ".file-picker .fp-pathbar" "css_element" And I should see "System" breadcrumb navigation in repository And I should see "3" elements in repository content area And I should see "filltheblanks.h5p" "file" in repository content area And I should see "Category 1" "folder" in repository content area And I should see "Category1" "folder" in repository content area And I click on "Category1" "folder" in repository content area And I should see "System > Category1" breadcrumb navigation in repository And I should see "3" elements in repository content area And I should see "SubCategory1" "folder" in repository content area And I should see "Category1Course1" "folder" in repository content area And I should see "ipsums.h5p" "file" in repository content area And I click on "SubCategory1" "folder" in repository content area And I should see "System > Category1 > SubCategory1" breadcrumb navigation in repository And I should see "2" elements in repository content area And I should see "SubCategory1Course1" "folder" in repository content area And I should see "multiple-choice-2-6.h5p" "file" in repository content area And I click on "SubCategory1Course1" "folder" in repository content area And I should see "System > Category1 > SubCategory1 > SubCategory1Course1" breadcrumb navigation in repository And I should see "1" elements in repository content area And I should see "greeting-card.h5p" "file" in repository content area Scenario: Admin can select and re-use content bank files using the content bank repository Given I am on the Folder "Folder activity" page logged in as admin And I click on "Edit" "button" And I click on "Add..." "button" And I should see "Content bank" in the ".fp-repo-area" "css_element" And I select "Content bank" repository in file picker And I should see "System > Category 1 > MiscellaneousCourse1" breadcrumb navigation in repository And I click on "System" "link" in the ".file-picker .fp-pathbar" "css_element" And I click on "Category1" "folder" in repository content area And I should see "ipsums.h5p" "file" in repository content area And I click on "ipsums.h5p" "file" in repository content area And I should see "Select ipsums.h5p" When I click on "Select this file" "button" Then I should see "1" elements in "Files" filemanager And I should see "ipsums.h5p" in the ".fp-content .fp-file" "css_element" Scenario: Editing teacher can navigate and see content bank files available to him using the content bank repository Given I am on the Folder "Folder activity" page logged in as teacher1 And I click on "Edit" "button" And I click on "Add..." "button" And I should see "Content bank" in the ".fp-repo-area" "css_element" When I select "Content bank" repository in file picker Then I should see "System > Category 1 > MiscellaneousCourse1" breadcrumb navigation in repository And I should see "1" elements in repository content area And I should see "filltheblanks.h5p" "file" in repository content area And I click on "Category 1" "link" in the ".file-picker .fp-pathbar" "css_element" And I should see "System > Category 1" breadcrumb navigation in repository And I should see "1" elements in repository content area And I should see "MiscellaneousCourse1" "folder" in repository content area And I click on "System" "link" in the ".file-picker .fp-pathbar" "css_element" And I should see "System" breadcrumb navigation in repository And I should see "2" elements in repository content area And I should see "filltheblanks.h5p" "file" in repository content area And I should see "Category 1" "folder" in repository content area Scenario: Editing teacher can select and re-use content bank files available to him using the content bank repository Given I am on the Folder "Folder activity" page logged in as teacher1 And I click on "Edit" "button" And I click on "Add..." "button" And I should see "Content bank" in the ".fp-repo-area" "css_element" And I select "Content bank" repository in file picker And I should see "System > Category 1 > MiscellaneousCourse1" breadcrumb navigation in repository And I click on "System" "link" in the ".file-picker .fp-pathbar" "css_element" And I should see "filltheblanks.h5p" "file" in repository content area And I click on "filltheblanks.h5p" "file" in repository content area And I should see "Select filltheblanks.h5p" When I click on "Select this file" "button" Then I should see "1" elements in "Files" filemanager And I should see "filltheblanks.h5p" in the ".fp-content .fp-file" "css_element" Scenario: Non-editing teacher can not see the content bank repository Given I am on the Forum "Forum activity" page logged in as teacher2 And I click on "Add discussion topic" "link" And I click on "Advanced" "button" When I click on "Add..." "button" Then I should not see "Content bank" in the ".fp-repo-area" "css_element" Scenario: Student can not see the content bank repository Given I am on the Forum "Forum activity" page logged in as student And I click on "Add discussion topic" "link" And I click on "Advanced" "button" When I click on "Add..." "button" Then I should not see "Content bank" in the ".fp-repo-area" "css_element" Scenario: Both content name and file name are shown when a content is selected Given the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | Course | mscC1 | contenttype_h5p | admin | My content | /h5p/tests/fixtures/ipsums.h5p | And I am on the Folder "Folder activity" page logged in as admin And I click on "Edit" "button" And I click on "Add..." "button" And I should see "Content bank" in the ".fp-repo-area" "css_element" When I select "Content bank" repository in file picker Then I should see "My content" And I click on "My content" "link" And I should see "Select My content" And the field "Save as" matches value "ipsums.h5p" PK*\edSSview.phpnu[PK*\??customfield.phpnu[PK*\i" lib.phpnu[PK*\`2'-classes/customfield/content_handler.phpnu[PK*\f""Iclasses/form/upload_files.phpnu[PK*\ȲA A lclasses/form/edit_content.phpnu[PK*\ѱ/#zclasses/external/rename_content.phpnu[PK*\d #classes/external/delete_content.phpnu[PK*\l!classes/external/copy_content.phpnu[PK*\hJ;;+classes/external/set_content_visibility.phpnu[PK*\classes/privacy/provider.phpnu[PK*\Zч;;classes/contentbank.phpnu[PK*\D classes/helper.phpnu[PK*\:06TTclasses/output/viewcontent.phpnu[PK*\<;"">classes/output/bankcontent.phpnu[PK*\z!mmaclasses/output/customfields.phpnu[PK*\ENN^iclasses/contenttype.phpnu[PK*\+_55fclasses/content.phpnu[PK*\IsIItemplates/copycontent.mustachenu[PK*\J^templates/viewcontent.mustachenu[PK*\T *templates/viewcontent/toolbarview.mustachenu[PK*\Y,yy"templates/contentbankmenu.mustachenu[PK*\ 89191 templates/bankcontent.mustachenu[PK*\fY* * /C=templates/bankcontent/toolbar_dropdown.mustachenu[PK*\U%Htemplates/bankcontent/search.mustachenu[PK*\LQff)Mtemplates/bankcontent/navigation.mustachenu[PK*\`,v &Ptemplates/bankcontent/toolbar.mustachenu[PK*\fh]templates/customfields.mustachenu[PK*\@TT atemplates/renamecontent.mustachenu[PK*\2kk xfupgrade.txtnu[PK*\BQQiedit.phpnu[PK*\% {index.phpnu[PK*\c\\amd/src/upload.jsnu[PK*\ K?&?&amd/src/sort.jsnu[PK*\]z5z5amd/src/actions.jsnu[PK*\gISYYamd/src/search.jsnu[PK*\E7g amd/src/displayunlisted.jsnu[PK*\~k  \amd/src/selectors.jsnu[PK*\}amd/build/search.min.js.mapnu[PK*\ABB:amd/build/actions.min.jsnu[PK*\l)II1Ramd/build/actions.min.js.mapnu[PK*\Q!)!hh Ramd/build/displayunlisted.min.jsnu[PK*\"&  amd/build/selectors.min.js.mapnu[PK*\eamd/build/sort.min.jsnu[PK*\Kamd/build/selectors.min.jsnu[PK*\,-$amd/build/displayunlisted.min.js.mapnu[PK*\b77amd/build/sort.min.js.mapnu[PK*\1l  amd/build/search.min.jsnu[PK*\kxYamd/build/upload.min.jsnu[PK*\Q Q 4amd/build/upload.min.js.mapnu[PK*\M9DD(contenttype/h5p/db/access.phpnu[PK*\b}AA+a0contenttype/h5p/lang/en/contenttype_h5p.phpnu[PK*\R5contenttype/h5p/version.phpnu[PK*\Z':contenttype/h5p/classes/form/editor.phpnu[PK*\peX,Xcontenttype/h5p/classes/privacy/provider.phpnu[PK*\%/'%_contenttype/h5p/classes/contenttype.phpnu[PK*\[Ntt#xcontenttype/h5p/classes/content.phpnu[PK*\-w*Ɋcontenttype/h5p/tests/content_h5p_test.phpnu[PK*\t?.contenttype/h5p/tests/contenttype_h5p_test.phpnu[PK*\BB;wff:contenttype/h5p/tests/behat/teacher_upload_content.featurenu[PK*\i_enn;contenttype/h5p/tests/behat/teacher_replace_content.featurenu[PK*\ܪi8contenttype/h5p/tests/behat/admin_upload_content.featurenu[PK*\w 2contenttype/h5p/tests/behat/manage_content.featurenu[PK*\wu8&contenttype/h5p/tests/behat/disable_contenttypes.featurenu[PK*\s}6*9U9contenttype/h5p/tests/behat/admin_replace_content.featurenu[PK*\!8||#?tests/fixtures/testable_content.phpnu[PK*\&ah6 'Gtests/fixtures/testable_contenttype.phpnu[PK*\z'lStests/generator/lib.phpnu[PK*\w}hh$~Xtests/external/copy_content_test.phpnu[PK*\{vww&:stests/external/delete_content_test.phpnu[PK*\!.&tests/external/rename_content_test.phpnu[PK*\. eectests/contenttype_test.phpnu[PK*\f`v E Eptests/privacy/provider_test.phpnu[PK*\*i${f{fJtests/contentbank_test.phpnu[PK*\S`»P9P9tests/content_test.phpnu[PK*\O` %tests/behat/sort_content.featurenu[PK*\J)) 8tests/behat/edit_content.featurenu[PK*\˟#4$tests/behat/restore_content.featurenu[PK*\)l $*,tests/behat/download_content.featurenu[PK*\wr  $K9tests/behat/view_preferences.featurenu[PK*\b#F T T Etests/behat/events.featurenu[PK*\*O<<">Stests/behat/search_content.featurenu[PK*\(%% ltests/behat/copy_content.featurenu[PK*\#H$ޒtests/behat/navigate_content.featurenu[PK*\%-ss&tests/behat/access_permissions.featurenu[PK*\YϘ tests/behat/customfields.featurenu[PK*\ ;;;"htests/behat/delete_content.featurenu[PK*\v##tests/behat/visibility.featurenu[PK/\iidb/install.phpnu[PK/\> db/access.phpnu[PK/\Ǚ[TT"lang/en/repository_contentbank.phpnu[PK/\2% Spix/icon.svgnu[PK/\$ pix/icon.pngnu[PK/\%   version.phpnu[PK/\ 6%classes/browser/contentbank_browser_context_course.phpnu[PK/\`8v* * 9%0classes/browser/contentbank_browser_context_coursecat.phpnu[PK/\P'=classes/browser/contentbank_browser.phpnu[PK/\^> 6Yclasses/browser/contentbank_browser_context_system.phpnu[PK/\oQ؃ rcclasses/contentbank_search.phpnu[PK/\ eeCntests/browser_test.phpnu[PK/\"{n+n+wtests/search/search_test.phpnu[PK/\"n1 tests/behat/file_update.featurenu[PK/\׆N''" tests/behat/select_content.featurenu[PKggB&#;