ÿØÿà 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>\LYYexternallib.phpnu[. 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; /** * Web service related functions * * @package core * @category external * @copyright 2012 Jerome Mouneyrac * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.4 */ class core_external extends external_api { /** * Format the received string parameters to be sent to the core get_string() function. * * @param array $stringparams * @return object|string * @since Moodle 2.4 */ public static function format_string_parameters($stringparams) { // Check if there are some string params. $strparams = new stdClass(); if (!empty($stringparams)) { // There is only one string parameter. if (count($stringparams) == 1) { $stringparam = array_pop($stringparams); if (isset($stringparam['name'])) { $strparams->{$stringparam['name']} = $stringparam['value']; } else { // It is a not named string parameter. $strparams = $stringparam['value']; } } else { // There are more than one parameter. foreach ($stringparams as $stringparam) { // If a parameter is unnamed throw an exception // unnamed param is only possible if one only param is sent. if (empty($stringparam['name'])) { throw new moodle_exception('unnamedstringparam', 'webservice'); } $strparams->{$stringparam['name']} = $stringparam['value']; } } } return $strparams; } /** * Returns description of get_string parameters * * @return external_function_parameters * @since Moodle 2.4 */ public static function get_string_parameters() { return new external_function_parameters( array('stringid' => new external_value(PARAM_STRINGID, 'string identifier'), 'component' => new external_value(PARAM_COMPONENT,'component', VALUE_DEFAULT, 'moodle'), 'lang' => new external_value(PARAM_LANG, 'lang', VALUE_DEFAULT, null), 'stringparams' => new external_multiple_structure ( new external_single_structure(array( 'name' => new external_value(PARAM_ALPHANUMEXT, 'param name - if the string expect only one $a parameter then don\'t send this field, just send the value.', VALUE_OPTIONAL), 'value' => new external_value(PARAM_RAW,'param value'))), 'the definition of a string param (i.e. {$a->name})', VALUE_DEFAULT, array() ) ) ); } /** * Return a core get_string() call * * @param string $identifier string identifier * @param string $component string component * @param array $stringparams the string params * @return string * @since Moodle 2.4 */ public static function get_string($stringid, $component = 'moodle', $lang = null, $stringparams = array()) { $params = self::validate_parameters(self::get_string_parameters(), array('stringid'=>$stringid, 'component' => $component, 'lang' => $lang, 'stringparams' => $stringparams)); $stringmanager = get_string_manager(); return $stringmanager->get_string($params['stringid'], $params['component'], core_external::format_string_parameters($params['stringparams']), $params['lang']); } /** * Returns description of get_string() result value * * @return \core_external\external_description * @since Moodle 2.4 */ public static function get_string_returns() { return new external_value(PARAM_RAW, 'translated string'); } /** * Returns description of get_string parameters * * @return external_function_parameters * @since Moodle 2.4 */ public static function get_strings_parameters() { return new external_function_parameters( array('strings' => new external_multiple_structure ( new external_single_structure (array( 'stringid' => new external_value(PARAM_STRINGID, 'string identifier'), 'component' => new external_value(PARAM_COMPONENT, 'component', VALUE_DEFAULT, 'moodle'), 'lang' => new external_value(PARAM_LANG, 'lang', VALUE_DEFAULT, null), 'stringparams' => new external_multiple_structure ( new external_single_structure(array( 'name' => new external_value(PARAM_ALPHANUMEXT, 'param name - if the string expect only one $a parameter then don\'t send this field, just send the value.', VALUE_OPTIONAL), 'value' => new external_value(PARAM_RAW, 'param value'))), 'the definition of a string param (i.e. {$a->name})', VALUE_DEFAULT, array() )) ) ) ) ); } /** * Return multiple call to core get_string() * * @param array $strings strings to translate * @return array * * @since Moodle 2.4 */ public static function get_strings($strings) { $params = self::validate_parameters(self::get_strings_parameters(), array('strings'=>$strings)); $stringmanager = get_string_manager(); $translatedstrings = array(); foreach($params['strings'] as $string) { if (!empty($string['lang'])) { $lang = $string['lang']; } else { $lang = current_language(); } $translatedstrings[] = array( 'stringid' => $string['stringid'], 'component' => $string['component'], 'lang' => $lang, 'string' => $stringmanager->get_string($string['stringid'], $string['component'], core_external::format_string_parameters($string['stringparams']), $lang)); } return $translatedstrings; } /** * Returns description of get_string() result value * * @return \core_external\external_description * @since Moodle 2.4 */ public static function get_strings_returns() { return new external_multiple_structure( new external_single_structure(array( 'stringid' => new external_value(PARAM_STRINGID, 'string id'), 'component' => new external_value(PARAM_COMPONENT, 'string component'), 'lang' => new external_value(PARAM_LANG, 'lang'), 'string' => new external_value(PARAM_RAW, 'translated string')) )); } /** * Returns description of get_user_dates parameters * * @return external_function_parameters */ public static function get_user_dates_parameters() { return new external_function_parameters( [ 'contextid' => new external_value( PARAM_INT, 'Context ID. Either use this value, or level and instanceid.', VALUE_DEFAULT, 0 ), 'contextlevel' => new external_value( PARAM_ALPHA, 'Context level. To be used with instanceid.', VALUE_DEFAULT, '' ), 'instanceid' => new external_value( PARAM_INT, 'Context instance ID. To be used with level', VALUE_DEFAULT, 0 ), 'timestamps' => new external_multiple_structure ( new external_single_structure ( [ 'timestamp' => new external_value(PARAM_INT, 'unix timestamp'), 'format' => new external_value(PARAM_TEXT, 'format string'), 'type' => new external_value(PARAM_PLUGIN, 'The calendar type', VALUE_DEFAULT), 'fixday' => new external_value(PARAM_INT, 'Remove leading zero for day', VALUE_DEFAULT, 1), 'fixhour' => new external_value(PARAM_INT, 'Remove leading zero for hour', VALUE_DEFAULT, 1), ] ) ) ] ); } /** * Format an array of timestamps. * * @param int|null $contextid The contenxt id * @param string|null $contextlevel The context level * @param int|null $instanceid The instnace id for the context level * @param array $timestamps Timestamps to format * @return array */ public static function get_user_dates($contextid, $contextlevel, $instanceid, $timestamps) { $params = self::validate_parameters( self::get_user_dates_parameters(), [ 'contextid' => $contextid, 'contextlevel' => $contextlevel, 'instanceid' => $instanceid, 'timestamps' => $timestamps, ] ); $context = self::get_context_from_params($params); self::validate_context($context); $formatteddates = array_map(function($timestamp) { $calendartype = $timestamp['type']; $fixday = !empty($timestamp['fixday']); $fixhour = !empty($timestamp['fixhour']); $calendar = \core_calendar\type_factory::get_calendar_instance($calendartype); return $calendar->timestamp_to_date_string($timestamp['timestamp'], $timestamp['format'], 99, $fixday, $fixhour); }, $params['timestamps']); return ['dates' => $formatteddates]; } /** * Returns description of get_user_dates() result value * * @return \core_external\external_description */ public static function get_user_dates_returns() { return new external_single_structure( [ 'dates' => new external_multiple_structure ( new external_value(PARAM_TEXT, 'formatted dates strings') ) ] ); } /** * Returns description of get_component_strings parameters * * @return external_function_parameters * @since Moodle 2.4 */ public static function get_component_strings_parameters() { return new external_function_parameters( array('component' => new external_value(PARAM_COMPONENT, 'component'), 'lang' => new external_value(PARAM_LANG, 'lang', VALUE_DEFAULT, null), ) ); } /** * Return all lang strings of a component - call to core get_component_strings(). * * @param string $component component name * @return array * * @since Moodle 2.4 */ public static function get_component_strings($component, $lang = null) { if (empty($lang)) { $lang = current_language(); } $params = self::validate_parameters(self::get_component_strings_parameters(), array('component'=>$component, 'lang' => $lang)); $stringmanager = get_string_manager(); $wsstrings = array(); $componentstrings = $stringmanager->load_component_strings($params['component'], $params['lang']); foreach($componentstrings as $stringid => $string) { $wsstring = array(); $wsstring['stringid'] = $stringid; $wsstring['string'] = $string; $wsstrings[] = $wsstring; } return $wsstrings; } /** * Returns description of get_component_strings() result value * * @return \core_external\external_description * @since Moodle 2.4 */ public static function get_component_strings_returns() { return new external_multiple_structure( new external_single_structure(array( 'stringid' => new external_value(PARAM_STRINGID, 'string id'), 'string' => new external_value(PARAM_RAW, 'translated string')) )); } /** * Returns description of get_fragment parameters * * @return external_function_parameters * @since Moodle 3.1 */ public static function get_fragment_parameters() { return new external_function_parameters( array( 'component' => new external_value(PARAM_COMPONENT, 'Component for the callback e.g. mod_assign'), 'callback' => new external_value(PARAM_ALPHANUMEXT, 'Name of the callback to execute'), 'contextid' => new external_value(PARAM_INT, 'Context ID that the fragment is from'), 'args' => new external_multiple_structure( new external_single_structure( array( 'name' => new external_value(PARAM_ALPHANUMEXT, 'param name'), 'value' => new external_value(PARAM_RAW, 'param value') ) ), 'args for the callback are optional', VALUE_OPTIONAL ) ) ); } /** * Get a HTML fragment for inserting into something. Initial use is for inserting mforms into * a page using AJAX. * This web service is designed to be called only via AJAX and not directly. * Callbacks that are called by this web service are responsible for doing the appropriate security checks * to access the information returned. This only does minimal validation on the context. * * @param string $component Name of the component. * @param string $callback Function callback name. * @param int $contextid Context ID this fragment is in. * @param array $args optional arguments for the callback. * @return array HTML and JavaScript fragments for insertion into stuff. * @since Moodle 3.1 */ public static function get_fragment($component, $callback, $contextid, $args = null) { global $OUTPUT, $PAGE; $params = self::validate_parameters(self::get_fragment_parameters(), array( 'component' => $component, 'callback' => $callback, 'contextid' => $contextid, 'args' => $args ) ); // Reformat arguments into something less unwieldy. $arguments = array(); foreach ($params['args'] as $paramargument) { $arguments[$paramargument['name']] = $paramargument['value']; } $context = context::instance_by_id($contextid); self::validate_context($context); $arguments['context'] = $context; // Hack alert: Set a default URL to stop the annoying debug. $PAGE->set_url('/'); // Hack alert: Forcing bootstrap_renderer to initiate moodle page. $OUTPUT->header(); // Overwriting page_requirements_manager with the fragment one so only JS included from // this point is returned to the user. $PAGE->start_collecting_javascript_requirements(); $data = component_callback($params['component'], 'output_fragment_' . $params['callback'], array($arguments)); $jsfooter = $PAGE->requires->get_end_code(); $output = array('html' => $data, 'javascript' => $jsfooter); return $output; } /** * Returns description of get_fragment() result value * * @return \core_external\external_description * @since Moodle 3.1 */ public static function get_fragment_returns() { return new external_single_structure( array( 'html' => new external_value(PARAM_RAW, 'HTML fragment.'), 'javascript' => new external_value(PARAM_RAW, 'JavaScript fragment') ) ); } /** * Parameters for function update_inplace_editable() * * @since Moodle 3.1 * @return external_function_parameters */ public static function update_inplace_editable_parameters() { return new external_function_parameters( array( 'component' => new external_value(PARAM_COMPONENT, 'component responsible for the update', VALUE_REQUIRED), 'itemtype' => new external_value(PARAM_NOTAGS, 'type of the updated item inside the component', VALUE_REQUIRED), 'itemid' => new external_value(PARAM_RAW, 'identifier of the updated item', VALUE_REQUIRED), 'value' => new external_value(PARAM_RAW, 'new value', VALUE_REQUIRED), )); } /** * Update any component's editable value assuming that component implements necessary callback * * @since Moodle 3.1 * @param string $component * @param string $itemtype * @param string $itemid * @param string $value */ public static function update_inplace_editable($component, $itemtype, $itemid, $value) { global $PAGE; // Validate and normalize parameters. $params = self::validate_parameters(self::update_inplace_editable_parameters(), array('component' => $component, 'itemtype' => $itemtype, 'itemid' => $itemid, 'value' => $value)); if (!$functionname = component_callback_exists($component, 'inplace_editable')) { throw new \moodle_exception('inplaceeditableerror'); } $tmpl = component_callback($params['component'], 'inplace_editable', array($params['itemtype'], $params['itemid'], $params['value'])); if (!$tmpl || !($tmpl instanceof \core\output\inplace_editable)) { throw new \moodle_exception('inplaceeditableerror'); } return $tmpl->export_for_template($PAGE->get_renderer('core')); } /** * Return structure for update_inplace_editable() * * @since Moodle 3.1 * @return \core_external\external_description */ public static function update_inplace_editable_returns() { return new external_single_structure( array( 'displayvalue' => new external_value(PARAM_RAW, 'display value (may contain link or other html tags)'), 'component' => new external_value(PARAM_NOTAGS, 'component responsible for the update', VALUE_OPTIONAL), 'itemtype' => new external_value(PARAM_NOTAGS, 'itemtype', VALUE_OPTIONAL), 'value' => new external_value(PARAM_RAW, 'value of the item as it is stored', VALUE_OPTIONAL), 'itemid' => new external_value(PARAM_RAW, 'identifier of the updated item', VALUE_OPTIONAL), 'edithint' => new external_value(PARAM_NOTAGS, 'hint for editing element', VALUE_OPTIONAL), 'editlabel' => new external_value(PARAM_RAW, 'label for editing element', VALUE_OPTIONAL), 'editicon' => new external_single_structure([ 'key' => new external_value(PARAM_RAW, 'Edit icon key', VALUE_OPTIONAL), 'component' => new external_value(PARAM_COMPONENT, 'Edit icon component', VALUE_OPTIONAL), 'title' => new external_value(PARAM_NOTAGS, 'Edit icon title', VALUE_OPTIONAL), ], 'Edit icon', VALUE_OPTIONAL), 'type' => new external_value(PARAM_ALPHA, 'type of the element (text, toggle, select)', VALUE_OPTIONAL), 'options' => new external_value(PARAM_RAW, 'options of the element, format depends on type', VALUE_OPTIONAL), 'linkeverything' => new external_value(PARAM_INT, 'Should everything be wrapped in the edit link or link displayed separately', VALUE_OPTIONAL), ) ); } /** * Returns description of fetch_notifications() parameters. * * @return external_function_parameters * @since Moodle 3.1 */ public static function fetch_notifications_parameters() { return new external_function_parameters( array( 'contextid' => new external_value(PARAM_INT, 'Context ID', VALUE_REQUIRED), )); } /** * Returns description of fetch_notifications() result value. * * @return \core_external\external_description * @since Moodle 3.1 */ public static function fetch_notifications_returns() { return new external_multiple_structure( new external_single_structure( array( 'template' => new external_value(PARAM_RAW, 'Name of the template'), 'variables' => new external_single_structure(array( 'message' => new external_value(PARAM_RAW, 'HTML content of the Notification'), 'extraclasses' => new external_value(PARAM_RAW, 'Extra classes to provide to the tmeplate'), 'announce' => new external_value(PARAM_RAW, 'Whether to announce'), 'closebutton' => new external_value(PARAM_RAW, 'Whether to close'), )), ) ) ); } /** * Returns the list of notifications against the current session. * * @return array * @since Moodle 3.1 */ public static function fetch_notifications($contextid) { global $PAGE; self::validate_parameters(self::fetch_notifications_parameters(), [ 'contextid' => $contextid, ]); $context = \context::instance_by_id($contextid); self::validate_context($context); return \core\notification::fetch_as_array($PAGE->get_renderer('core')); } } PK>\Z1'classes/external_multiple_structure.phpnu[. namespace core_external; /** * Bulk array description class. * * @package core_external * @copyright 2009 Petr Skodak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class external_multiple_structure extends external_description { /** @var external_description content */ public $content; /** * Constructor * * @param external_description $content * @param string $desc * @param int $required * @param array $default * @param bool $allownull */ public function __construct( external_description $content, $desc = '', $required = VALUE_REQUIRED, $default = null, $allownull = NULL_NOT_ALLOWED ) { parent::__construct($desc, $required, $default, $allownull); $this->content = $content; } } PK>\#{\%classes/external_single_structure.phpnu[. namespace core_external; /** * Associative array description class * * @package core_external * @copyright 2009 Petr Skodak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class external_single_structure extends external_description { /** @var array Description of array keys key=>external_description */ public $keys; /** * Constructor * * @param array $keys * @param string $desc * @param int $required * @param array $default * @param bool $allownull */ public function __construct( array $keys, $desc = '', $required = VALUE_REQUIRED, $default = null, $allownull = NULL_NOT_ALLOWED ) { parent::__construct($desc, $required, $default, $allownull); $this->keys = $keys; } } PK>\jɓ7T2T2classes/privacy/provider.phpnu[. namespace core_external\privacy; use context; use context_user; use core_privacy\local\metadata\collection; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\transform; use core_privacy\local\request\writer; use core_privacy\local\request\userlist; use core_privacy\local\request\approved_userlist; /** * Data provider class. * * @package core_external * @copyright 2018 Frédéric Massart * @author Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\subsystem\provider { /** * Returns metadata. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection): collection { $collection->add_database_table('external_tokens', [ 'token' => 'privacy:metadata:tokens:token', 'privatetoken' => 'privacy:metadata:tokens:privatetoken', 'tokentype' => 'privacy:metadata:tokens:tokentype', 'userid' => 'privacy:metadata:tokens:userid', 'creatorid' => 'privacy:metadata:tokens:creatorid', 'iprestriction' => 'privacy:metadata:tokens:iprestriction', 'validuntil' => 'privacy:metadata:tokens:validuntil', 'timecreated' => 'privacy:metadata:tokens:timecreated', 'lastaccess' => 'privacy:metadata:tokens:lastaccess', 'name' => 'privacy:metadata:tokens:name', ], 'privacy:metadata:tokens'); $collection->add_database_table('external_services_users', [ 'userid' => 'privacy:metadata:serviceusers:userid', 'iprestriction' => 'privacy:metadata:serviceusers:iprestriction', 'validuntil' => 'privacy:metadata:serviceusers:validuntil', 'timecreated' => 'privacy:metadata:serviceusers:timecreated', ], 'privacy:metadata:serviceusers'); return $collection; } /** * Get the list of contexts that contain user information for the specified user. * * @param int $userid The user to search. * @return \core_privacy\local\request\contextlist $contextlist The contextlist containing the list of contexts * used in this plugin. */ public static function get_contexts_for_userid(int $userid): \core_privacy\local\request\contextlist { $contextlist = new \core_privacy\local\request\contextlist(); $sql = " SELECT ctx.id FROM {external_tokens} t JOIN {context} ctx ON ctx.instanceid = t.userid AND ctx.contextlevel = :userlevel WHERE t.userid = :userid1 OR t.creatorid = :userid2"; $contextlist->add_from_sql($sql, ['userlevel' => CONTEXT_USER, 'userid1' => $userid, 'userid2' => $userid]); $sql = " SELECT ctx.id FROM {external_services_users} su JOIN {context} ctx ON ctx.instanceid = su.userid AND ctx.contextlevel = :userlevel WHERE su.userid = :userid"; $contextlist->add_from_sql($sql, ['userlevel' => CONTEXT_USER, 'userid' => $userid]); return $contextlist; } /** * Get the list of users within a specific context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { global $DB; $context = $userlist->get_context(); if (!$context instanceof \context_user) { return; } $userid = $context->instanceid; $hasdata = false; $hasdata = $hasdata || $DB->record_exists_select('external_tokens', 'userid = ? OR creatorid = ?', [$userid, $userid]); $hasdata = $hasdata || $DB->record_exists('external_services_users', ['userid' => $userid]); if ($hasdata) { $userlist->add_user($userid); } } /** * Export all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist) { global $DB; $userid = $contextlist->get_user()->id; $contexts = array_reduce($contextlist->get_contexts(), function($carry, $context) use ($userid) { if ($context->contextlevel == CONTEXT_USER) { if ($context->instanceid == $userid) { $carry['has_mine'] = true; } else { $carry['others'][] = $context->instanceid; } } return $carry; }, [ 'has_mine' => false, 'others' => [] ]); $path = [get_string('services', 'core_external')]; // Exporting my stuff. if ($contexts['has_mine']) { $data = []; // Exporting my tokens. $sql = " SELECT t.*, s.name as externalservicename FROM {external_tokens} t JOIN {external_services} s ON s.id = t.externalserviceid WHERE t.userid = :userid ORDER BY t.id"; $recordset = $DB->get_recordset_sql($sql, ['userid' => $userid]); foreach ($recordset as $record) { if (!isset($data['tokens'])) { $data['tokens'] = []; } $data['tokens'][] = static::transform_token($record); } $recordset->close(); // Exporting the services I have access to. $sql = " SELECT su.*, s.name as externalservicename FROM {external_services_users} su JOIN {external_services} s ON s.id = su.externalserviceid WHERE su.userid = :userid ORDER BY su.id"; $recordset = $DB->get_recordset_sql($sql, ['userid' => $userid]); foreach ($recordset as $record) { if (!isset($data['services_user'])) { $data['services_user'] = []; } $data['services_user'][] = [ 'external_service' => $record->externalservicename, 'ip_restriction' => $record->iprestriction, 'valid_until' => $record->validuntil ? transform::datetime($record->validuntil) : null, 'created_on' => transform::datetime($record->timecreated), ]; } $recordset->close(); if (!empty($data)) { writer::with_context(context_user::instance($userid))->export_data($path, (object) $data); }; } // Exporting the tokens I created. if (!empty($contexts['others'])) { list($insql, $inparams) = $DB->get_in_or_equal($contexts['others'], SQL_PARAMS_NAMED); $sql = " SELECT t.*, s.name as externalservicename FROM {external_tokens} t JOIN {external_services} s ON s.id = t.externalserviceid WHERE t.userid $insql AND t.creatorid = :userid1 AND t.userid <> :userid2 ORDER BY t.userid, t.id"; $params = array_merge($inparams, ['userid1' => $userid, 'userid2' => $userid]); $recordset = $DB->get_recordset_sql($sql, $params); static::recordset_loop_and_export($recordset, 'userid', [], function($carry, $record) { $carry[] = static::transform_token($record); return $carry; }, function($userid, $data) use ($path) { writer::with_context(context_user::instance($userid))->export_related_data($path, 'created_by_you', (object) [ 'tokens' => $data ]); }); } } /** * Delete all data for all users in the specified context. * * @param context $context The specific context to delete data for. */ public static function delete_data_for_all_users_in_context(context $context) { if ($context->contextlevel != CONTEXT_USER) { return; } static::delete_user_data($context->instanceid); } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { $context = $userlist->get_context(); if ($context instanceof \context_user) { static::delete_user_data($context->instanceid); } } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { $userid = $contextlist->get_user()->id; foreach ($contextlist as $context) { if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $userid) { static::delete_user_data($context->instanceid); break; } } } /** * Delete user data. * * @param int $userid The user ID. * @return void */ protected static function delete_user_data($userid) { global $DB; $DB->delete_records('external_tokens', ['userid' => $userid]); $DB->delete_records('external_services_users', ['userid' => $userid]); } /** * Transform a token entry. * * @param object $record The token record. * @return array */ protected static function transform_token($record) { $notexportedstr = get_string('privacy:request:notexportedsecurity', 'core_external'); return [ 'external_service' => $record->externalservicename, 'token' => $notexportedstr, 'private_token' => $record->privatetoken ? $notexportedstr : null, 'ip_restriction' => $record->iprestriction, 'valid_until' => $record->validuntil ? transform::datetime($record->validuntil) : null, 'created_on' => transform::datetime($record->timecreated), 'last_access' => $record->lastaccess ? transform::datetime($record->lastaccess) : null, 'name' => $record->name, ]; } /** * Loop and export from a recordset. * * @param \moodle_recordset $recordset The recordset. * @param string $splitkey The record key to determine when to export. * @param mixed $initial The initial data to reduce from. * @param callable $reducer The function to return the dataset, receives current dataset, and the current record. * @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset. * @return void */ protected static function recordset_loop_and_export(\moodle_recordset $recordset, $splitkey, $initial, callable $reducer, callable $export) { $data = $initial; $lastid = null; foreach ($recordset as $record) { if ($lastid && $record->{$splitkey} != $lastid) { $export($lastid, $data); $data = $initial; } $data = $reducer($data, $record); $lastid = $record->{$splitkey}; } $recordset->close(); if (!empty($lastid)) { $export($lastid, $data); } } } PK>\DͿ'h'hclasses/external_api.phpnu[. namespace core_external; use coding_exception; use context; use context_helper; use context_system; use core_component; use core_php_time_limit; use invalid_parameter_exception; use invalid_response_exception; use moodle_exception; /** * Base class for external api methods. * * @package core_webservice * @copyright 2009 Petr Skodak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.0 */ class external_api { /** @var \stdClass context where the function calls will be restricted */ private static $contextrestriction; /** * Returns detailed function information * * @param string|\stdClass $function name of external function or record from external_function * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found; * MUST_EXIST means throw exception if no record or multiple records found * @return \stdClass|bool description or false if not found or exception thrown * @throws coding_exception for any property and/or method that is missing or invalid * @since Moodle 2.0 */ public static function external_function_info($function, $strictness = MUST_EXIST) { global $DB, $CFG; if (!is_object($function)) { if (!$function = $DB->get_record('external_functions', ['name' => $function], '*', $strictness)) { return false; } } // First try class autoloading. if (!class_exists($function->classname)) { // Fallback to explicit include of externallib.php. if (empty($function->classpath)) { $function->classpath = core_component::get_component_directory($function->component) . '/externallib.php'; } else { $function->classpath = "{$CFG->dirroot}/{$function->classpath}"; } if (!file_exists($function->classpath)) { throw new coding_exception( "Cannot find file {$function->classpath} with external function implementation " . "for {$function->classname}::{$function->methodname}" ); } require_once($function->classpath); if (!class_exists($function->classname)) { throw new coding_exception("Cannot find external class {$function->classname}"); } } $function->ajax_method = "{$function->methodname}_is_allowed_from_ajax"; $function->parameters_method = "{$function->methodname}_parameters"; $function->returns_method = "{$function->methodname}_returns"; $function->deprecated_method = "{$function->methodname}_is_deprecated"; // Make sure the implementaion class is ok. if (!method_exists($function->classname, $function->methodname)) { throw new coding_exception( "Missing implementation method {$function->classname}::{$function->methodname}" ); } if (!method_exists($function->classname, $function->parameters_method)) { throw new coding_exception( "Missing parameters description method {$function->classname}::{$function->parameters_method}" ); } if (!method_exists($function->classname, $function->returns_method)) { throw new coding_exception( "Missing returned values description method {$function->classname}::{$function->returns_method}" ); } if (method_exists($function->classname, $function->deprecated_method)) { if (call_user_func([$function->classname, $function->deprecated_method]) === true) { $function->deprecated = true; } } $function->allowed_from_ajax = false; // Fetch the parameters description. $function->parameters_desc = call_user_func([$function->classname, $function->parameters_method]); if (!($function->parameters_desc instanceof external_function_parameters)) { throw new coding_exception( "{$function->classname}::{$function->parameters_method} did not return a valid external_function_parameters object" ); } // Fetch the return values description. $function->returns_desc = call_user_func([$function->classname, $function->returns_method]); // Null means void result or result is ignored. if (!is_null($function->returns_desc) && !($function->returns_desc instanceof external_description)) { throw new coding_exception( "{$function->classname}::{$function->returns_method} did not return a valid external_description object" ); } // Now get the function description. // TODO MDL-31115 use localised lang pack descriptions, it would be nice to have // easy to understand descriptions in admin UI, // on the other hand this is still a bit in a flux and we need to find some new naming // conventions for these descriptions in lang packs. $function->description = null; $servicesfile = core_component::get_component_directory($function->component) . '/db/services.php'; if (file_exists($servicesfile)) { $functions = null; include($servicesfile); if (isset($functions[$function->name]['description'])) { $function->description = $functions[$function->name]['description']; } if (isset($functions[$function->name]['testclientpath'])) { $function->testclientpath = $functions[$function->name]['testclientpath']; } if (isset($functions[$function->name]['type'])) { $function->type = $functions[$function->name]['type']; } if (isset($functions[$function->name]['ajax'])) { $function->allowed_from_ajax = $functions[$function->name]['ajax']; } else if (method_exists($function->classname, $function->ajax_method)) { if (call_user_func([$function->classname, $function->ajax_method]) === true) { debugging('External function ' . $function->ajax_method . '() function is deprecated.' . 'Set ajax=>true in db/services.php instead.', DEBUG_DEVELOPER); $function->allowed_from_ajax = true; } } if (isset($functions[$function->name]['loginrequired'])) { $function->loginrequired = $functions[$function->name]['loginrequired']; } else { $function->loginrequired = true; } if (isset($functions[$function->name]['readonlysession'])) { $function->readonlysession = $functions[$function->name]['readonlysession']; } else { $function->readonlysession = false; } } return $function; } /** * Call an external function validating all params/returns correctly. * * Note that an external function may modify the state of the current page, so this wrapper * saves and restores tha PAGE and COURSE global variables before/after calling the external function. * * @param string $function A webservice function name. * @param array $args Params array (named params) * @param boolean $ajaxonly If true, an extra check will be peformed to see if ajax is required. * @return array containing keys for error (bool), exception and data. */ public static function call_external_function($function, $args, $ajaxonly = false) { global $PAGE, $COURSE, $CFG, $SITE; require_once("{$CFG->libdir}/pagelib.php"); $externalfunctioninfo = static::external_function_info($function); // Eventually this should shift into the various handlers and not be handled via config. $readonlysession = $externalfunctioninfo->readonlysession ?? false; if (!$readonlysession || empty($CFG->enable_read_only_sessions)) { \core\session\manager::restart_with_write_lock($readonlysession); } $currentpage = $PAGE; $currentcourse = $COURSE; $response = []; try { // Taken straight from from setup.php. if (!empty($CFG->moodlepageclass)) { if (!empty($CFG->moodlepageclassfile)) { require_once($CFG->moodlepageclassfile); } $classname = $CFG->moodlepageclass; } else { $classname = 'moodle_page'; } $PAGE = new $classname(); $COURSE = clone($SITE); if ($ajaxonly && !$externalfunctioninfo->allowed_from_ajax) { throw new moodle_exception('servicenotavailable', 'webservice'); } // Do not allow access to write or delete webservices as a public user. if ($externalfunctioninfo->loginrequired && !WS_SERVER) { if (defined('NO_MOODLE_COOKIES') && NO_MOODLE_COOKIES && !PHPUNIT_TEST) { throw new moodle_exception('servicerequireslogin', 'webservice'); } if (!isloggedin()) { throw new moodle_exception('servicerequireslogin', 'webservice'); } else { require_sesskey(); } } // Validate params, this also sorts the params properly, we need the correct order in the next part. $callable = [$externalfunctioninfo->classname, 'validate_parameters']; $params = call_user_func( $callable, $externalfunctioninfo->parameters_desc, $args ); $params = array_values($params); // Allow any Moodle plugin a chance to override this call. This is a convenient spot to // make arbitrary behaviour customisations. The overriding plugin could call the 'real' // function first and then modify the results, or it could do a completely separate // thing. $callbacks = get_plugins_with_function('override_webservice_execution'); $result = false; foreach (array_values($callbacks) as $plugins) { foreach (array_values($plugins) as $callback) { $result = $callback($externalfunctioninfo, $params); if ($result !== false) { break 2; } } } // If the function was not overridden, call the real one. if ($result === false) { $callable = [$externalfunctioninfo->classname, $externalfunctioninfo->methodname]; $result = call_user_func_array($callable, $params); } // Validate the return parameters. if ($externalfunctioninfo->returns_desc !== null) { $callable = [$externalfunctioninfo->classname, 'clean_returnvalue']; $result = call_user_func($callable, $externalfunctioninfo->returns_desc, $result); } $response['error'] = false; $response['data'] = $result; } catch (\Throwable $e) { $exception = get_exception_info($e); unset($exception->a); $exception->backtrace = format_backtrace($exception->backtrace, true); if (!debugging('', DEBUG_DEVELOPER)) { unset($exception->debuginfo); unset($exception->backtrace); } $response['error'] = true; $response['exception'] = $exception; // Do not process the remaining requests. } $PAGE = $currentpage; $COURSE = $currentcourse; return $response; } /** * Set context restriction for all following subsequent function calls. * * @param \stdClass $context the context restriction * @since Moodle 2.0 */ public static function set_context_restriction($context) { self::$contextrestriction = $context; } /** * This method has to be called before every operation * that takes a longer time to finish! * * @param int $seconds max expected time the next operation needs * @since Moodle 2.0 */ public static function set_timeout($seconds = 360) { $seconds = ($seconds < 300) ? 300 : $seconds; core_php_time_limit::raise($seconds); } /** * Validates submitted function parameters, if anything is incorrect * invalid_parameter_exception is thrown. * This is a simple recursive method which is intended to be called from * each implementation method of external API. * * @param external_description $description description of parameters * @param mixed $params the actual parameters * @return mixed params with added defaults for optional items, invalid_parameters_exception thrown if any problem found * @since Moodle 2.0 */ public static function validate_parameters(external_description $description, $params) { if ($params === null && $description->allownull == NULL_ALLOWED) { return null; } if ($description instanceof external_value) { if (is_array($params) || is_object($params)) { throw new invalid_parameter_exception('Scalar type expected, array or object received.'); } if ($description->type == PARAM_BOOL) { // Special case for PARAM_BOOL - we want true/false instead of the usual 1/0 - we can not be too strict here. if (is_bool($params) || $params === 0 || $params === 1 || $params === '0' || $params === '1') { return (bool) $params; } } $debuginfo = "Invalid external api parameter: the value is \"{$params}\", "; $debuginfo .= "the server was expecting \"{$description->type}\" type"; return validate_param($params, $description->type, $description->allownull, $debuginfo); } else if ($description instanceof external_single_structure) { if (!is_array($params)) { throw new invalid_parameter_exception( // phpcs:ignore moodle.PHP.ForbiddenFunctions.Found "Only arrays accepted. The bad value is: '" . print_r($params, true) . "'" ); } $result = []; foreach ($description->keys as $key => $subdesc) { if (!array_key_exists($key, $params)) { if ($subdesc->required == VALUE_REQUIRED) { throw new invalid_parameter_exception("Missing required key in single structure: {$key}"); } if ($subdesc->required == VALUE_DEFAULT) { try { $result[$key] = static::validate_parameters($subdesc, $subdesc->default); } catch (invalid_parameter_exception $e) { // We are only interested by exceptions returned by validate_param() and validate_parameters(). // This is used to build the path to the faulty attribute. throw new invalid_parameter_exception("{$key} => " . $e->getMessage() . ': ' . $e->debuginfo); } } } else { try { $result[$key] = static::validate_parameters($subdesc, $params[$key]); } catch (invalid_parameter_exception $e) { // We are only interested by exceptions returned by validate_param() and validate_parameters(). // This is used to build the path to the faulty attribute. throw new invalid_parameter_exception($key . " => " . $e->getMessage() . ': ' . $e->debuginfo); } } unset($params[$key]); } if (!empty($params)) { throw new invalid_parameter_exception( 'Unexpected keys (' . implode(', ', array_keys($params)) . ') detected in parameter array.' ); } return $result; } else if ($description instanceof external_multiple_structure) { if (!is_array($params)) { throw new invalid_parameter_exception( 'Only arrays accepted. The bad value is: \'' . // phpcs:ignore moodle.PHP.ForbiddenFunctions.Found print_r($params, true) . "'" ); } $result = []; foreach ($params as $param) { $result[] = static::validate_parameters($description->content, $param); } return $result; } else { throw new invalid_parameter_exception('Invalid external api description'); } } /** * Clean response * If a response attribute is unknown from the description, we just ignore the attribute. * If a response attribute is incorrect, invalid_response_exception is thrown. * Note: this function is similar to validate parameters, however it is distinct because * parameters validation must be distinct from cleaning return values. * * @param external_description $description description of the return values * @param mixed $response the actual response * @return mixed response with added defaults for optional items, invalid_response_exception thrown if any problem found * @author 2010 Jerome Mouneyrac * @since Moodle 2.0 */ public static function clean_returnvalue(external_description $description, $response) { if ($response === null && $description->allownull == NULL_ALLOWED) { return null; } if ($description instanceof external_value) { if (is_array($response) || is_object($response)) { throw new invalid_response_exception('Scalar type expected, array or object received.'); } if ($description->type == PARAM_BOOL) { // Special case for PARAM_BOOL - we want true/false instead of the usual 1/0 - we can not be too strict here. if (is_bool($response) || $response === 0 || $response === 1 || $response === '0' || $response === '1') { return (bool) $response; } } $responsetype = gettype($response); $debuginfo = "Invalid external api response: the value is \"{$response}\" of PHP type \"{$responsetype}\", "; $debuginfo .= "the server was expecting \"{$description->type}\" type"; try { return validate_param($response, $description->type, $description->allownull, $debuginfo); } catch (invalid_parameter_exception $e) { // Proper exception name, to be recursively catched to build the path to the faulty attribute. throw new invalid_response_exception($e->debuginfo); } } else if ($description instanceof external_single_structure) { if (!is_array($response) && !is_object($response)) { throw new invalid_response_exception( // phpcs:ignore moodle.PHP.ForbiddenFunctions.Found "Only arrays/objects accepted. The bad value is: '" . print_r($response, true) . "'" ); } // Cast objects into arrays. if (is_object($response)) { $response = (array) $response; } $result = []; foreach ($description->keys as $key => $subdesc) { if (!array_key_exists($key, $response)) { if ($subdesc->required == VALUE_REQUIRED) { throw new invalid_response_exception( "Error in response - Missing following required key in a single structure: {$key}" ); } if ($subdesc instanceof external_value) { if ($subdesc->required == VALUE_DEFAULT) { try { $result[$key] = static::clean_returnvalue($subdesc, $subdesc->default); } catch (invalid_response_exception $e) { // Build the path to the faulty attribute. throw new invalid_response_exception("{$key} => " . $e->getMessage() . ': ' . $e->debuginfo); } } } } else { try { $result[$key] = static::clean_returnvalue($subdesc, $response[$key]); } catch (invalid_response_exception $e) { // Build the path to the faulty attribute. throw new invalid_response_exception("{$key} => " . $e->getMessage() . ': ' . $e->debuginfo); } } unset($response[$key]); } return $result; } else if ($description instanceof external_multiple_structure) { if (!is_array($response)) { throw new invalid_response_exception( // phpcs:ignore moodle.PHP.ForbiddenFunctions.Found "Only arrays accepted. The bad value is: '" . print_r($response, true) . "'" ); } $result = []; foreach ($response as $param) { $result[] = static::clean_returnvalue($description->content, $param); } return $result; } else { throw new invalid_response_exception('Invalid external api response description'); } } /** * Makes sure user may execute functions in this context. * * @param context $context * @since Moodle 2.0 */ public static function validate_context($context) { global $PAGE; if (empty($context)) { throw new invalid_parameter_exception('Context does not exist'); } if (empty(self::$contextrestriction)) { self::$contextrestriction = context_system::instance(); } $rcontext = self::$contextrestriction; if ($rcontext->contextlevel == $context->contextlevel) { if ($rcontext->id != $context->id) { throw new restricted_context_exception(); } } else if ($rcontext->contextlevel > $context->contextlevel) { throw new restricted_context_exception(); } else { $parents = $context->get_parent_context_ids(); if (!in_array($rcontext->id, $parents)) { throw new restricted_context_exception(); } } $PAGE->reset_theme_and_output(); [, $course, $cm] = get_context_info_array($context->id); require_login($course, false, $cm, false, true); $PAGE->set_context($context); } /** * Get context from passed parameters. * The passed array must either contain a contextid or a combination of context level and instance id to fetch the context. * For example, the context level can be "course" and instanceid can be courseid. * * See context_helper::get_all_levels() for a list of valid numeric context levels, * legacy short names such as 'system', 'user', 'course' are not supported in new * plugin capabilities. * * @param array $param * @since Moodle 2.6 * @throws invalid_parameter_exception * @return context */ protected static function get_context_from_params($param) { if (!empty($param['contextid'])) { return context::instance_by_id($param['contextid'], IGNORE_MISSING); } else if (!empty($param['contextlevel']) && isset($param['instanceid'])) { // Numbers and short names are supported since Moodle 4.2. $classname = \core\context_helper::parse_external_level($param['contextlevel']); if (!$classname) { throw new invalid_parameter_exception('Invalid context level = '.$param['contextlevel']); } return $classname::instance($param['instanceid'], IGNORE_MISSING); } else { // No valid context info was found. throw new invalid_parameter_exception( 'Missing parameters, please provide either context level with instance id or contextid' ); } } /** * Returns a prepared structure to use a context parameters. * @return external_single_structure */ protected static function get_context_parameters() { $id = new external_value( PARAM_INT, 'Context ID. Either use this value, or level and instanceid.', VALUE_DEFAULT, 0 ); $level = new external_value( PARAM_ALPHANUM, // Since Moodle 4.2 numeric context level values are supported too. 'Context level. To be used with instanceid.', VALUE_DEFAULT, '' ); $instanceid = new external_value( PARAM_INT, 'Context instance ID. To be used with level', VALUE_DEFAULT, 0 ); return new external_single_structure([ 'contextid' => $id, 'contextlevel' => $level, 'instanceid' => $instanceid, ]); } } PK>\SD!!classes/external_files.phpnu[. namespace core_external; /** * External structure representing a set of files. * * @package core_external * @copyright 2016 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class external_files extends external_multiple_structure { /** * Constructor * @param string $desc Description for the multiple structure. * @param int $required The type of value (VALUE_REQUIRED OR VALUE_OPTIONAL). */ public function __construct($desc = 'List of files.', $required = VALUE_REQUIRED) { parent::__construct( new external_single_structure([ 'filename' => new external_value(PARAM_FILE, 'File name.', VALUE_OPTIONAL), 'filepath' => new external_value(PARAM_PATH, 'File path.', VALUE_OPTIONAL), 'filesize' => new external_value(PARAM_INT, 'File size.', VALUE_OPTIONAL), 'fileurl' => new external_value(PARAM_URL, 'Downloadable file url.', VALUE_OPTIONAL), 'timemodified' => new external_value(PARAM_INT, 'Time modified.', VALUE_OPTIONAL), 'mimetype' => new external_value(PARAM_RAW, 'File mime type.', VALUE_OPTIONAL), 'isexternalfile' => new external_value(PARAM_BOOL, 'Whether is an external file.', VALUE_OPTIONAL), 'repositorytype' => new external_value(PARAM_PLUGIN, 'The repository type for external files.', VALUE_OPTIONAL), 'icon' => new external_value(PARAM_RAW, 'The relative path to the relevant file type icon based on the file\'s mime type.', VALUE_OPTIONAL), ], 'File.'), $desc, $required, ); } /** * Return the properties ready to be used by an exporter. * * @return array properties * @since Moodle 3.3 */ public static function get_properties_for_exporter() { return [ 'filename' => [ 'type' => PARAM_FILE, 'description' => 'File name.', 'optional' => true, 'null' => NULL_NOT_ALLOWED, ], 'filepath' => [ 'type' => PARAM_PATH, 'description' => 'File path.', 'optional' => true, 'null' => NULL_NOT_ALLOWED, ], 'filesize' => [ 'type' => PARAM_INT, 'description' => 'File size.', 'optional' => true, 'null' => NULL_NOT_ALLOWED, ], 'fileurl' => [ 'type' => PARAM_URL, 'description' => 'Downloadable file url.', 'optional' => true, 'null' => NULL_NOT_ALLOWED, ], 'timemodified' => [ 'type' => PARAM_INT, 'description' => 'Time modified.', 'optional' => true, 'null' => NULL_NOT_ALLOWED, ], 'mimetype' => [ 'type' => PARAM_RAW, 'description' => 'File mime type.', 'optional' => true, 'null' => NULL_NOT_ALLOWED, ], 'isexternalfile' => [ 'type' => PARAM_BOOL, 'description' => 'Whether is an external file.', 'optional' => true, 'null' => NULL_NOT_ALLOWED, ], 'repositorytype' => [ 'type' => PARAM_PLUGIN, 'description' => 'The repository type for the external files.', 'optional' => true, 'null' => NULL_ALLOWED, ], 'icon' => [ 'type' => PARAM_RAW, 'description' => 'Relative path to the relevant file type icon based on the file\'s mime type.', 'optional' => true, 'null' => NULL_ALLOWED, ], ]; } } PK>\VV(classes/external_function_parameters.phpnu[. namespace core_external; /** * Description of top level - PHP function parameters. * * @package core_external * @copyright 2009 Petr Skodak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class external_function_parameters extends external_single_structure { /** * Constructor - does extra checking to prevent top level optional parameters. * * @param array $keys * @param string $desc * @param int $required * @param array $default */ public function __construct( array $keys, $desc = '', $required = VALUE_REQUIRED, $default = null ) { global $CFG; if ($CFG->debugdeveloper) { foreach (array_values($keys) as $value) { if ($value instanceof external_value) { if ($value->required == VALUE_OPTIONAL) { debugging('External function parameters: invalid OPTIONAL value specified.', DEBUG_DEVELOPER); break; } } } } parent::__construct($keys, $desc, $required, $default, NULL_NOT_ALLOWED); } } PK>\eTTclasses/external_warnings.phpnu[. namespace core_external; /** * Standard Moodle web service warnings. * * @package core_external * @copyright 2012 Jerome Mouneyrac * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class external_warnings extends external_multiple_structure { /** * Constructor * * @param string $itemdesc * @param string $itemiddesc * @param string $warningcodedesc */ public function __construct( $itemdesc = 'item', $itemiddesc = 'item id', $warningcodedesc = 'the warning code can be used by the client app to implement specific behaviour' ) { parent::__construct( new external_single_structure([ 'item' => new external_value(PARAM_TEXT, $itemdesc, VALUE_OPTIONAL), 'itemid' => new external_value(PARAM_INT, $itemiddesc, VALUE_OPTIONAL), 'warningcode' => new external_value(PARAM_ALPHANUM, $warningcodedesc), 'message' => new external_value(PARAM_RAW, 'untranslated english message to explain the warning'), ], 'warning'), 'list of warnings', VALUE_OPTIONAL ); } } PK>\Pd(classes/restricted_context_exception.phpnu[. namespace core_external; /** * Exception indicating user is not allowed to use external function in the current context. * * @package core_external * @copyright 2009 Petr Skodak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restricted_context_exception extends \moodle_exception { /** * Constructor */ public function __construct() { parent::__construct('restrictedcontextexception', 'error'); } } PK>\ǭCkCkclasses/util.phpnu[. namespace core_external; use context; use context_course; use context_helper; use context_system; use core_user; use moodle_exception; use moodle_url; use stdClass; /** * Utility functions for the external API. * * @package core_webservice * @copyright 2015 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class util { /** * Validate a list of courses, returning the complete course objects for valid courses. * * Each course has an additional 'contextvalidated' field, this will be set to true unless * you set $keepfails, in which case it will be false if validation fails for a course. * * @param array $courseids A list of course ids * @param array $courses An array of courses already pre-fetched, indexed by course id. * @param bool $addcontext True if the returned course object should include the full context object. * @param bool $keepfails True to keep all the course objects even if validation fails * @return array An array of courses and the validation warnings */ public static function validate_courses( $courseids, $courses = [], $addcontext = false, $keepfails = false ) { global $DB; // Delete duplicates. $courseids = array_unique($courseids); $warnings = []; // Remove courses which are not even requested. $courses = array_intersect_key($courses, array_flip($courseids)); // For any courses NOT loaded already, get them in a single query (and preload contexts) // for performance. Preserve ordering because some tests depend on it. $newcourseids = []; foreach ($courseids as $cid) { if (!array_key_exists($cid, $courses)) { $newcourseids[] = $cid; } } if ($newcourseids) { [$listsql, $listparams] = $DB->get_in_or_equal($newcourseids); // Load list of courses, and preload associated contexts. $contextselect = context_helper::get_preload_record_columns_sql('x'); $newcourses = $DB->get_records_sql( " SELECT c.*, $contextselect FROM {course} c JOIN {context} x ON x.instanceid = c.id WHERE x.contextlevel = ? AND c.id $listsql", array_merge([CONTEXT_COURSE], $listparams) ); foreach ($newcourseids as $cid) { if (array_key_exists($cid, $newcourses)) { $course = $newcourses[$cid]; context_helper::preload_from_record($course); $courses[$course->id] = $course; } } } foreach ($courseids as $cid) { // Check the user can function in this context. try { $context = context_course::instance($cid); external_api::validate_context($context); if ($addcontext) { $courses[$cid]->context = $context; } $courses[$cid]->contextvalidated = true; } catch (\Exception $e) { if ($keepfails) { $courses[$cid]->contextvalidated = false; } else { unset($courses[$cid]); } $warnings[] = [ 'item' => 'course', 'itemid' => $cid, 'warningcode' => '1', 'message' => 'No access rights in course context', ]; } } return [$courses, $warnings]; } /** * Returns all area files (optionally limited by itemid). * * @param int $contextid context ID * @param string $component component * @param string $filearea file area * @param int $itemid item ID or all files if not specified * @param bool $useitemidinurl wether to use the item id in the file URL (modules intro don't use it) * @return array of files, compatible with the external_files structure. * @since Moodle 3.2 */ public static function get_area_files($contextid, $component, $filearea, $itemid = false, $useitemidinurl = true) { $files = []; $fs = get_file_storage(); if ($areafiles = $fs->get_area_files($contextid, $component, $filearea, $itemid, 'itemid, filepath, filename', false)) { foreach ($areafiles as $areafile) { $file = []; $file['filename'] = $areafile->get_filename(); $file['filepath'] = $areafile->get_filepath(); $file['mimetype'] = $areafile->get_mimetype(); $file['filesize'] = $areafile->get_filesize(); $file['timemodified'] = $areafile->get_timemodified(); $file['isexternalfile'] = $areafile->is_external_file(); if ($file['isexternalfile']) { $file['repositorytype'] = $areafile->get_repository_type(); } $fileitemid = $useitemidinurl ? $areafile->get_itemid() : null; // If AJAX request, generate a standard plugin file url. if (AJAX_SCRIPT) { $fileurl = moodle_url::make_pluginfile_url( $contextid, $component, $filearea, $fileitemid, $areafile->get_filepath(), $areafile->get_filename() ); } else { // Otherwise, generate a webservice plugin file url. $fileurl = moodle_url::make_webservice_pluginfile_url( $contextid, $component, $filearea, $fileitemid, $areafile->get_filepath(), $areafile->get_filename() ); } $file['fileurl'] = $fileurl->out(false); $file['icon'] = file_file_icon($areafile); $files[] = $file; } } return $files; } /** * Create and return a session linked token. Token to be used for html embedded client apps that want to communicate * with the Moodle server through web services. The token is linked to the current session for the current page request. * It is expected this will be called in the script generating the html page that is embedding the client app and that the * returned token will be somehow passed into the client app being embedded in the page. * * @param int $tokentype EXTERNAL_TOKEN_EMBEDDED|EXTERNAL_TOKEN_PERMANENT * @param stdClass $service service linked to the token * @param int $userid user linked to the token * @param context $context * @param int $validuntil date when the token expired * @param string $iprestriction allowed ip - if 0 or empty then all ips are allowed * @param string $name token name as a note or token identity at the table view. * @return string generated token */ public static function generate_token( int $tokentype, stdClass $service, int $userid, context $context, int $validuntil = 0, string $iprestriction = '', string $name = '' ): string { global $DB, $USER, $SESSION; // Make sure the token doesn't exist (even if it should be almost impossible with the random generation). $numtries = 0; do { $numtries++; $generatedtoken = md5(uniqid((string) rand(), true)); if ($numtries > 5) { throw new moodle_exception('tokengenerationfailed'); } } while ($DB->record_exists('external_tokens', ['token' => $generatedtoken])); $newtoken = (object) [ 'token' => $generatedtoken, ]; if (empty($service->requiredcapability) || has_capability($service->requiredcapability, $context, $userid)) { $newtoken->externalserviceid = $service->id; } else { throw new moodle_exception('nocapabilitytousethisservice'); } $newtoken->tokentype = $tokentype; $newtoken->userid = $userid; if ($tokentype == EXTERNAL_TOKEN_EMBEDDED) { $newtoken->sid = session_id(); } $newtoken->contextid = $context->id; $newtoken->creatorid = $USER->id; $newtoken->timecreated = time(); $newtoken->validuntil = $validuntil; if (!empty($iprestriction)) { $newtoken->iprestriction = $iprestriction; } // Generate the private token, it must be transmitted only via https. $newtoken->privatetoken = random_string(64); if (!$name) { // Generate a token name. $name = self::generate_token_name(); } $newtoken->name = $name; $tokenid = $DB->insert_record('external_tokens', $newtoken); // Create new session to hold newly created token ID. $SESSION->webservicenewlycreatedtoken = $tokenid; return $newtoken->token; } /** * Get a service by its id. * * @param int $serviceid * @return stdClass */ public static function get_service_by_id(int $serviceid): stdClass { global $DB; return $DB->get_record('external_services', ['id' => $serviceid], '*', MUST_EXIST); } /** * Get a service by its name. * * @param string $name The service name. * @return stdClass */ public static function get_service_by_name(string $name): stdClass { global $DB; return $DB->get_record('external_services', ['name' => $name], '*', MUST_EXIST); } /** * Set the last time a token was sent and trigger the \core\event\webservice_token_sent event. * * This function is used when a token is generated by the user via login/token.php or admin/tool/mobile/launch.php. * In order to protect the privatetoken, we remove it from the event params. * * @param stdClass $token token object */ public static function log_token_request(stdClass $token): void { global $DB, $USER; $token->privatetoken = null; // Log token access. $DB->set_field('external_tokens', 'lastaccess', time(), ['id' => $token->id]); $event = \core\event\webservice_token_sent::create([ 'objectid' => $token->id, ]); $event->add_record_snapshot('external_tokens', $token); $event->trigger(); // Check if we need to notify the user about the new login via token. $loginip = getremoteaddr(); if ($USER->lastip === $loginip) { return; } $shouldskip = WS_SERVER || CLI_SCRIPT || !NO_MOODLE_COOKIES; if ($shouldskip && !PHPUNIT_TEST) { return; } // Schedule adhoc task to sent a login notification to the user. $task = new \core\task\send_login_notifications(); $task->set_userid($USER->id); $logintime = time(); $task->set_custom_data([ 'useragent' => \core_useragent::get_user_agent_string(), 'ismoodleapp' => \core_useragent::is_moodle_app(), 'loginip' => $loginip, 'logintime' => $logintime, ]); $task->set_component('core'); // We need sometime so the mobile app will send to Moodle the device information after login. $task->set_next_run_time(time() + (2 * MINSECS)); \core\task\manager::reschedule_or_queue_adhoc_task($task); } /** * Generate or return an existing token for the current authenticated user. * This function is used for creating a valid token for users authenticathing via places, including: * - login/token.php * - admin/tool/mobile/launch.php. * * @param stdClass $service external service object * @return stdClass token object * @throws moodle_exception */ public static function generate_token_for_current_user(stdClass $service) { global $DB, $USER, $CFG; core_user::require_active_user($USER, true, true); // Check if there is any required system capability. if ($service->requiredcapability && !has_capability($service->requiredcapability, context_system::instance())) { throw new moodle_exception('missingrequiredcapability', 'webservice', '', $service->requiredcapability); } // Specific checks related to user restricted service. if ($service->restrictedusers) { $authoriseduser = $DB->get_record('external_services_users', [ 'externalserviceid' => $service->id, 'userid' => $USER->id, ]); if (empty($authoriseduser)) { throw new moodle_exception('usernotallowed', 'webservice', '', $service->shortname); } if (!empty($authoriseduser->validuntil) && $authoriseduser->validuntil < time()) { throw new moodle_exception('invalidtimedtoken', 'webservice'); } if (!empty($authoriseduser->iprestriction) && !address_in_subnet(getremoteaddr(), $authoriseduser->iprestriction)) { throw new moodle_exception('invalidiptoken', 'webservice'); } } // Check if a token has already been created for this user and this service. $conditions = [ 'userid' => $USER->id, 'externalserviceid' => $service->id, 'tokentype' => EXTERNAL_TOKEN_PERMANENT, ]; $tokens = $DB->get_records('external_tokens', $conditions, 'timecreated ASC'); // A bit of sanity checks. foreach ($tokens as $key => $token) { // Checks related to a specific token. (script execution continue). $unsettoken = false; // If sid is set then there must be a valid associated session no matter the token type. if (!empty($token->sid)) { if (!\core\session\manager::session_exists($token->sid)) { // This token will never be valid anymore, delete it. $DB->delete_records('external_tokens', ['sid' => $token->sid]); $unsettoken = true; } } // Remove token is not valid anymore. if (!empty($token->validuntil) && $token->validuntil < time()) { $DB->delete_records('external_tokens', ['token' => $token->token, 'tokentype' => EXTERNAL_TOKEN_PERMANENT]); $unsettoken = true; } // Remove token if its IP is restricted. if (isset($token->iprestriction) && !address_in_subnet(getremoteaddr(), $token->iprestriction)) { $unsettoken = true; } if ($unsettoken) { unset($tokens[$key]); } } // If some valid tokens exist then use the most recent. if (count($tokens) > 0) { $token = array_pop($tokens); } else { $context = context_system::instance(); $isofficialservice = $service->shortname == MOODLE_OFFICIAL_MOBILE_SERVICE; if ( ($isofficialservice && has_capability('moodle/webservice:createmobiletoken', $context)) || (!is_siteadmin($USER) && has_capability('moodle/webservice:createtoken', $context)) ) { // Create a new token. $token = new stdClass(); $token->token = md5(uniqid((string) rand(), true)); $token->userid = $USER->id; $token->tokentype = EXTERNAL_TOKEN_PERMANENT; $token->contextid = context_system::instance()->id; $token->creatorid = $USER->id; $token->timecreated = time(); $token->externalserviceid = $service->id; // By default tokens are valid for 12 weeks. $token->validuntil = $token->timecreated + $CFG->tokenduration; $token->iprestriction = null; $token->sid = null; $token->lastaccess = null; $token->name = self::generate_token_name(); // Generate the private token, it must be transmitted only via https. $token->privatetoken = random_string(64); $token->id = $DB->insert_record('external_tokens', $token); $eventtoken = clone $token; $eventtoken->privatetoken = null; $params = [ 'objectid' => $eventtoken->id, 'relateduserid' => $USER->id, 'other' => [ 'auto' => true, ], ]; $event = \core\event\webservice_token_created::create($params); $event->add_record_snapshot('external_tokens', $eventtoken); $event->trigger(); } else { throw new moodle_exception('cannotcreatetoken', 'webservice', '', $service->shortname); } } return $token; } /** * Format the string to be returned properly as requested by the either the web service server, * either by an internally call. * The caller can change the format (raw) with the settings singleton * All web service servers must set this singleton when parsing the $_GET and $_POST. * *
     * Options are the same that in {@see format_string()} with some changes:
     *      filter      : Can be set to false to force filters off, else observes {@see settings}.
     * 
* * @param string|null $content The string to be filtered. Should be plain text, expect * possibly for multilang tags. * @param int|context $context The id of the context for the string or the context (affects filters). * @param boolean $striplinks To strip any link in the result text. Moodle 1.8 default changed from false to true! MDL-8713 * @param array $options options array/object or courseid * @return string text */ public static function format_string( $content, $context, $striplinks = true, $options = [] ) { if ($content === null || $content === '') { // Nothing to return. // Note: It's common for the DB to return null, so we allow format_string to take a null, // even though it is counter-intuitive. return ''; } // Get settings (singleton). $settings = external_settings::get_instance(); if (!$settings->get_raw()) { $options['context'] = $context; $options['filter'] = isset($options['filter']) && !$options['filter'] ? false : $settings->get_filter(); return format_string($content, $striplinks, $options); } return $content; } /** * Format the text to be returned properly as requested by the either the web service server, * either by an internally call. * The caller can change the format (raw, filter, file, fileurl) with the \core_external\settings singleton * All web service servers must set this singleton when parsing the $_GET and $_POST. * *
     * Options are the same that in {@see format_text()} with some changes in defaults to provide backwards compatibility:
     *      trusted     :   If true the string won't be cleaned. Default false.
     *      noclean     :   If true the string won't be cleaned only if trusted is also true. Default false.
     *      nocache     :   If true the string will not be cached and will be formatted every call. Default false.
     *      filter      :   Can be set to false to force filters off, else observes {@see \core_external\settings}.
     *      para        :   If true then the returned string will be wrapped in div tags.
     *                      Default (different from format_text) false.
     *                      Default changed because div tags are not commonly needed.
     *      newlines    :   If true then lines newline breaks will be converted to HTML newline breaks. Default true.
     *      context     :   Not used! Using contextid parameter instead.
     *      overflowdiv :   If set to true the formatted text will be encased in a div with the class no-overflow before being
     *                      returned. Default false.
     *      allowid     :   If true then id attributes will not be removed, even when using htmlpurifier. Default (different from
     *                      format_text) true. Default changed id attributes are commonly needed.
     *      blanktarget :   If true all  tags will have target="_blank" added unless target is explicitly specified.
     * 
* * @param string|null $text The content that may contain ULRs in need of rewriting. * @param string|int|null $textformat The text format. * @param context $context This parameter and the next two identify the file area to use. * @param string|null $component * @param string|null $filearea helps identify the file area. * @param int|string|null $itemid helps identify the file area. * @param array|stdClass|null $options text formatting options * @return array text + textformat */ public static function format_text( $text, $textformat, $context, $component = null, $filearea = null, $itemid = null, $options = null ) { global $CFG; if ($text === null || $text === '') { // Nothing to return. // Note: It's common for the DB to return null, so we allow format_string to take nulls, // even though it is counter-intuitive. return ['', $textformat ?? FORMAT_MOODLE]; } if ($itemid !== null) { $itemid = (int) $itemid; } // Get settings (singleton). $settings = external_settings::get_instance(); if ($component && $filearea && $settings->get_fileurl()) { require_once($CFG->libdir . "/filelib.php"); $text = file_rewrite_pluginfile_urls($text, $settings->get_file(), $context->id, $component, $filearea, $itemid); } // Note that $CFG->forceclean does not apply here if the client requests for the raw database content. // This is consistent with web clients that are still able to load non-cleaned text into editors, too. if (!$settings->get_raw()) { $options = (array) $options; // If context is passed in options, check that is the same to show a debug message. if (isset($options['context'])) { if (is_int($options['context'])) { if ($options['context'] != $context->id) { debugging( 'Different contexts found in external_format_text parameters. $options[\'context\'] not allowed. ' . 'Using $contextid parameter...', DEBUG_DEVELOPER ); } } else if ($options['context'] instanceof context) { if ($options['context']->id != $context->id) { debugging( 'Different contexts found in external_format_text parameters. $options[\'context\'] not allowed. ' . 'Using $contextid parameter...', DEBUG_DEVELOPER ); } } } $options['filter'] = isset($options['filter']) && !$options['filter'] ? false : $settings->get_filter(); $options['para'] = isset($options['para']) ? $options['para'] : false; $options['context'] = $context; $options['allowid'] = isset($options['allowid']) ? $options['allowid'] : true; $text = format_text($text, $textformat, $options); // Once converted to html (from markdown, plain... lets inform consumer this is already HTML). $textformat = FORMAT_HTML; } // Note: The formats defined in weblib are strings. return [$text, $textformat]; } /** * Validate text field format against known FORMAT_XXX * * @param string $format the format to validate * @return string the validated format * @throws \moodle_exception * @since Moodle 2.3 */ public static function validate_format($format) { $allowedformats = [FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN]; if (!in_array($format, $allowedformats)) { throw new moodle_exception( 'formatnotsupported', 'webservice', '', null, "The format with value={$format} is not supported by this Moodle site" ); } return $format; } /** * Delete all pre-built services, related tokens, and external functions information defined for the specified component. * * @param string $component The frankenstyle component name */ public static function delete_service_descriptions(string $component): void { global $DB; $params = [$component]; $DB->delete_records_select( 'external_tokens', "externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?)", $params ); $DB->delete_records_select( 'external_services_users', "externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?)", $params ); $DB->delete_records_select( 'external_services_functions', "functionname IN (SELECT name FROM {external_functions} WHERE component = ?)", $params ); $DB->delete_records('external_services', ['component' => $component]); $DB->delete_records('external_functions', ['component' => $component]); } /** * Generate token name. * * @return string */ public static function generate_token_name(): string { return get_string( 'tokennameprefix', 'webservice', random_string(5) ); } } PK>\hHHclasses/external_settings.phpnu[. namespace core_external; /** * Singleton to handle the external settings.. * * We use singleton to encapsulate the "logic". * * @package core_external * @copyright 2012 Jerome Mouneyrac * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class external_settings { /** @var settings|null the singleton instance */ public static $instance = null; /** @var bool Should the external function return raw text or formatted */ private $raw = false; /** @var bool Should the external function filter the text */ private $filter = false; /** @var bool Should the external function rewrite plugin file url */ private $fileurl = true; /** @var string In which file should the urls be rewritten */ private $file = 'webservice/pluginfile.php'; /** @var string The session lang */ private $lang = ''; /** @var string The timezone to use during this WS request */ private $timezone = ''; /** * Constructor - protected - can not be instanciated */ protected function __construct() { if ((AJAX_SCRIPT == false) && (CLI_SCRIPT == false) && (WS_SERVER == false)) { // For normal pages, the default should match the default for format_text. $this->filter = true; // Use pluginfile.php for web requests. $this->file = 'pluginfile.php'; } } /** * Return only one instance * * @return self */ public static function get_instance() { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } /** * Reset the singleton instance. */ public static function reset(): void { self::$instance = null; } /** * Set raw * * @param boolean $raw */ public function set_raw($raw) { $this->raw = $raw; } /** * Get raw * * @return boolean */ public function get_raw() { return $this->raw; } /** * Set filter * * @param boolean $filter */ public function set_filter($filter) { $this->filter = $filter; } /** * Get filter * * @return boolean */ public function get_filter() { return $this->filter; } /** * Set fileurl * * @param bool $fileurl */ public function set_fileurl($fileurl) { $this->fileurl = $fileurl; } /** * Get fileurl * * @return bool */ public function get_fileurl() { return $this->fileurl; } /** * Set file * * @param string $file */ public function set_file($file) { $this->file = $file; } /** * Get file * * @return string */ public function get_file() { return $this->file; } /** * Set lang * * @param string $lang */ public function set_lang($lang) { $this->lang = $lang; } /** * Get lang * * @return string */ public function get_lang() { return $this->lang; } /** * Set timezone * * @param string $timezone */ public function set_timezone($timezone) { $this->timezone = $timezone; } /** * Get timezone * * @return string */ public function get_timezone() { return $this->timezone; } } PK>\ D classes/external_value.phpnu[. namespace core_external; /** * Scalar value description class. * * @package core_external * @copyright 2009 Petr Skodak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class external_value extends external_description { /** @var mixed Value type PARAM_XX */ public $type; /** * Constructor for the external_value class. * * @param mixed $type * @param string $desc * @param int $required * @param mixed $default * @param bool $allownull */ public function __construct( $type, $desc = '', $required = VALUE_REQUIRED, $default = null, $allownull = NULL_ALLOWED ) { parent::__construct($desc, $required, $default, $allownull); $this->type = $type; } } PK>\: : !classes/external_format_value.phpnu[. namespace core_external; /** * A pre-filled external_value class for text format. * * Default is FORMAT_HTML * This should be used all the time in external xxx_params()/xxx_returns functions * as it is the standard way to implement text format param/return values. * * @package core_webservice * @copyright 2012 Jerome Mouneyrac * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.3 */ class external_format_value extends external_value { /** * Constructor * * @param string $textfieldname Name of the text field * @param int $required if VALUE_REQUIRED then set standard default FORMAT_HTML * @param int $default Default value. */ public function __construct($textfieldname, $required = VALUE_REQUIRED, $default = null) { // Make sure the default format's value is correct. if ($default !== null && !in_array($default, [FORMAT_MOODLE, FORMAT_HTML, FORMAT_PLAIN, FORMAT_MARKDOWN])) { debugging("Invalid default format for $textfieldname: $default. " . "It must be either FORMAT_MOODLE, FORMAT_HTML, FORMAT_PLAIN, or FORMAT_MARKDOWN.", DEBUG_DEVELOPER); $default = null; } if ($default == null && $required == VALUE_DEFAULT) { $default = FORMAT_HTML; } $desc = sprintf( "%s format (%s = HTML, %s = MOODLE, %s = PLAIN, or %s = MARKDOWN)", $textfieldname, FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN, ); parent::__construct(PARAM_INT, $desc, $required, $default); } } PK>\"O߶ classes/external_description.phpnu[. namespace core_external; /** * Common ancestor of all parameter description classes * * @package core_external * @copyright 2009 Petr Skodak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class external_description { /** @var string Description of element */ public $desc; /** @var bool Element value required, null not allowed */ public $required; /** @var mixed Default value */ public $default; /** @var bool Allow null values */ public $allownull; /** * Contructor. * * @param string $desc Description of element * @param int $required Whether the element value is required. Valid values are VALUE_DEFAULT, VALUE_REQUIRED, VALUE_OPTIONAL. * @param mixed $default The default value * @param bool $allownull Allow null value */ public function __construct($desc, $required, $default, $allownull = NULL_NOT_ALLOWED) { if (!in_array($required, [VALUE_DEFAULT, VALUE_REQUIRED, VALUE_OPTIONAL], true)) { $requiredstr = $required; if (is_array($required)) { $requiredstr = "Array: " . implode(" ", $required); } debugging("Invalid \$required parameter value: '{$requiredstr}' . It must be either VALUE_DEFAULT, VALUE_REQUIRED, or VALUE_OPTIONAL", DEBUG_DEVELOPER); } $this->desc = $desc; $this->required = $required; $this->default = $default; $this->allownull = (bool)$allownull; } } PK>\O   UPGRADING.mdnu[# core_external (subsystem) Upgrade notes ## 4.5 ### Changed - The external function `core_webservice_external::get_site_info` now returns the default home page URL when needed. For more information see [MDL-82844](https://tracker.moodle.org/browse/MDL-82844) PK>\H\\tests/external_api_test.phpnu[. namespace core_external; use core\tests\fake_plugins_test_trait; /** * Unit tests for core_external\external_api. * * @package core_external * @category test * @copyright 2022 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @covers \core_external\external_api */ final class external_api_test extends \advanced_testcase { use fake_plugins_test_trait; /** * Test the validate_parameters method. * * @covers \core_external\external_api::validate_parameters */ public function test_validate_params(): void { $params = ['text' => 'aaa', 'someid' => '6']; $description = new external_function_parameters([ 'someid' => new external_value(PARAM_INT, 'Some int value'), 'text' => new external_value(PARAM_ALPHA, 'Some text value'), ]); $result = external_api::validate_parameters($description, $params); $this->assertCount(2, $result); reset($result); $this->assertSame('someid', key($result)); $this->assertSame(6, $result['someid']); $this->assertSame('aaa', $result['text']); $params = [ 'someids' => ['1', 2, 'a' => '3'], 'scalar' => 666, ]; $description = new external_function_parameters([ 'someids' => new external_multiple_structure(new external_value(PARAM_INT, 'Some ID')), 'scalar' => new external_value(PARAM_ALPHANUM, 'Some text value'), ]); $result = external_api::validate_parameters($description, $params); $this->assertCount(2, $result); reset($result); $this->assertSame('someids', key($result)); $this->assertEquals([0 => 1, 1 => 2, 2 => 3], $result['someids']); $this->assertSame('666', $result['scalar']); $params = ['text' => 'aaa']; $description = new external_function_parameters([ 'someid' => new external_value(PARAM_INT, 'Some int value', VALUE_DEFAULT), 'text' => new external_value(PARAM_ALPHA, 'Some text value'), ]); $result = external_api::validate_parameters($description, $params); $this->assertCount(2, $result); reset($result); $this->assertSame('someid', key($result)); $this->assertNull($result['someid']); $this->assertSame('aaa', $result['text']); $params = ['text' => 'aaa']; $description = new external_function_parameters([ 'someid' => new external_value(PARAM_INT, 'Some int value', VALUE_DEFAULT, 6), 'text' => new external_value(PARAM_ALPHA, 'Some text value'), ]); $result = external_api::validate_parameters($description, $params); $this->assertCount(2, $result); reset($result); $this->assertSame('someid', key($result)); $this->assertSame(6, $result['someid']); $this->assertSame('aaa', $result['text']); // Missing required value (an exception is thrown). $testdata = []; try { external_api::clean_returnvalue($description, $testdata); $this->fail('Exception expected'); } catch (\moodle_exception $ex) { $this->assertInstanceOf(\invalid_response_exception::class, $ex); $this->assertSame('Invalid response value detected (Error in response - ' . 'Missing following required key in a single structure: text)', $ex->getMessage()); } // Test nullable external_value may optionally return data. $description = new external_function_parameters([ 'value' => new external_value(PARAM_INT, '', VALUE_REQUIRED, null, NULL_ALLOWED) ]); $testdata = ['value' => null]; $cleanedvalue = external_api::clean_returnvalue($description, $testdata); $this->assertSame($testdata, $cleanedvalue); $testdata = ['value' => 1]; $cleanedvalue = external_api::clean_returnvalue($description, $testdata); $this->assertSame($testdata, $cleanedvalue); // Test nullable external_single_structure may optionally return data. $description = new external_function_parameters([ 'value' => new external_single_structure(['value2' => new external_value(PARAM_INT)], '', VALUE_REQUIRED, null, NULL_ALLOWED) ]); $testdata = ['value' => null]; $cleanedvalue = external_api::clean_returnvalue($description, $testdata); $this->assertSame($testdata, $cleanedvalue); $testdata = ['value' => ['value2' => 1]]; $cleanedvalue = external_api::clean_returnvalue($description, $testdata); $this->assertSame($testdata, $cleanedvalue); // Test nullable external_multiple_structure may optionally return data. $description = new external_function_parameters([ 'value' => new external_multiple_structure( new external_value(PARAM_INT), '', VALUE_REQUIRED, null, NULL_ALLOWED) ]); $testdata = ['value' => null]; $cleanedvalue = external_api::clean_returnvalue($description, $testdata); $this->assertSame($testdata, $cleanedvalue); $testdata = ['value' => [1]]; $cleanedvalue = external_api::clean_returnvalue($description, $testdata); $this->assertSame($testdata, $cleanedvalue); } /** * Test for clean_returnvalue() for testing that returns the PHP type. * * @covers \core_external\external_api::clean_returnvalue */ public function test_clean_returnvalue_return_php_type(): void { $returndesc = new external_single_structure([ 'value' => new external_value(PARAM_RAW, 'Some text', VALUE_OPTIONAL, null, NULL_NOT_ALLOWED), ]); // Check return type on exception because the external values does not allow NULL values. $testdata = ['value' => null]; try { $cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata); } catch (\moodle_exception $e) { $this->assertInstanceOf(\invalid_response_exception::class, $e); $this->assertStringContainsString('of PHP type "NULL"', $e->debuginfo); } } /** * Test for clean_returnvalue(). * * @covers \core_external\external_api::clean_returnvalue */ public function test_clean_returnvalue(): void { // Build some return value decription. $returndesc = new external_multiple_structure( new external_single_structure( [ 'object' => new external_single_structure( ['value1' => new external_value(PARAM_INT, 'this is a int')]), 'value2' => new external_value(PARAM_TEXT, 'some text', VALUE_OPTIONAL), ] )); // Clean an object (it should be cast into an array). $object = new \stdClass(); $object->value1 = 1; $singlestructure['object'] = $object; $singlestructure['value2'] = 'Some text'; $testdata = [$singlestructure]; $cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata); $cleanedsinglestructure = array_pop($cleanedvalue); $this->assertSame($object->value1, $cleanedsinglestructure['object']['value1']); $this->assertSame($singlestructure['value2'], $cleanedsinglestructure['value2']); // Missing VALUE_OPTIONAL. $object = new \stdClass(); $object->value1 = 1; $singlestructure = new \stdClass(); $singlestructure->object = $object; $testdata = [$singlestructure]; $cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata); $cleanedsinglestructure = array_pop($cleanedvalue); $this->assertSame($object->value1, $cleanedsinglestructure['object']['value1']); $this->assertArrayNotHasKey('value2', $cleanedsinglestructure); // Unknown attribute (the value should be ignored). $object = []; $object['value1'] = 1; $singlestructure = []; $singlestructure['object'] = $object; $singlestructure['value2'] = 'Some text'; $singlestructure['unknownvalue'] = 'Some text to ignore'; $testdata = [$singlestructure]; $cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata); $cleanedsinglestructure = array_pop($cleanedvalue); $this->assertSame($object['value1'], $cleanedsinglestructure['object']['value1']); $this->assertSame($singlestructure['value2'], $cleanedsinglestructure['value2']); $this->assertArrayNotHasKey('unknownvalue', $cleanedsinglestructure); // Missing required value (an exception is thrown). $object = []; $singlestructure = []; $singlestructure['object'] = $object; $singlestructure['value2'] = 'Some text'; $testdata = [$singlestructure]; try { external_api::clean_returnvalue($returndesc, $testdata); $this->fail('Exception expected'); } catch (\moodle_exception $ex) { $this->assertInstanceOf(\invalid_response_exception::class, $ex); $this->assertSame('Invalid response value detected (object => Invalid response value detected ' . '(Error in response - Missing following required key in a single structure: value1): Error in response - ' . 'Missing following required key in a single structure: value1)', $ex->getMessage()); } // Fail if no data provided when value required. $testdata = null; try { external_api::clean_returnvalue($returndesc, $testdata); $this->fail('Exception expected'); } catch (\moodle_exception $ex) { $this->assertInstanceOf(\invalid_response_exception::class, $ex); $this->assertSame('Invalid response value detected (Only arrays accepted. The bad value is: \'\')', $ex->getMessage()); } // Test nullable external_multiple_structure may optionally return data. $returndesc = new external_multiple_structure( new external_value(PARAM_INT), '', VALUE_REQUIRED, null, NULL_ALLOWED); $testdata = null; $cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata); $this->assertSame($testdata, $cleanedvalue); $testdata = [1]; $cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata); $this->assertSame($testdata, $cleanedvalue); // Test nullable external_single_structure may optionally return data. $returndesc = new external_single_structure(['value' => new external_value(PARAM_INT)], '', VALUE_REQUIRED, null, NULL_ALLOWED); $testdata = null; $cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata); $this->assertSame($testdata, $cleanedvalue); $testdata = ['value' => 1]; $cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata); $this->assertSame($testdata, $cleanedvalue); // Test nullable external_value may optionally return data. $returndesc = new external_value(PARAM_INT, '', VALUE_REQUIRED, null, NULL_ALLOWED); $testdata = null; $cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata); $this->assertSame($testdata, $cleanedvalue); $testdata = 1; $cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata); $this->assertSame($testdata, $cleanedvalue); } /** * Test \core_external\external_api::get_context_from_params(). * * @covers \core_external\external_api::get_context_from_params */ public function test_get_context_from_params(): void { $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $realcontext = \context_course::instance($course->id); // Use context id. $fetchedcontext = $this->get_context_from_params(["contextid" => $realcontext->id]); $this->assertEquals($realcontext, $fetchedcontext); // Use context level and instance id. $fetchedcontext = $this->get_context_from_params(["contextlevel" => "course", "instanceid" => $course->id]); $this->assertEquals($realcontext, $fetchedcontext); // Use context level numbers instead of legacy short level names. $fetchedcontext = $this->get_context_from_params( ["contextlevel" => \core\context\course::LEVEL, "instanceid" => $course->id]); $this->assertEquals($realcontext, $fetchedcontext); // Passing empty values. try { $fetchedcontext = $this->get_context_from_params(["contextid" => 0]); $this->fail('Exception expected from get_context_wrapper()'); } catch (\moodle_exception $e) { $this->assertInstanceOf(\invalid_parameter_exception::class, $e); } try { $fetchedcontext = $this->get_context_from_params(["instanceid" => 0]); $this->fail('Exception expected from get_context_wrapper()'); } catch (\moodle_exception $e) { $this->assertInstanceOf(\invalid_parameter_exception::class, $e); } try { $fetchedcontext = $this->get_context_from_params(["contextid" => null]); $this->fail('Exception expected from get_context_wrapper()'); } catch (\moodle_exception $e) { $this->assertInstanceOf(\invalid_parameter_exception::class, $e); } // Tests for context with instanceid equal to 0 (System context). $realcontext = \context_system::instance(); $fetchedcontext = $this->get_context_from_params(["contextlevel" => "system", "instanceid" => 0]); $this->assertEquals($realcontext, $fetchedcontext); // Passing wrong level name. try { $fetchedcontext = $this->get_context_from_params(["contextlevel" => "random", "instanceid" => $course->id]); $this->fail('exception expected when level name is invalid'); } catch (\moodle_exception $e) { $this->assertInstanceOf('invalid_parameter_exception', $e); $this->assertSame('Invalid parameter value detected (Invalid context level = random)', $e->getMessage()); } // Passing wrong level number. try { $fetchedcontext = $this->get_context_from_params(["contextlevel" => -10, "instanceid" => $course->id]); $this->fail('exception expected when level name is invalid'); } catch (\moodle_exception $e) { $this->assertInstanceOf('invalid_parameter_exception', $e); $this->assertSame('Invalid parameter value detected (Invalid context level = -10)', $e->getMessage()); } } /** * Test \core_external\external_api::get_context()_from_params parameter validation. * * @covers \core_external\external_api::get_context_from_params */ public function test_get_context_params(): void { global $USER; // Call without correct context details. $this->expectException('invalid_parameter_exception'); $this->get_context_from_params(['roleid' => 3, 'userid' => $USER->id]); } /** * Test \core_external\external_api::get_context()_from_params parameter validation. * * @covers \core_external\external_api::get_context_from_params */ public function test_get_context_params2(): void { global $USER; // Call without correct context details. $this->expectException('invalid_parameter_exception'); $this->get_context_from_params(['roleid' => 3, 'userid' => $USER->id, 'contextlevel' => "course"]); } /** * Test \core_external\external_api::get_context()_from_params parameter validation. * @covers \core_external\external_api::get_context_from_params */ public function test_get_context_params3(): void { global $USER; // Call without correct context details. $this->resetAfterTest(true); $course = self::getDataGenerator()->create_course(); $this->expectException('invalid_parameter_exception'); $this->get_context_from_params(['roleid' => 3, 'userid' => $USER->id, 'instanceid' => $course->id]); } /** * Data provider for the test_all_external_info test. * * @return array */ public static function all_external_info_provider(): array { global $DB; // We are testing here that all the external function descriptions can be generated without // producing warnings. E.g. misusing optional params will generate a debugging message which // will fail this test. $functions = $DB->get_records('external_functions', [], 'name'); $return = []; foreach ($functions as $f) { $return[$f->name] = [$f]; } return $return; } /** * Test \core_external\external_api::external_function_info. * * @group plugin_checks * @runInSeparateProcess * @dataProvider all_external_info_provider * @covers \core_external\external_api::external_function_info * @param \stdClass $definition */ public function test_all_external_info(\stdClass $definition): void { $desc = external_api::external_function_info($definition); $this->assertNotEmpty($desc->name); $this->assertNotEmpty($desc->classname); $this->assertNotEmpty($desc->methodname); $this->assertEquals($desc->component, clean_param($desc->component, PARAM_COMPONENT)); $this->assertInstanceOf(external_function_parameters::class, $desc->parameters_desc); if ($desc->returns_desc != null) { $this->assertInstanceOf(external_description::class, $desc->returns_desc); } } /** * Test the \core_external\external_api::call_external_function() function. * * @covers \core_external\external_api::call_external_function */ public function test_call_external_function(): void { global $PAGE, $COURSE, $CFG; $this->resetAfterTest(true); // Call some webservice functions and verify they are correctly handling $PAGE and $COURSE. // First test a function that calls validate_context outside a course. $this->setAdminUser(); $category = $this->getDataGenerator()->create_category(); $params = [ 'contextid' => \context_coursecat::instance($category->id)->id, 'name' => 'aaagrrryyy', 'idnumber' => '', 'description' => '', ]; $cohort1 = $this->getDataGenerator()->create_cohort($params); $cohort2 = $this->getDataGenerator()->create_cohort(); $beforepage = $PAGE; $beforecourse = $COURSE; $params = ['cohortids' => [$cohort1->id, $cohort2->id]]; $result = external_api::call_external_function('core_cohort_get_cohorts', $params); $this->assertSame($beforepage, $PAGE); $this->assertSame($beforecourse, $COURSE); // Now test a function that calls validate_context inside a course. $course = $this->getDataGenerator()->create_course(); $beforepage = $PAGE; $beforecourse = $COURSE; $params = ['courseid' => $course->id, 'options' => []]; $result = external_api::call_external_function('core_enrol_get_enrolled_users', $params); $this->assertSame($beforepage, $PAGE); $this->assertSame($beforecourse, $COURSE); // Test a function that triggers a PHP exception. require_once($CFG->dirroot . '/lib/tests/fixtures/test_external_function_throwable.php'); // Call our test function. $result = \test_external_function_throwable::call_external_function('core_throw_exception', [], false); $this->assertTrue($result['error']); $this->assertArrayHasKey('exception', $result); $this->assertEquals($result['exception']->message, 'Exception - Modulo by zero'); } /** * Test verifying external API for a deprecated plugin type. * * @runInSeparateProcess * @return void */ public function test_external_api_deprecated_plugintype(): void { $this->resetAfterTest(); global $CFG; require_once($CFG->libdir . '/upgradelib.php'); // Needed for external_update_descriptions(). // Inject the 'fake' plugin type and deprecate it. // Note: this method of injection is required to ensure core_component fully builds all caches from the ground up, // which is necessary to test things like class autoloading. $this->add_full_mocked_plugintype( plugintype: 'fake', path: 'lib/tests/fixtures/fakeplugins/fake', ); $this->deprecate_full_mocked_plugintype('fake'); external_update_descriptions('fake_fullfeatured'); $this->assertNotFalse( \core_external\external_api::external_function_info('fake_fullfeatured_service_test', IGNORE_MISSING) ); $result = \core_external\external_api::call_external_function('fake_fullfeatured_service_test', []); $this->assertArrayHasKey('error', $result); $this->assertFalse($result['error']); $this->assertArrayHasKey('data', $result); $this->assertEquals('fake_fullfeatured service result', $result['data']['result']); } /** * Test verifying external API for a phase 2 deprecated (deleted) plugin type. * * @runInSeparateProcess * @return void */ public function test_external_api_deleted_plugintype(): void { $this->resetAfterTest(); global $CFG; require_once($CFG->libdir . '/upgradelib.php'); // Needed for external_update_descriptions(). // Inject the 'fake' plugin type and flag it as deleted. // Note: this method of injection is required to ensure core_component fully builds all caches from the ground up, // which is necessary to test things like class autoloading. $this->add_full_mocked_plugintype( plugintype: 'fake', path: 'lib/tests/fixtures/fakeplugins/fake', ); $this->delete_full_mocked_plugintype('fake'); external_update_descriptions('fake_fullfeatured'); $this->assertFalse(\core_external\external_api::external_function_info('fake_fullfeatured_service_test', IGNORE_MISSING)); } /** * Call the get_contect_from_params methods on the api class. * * @return mixed */ protected function get_context_from_params() { $rc = new \ReflectionClass(external_api::class); $method = $rc->getMethod('get_context_from_params'); return $method->invokeArgs(null, func_get_args()); } } PK>\ʷ tests/external_value_test.phpnu[. namespace core_external; use advanced_testcase; /** * Unit tests for core_external\external_description. * * @package core * @category test * @copyright 2023 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass external_value */ final class external_value_test extends advanced_testcase { /** * Data provider for the required param test. * * @return array[] */ public static function required_param_provider(): array { return [ [ VALUE_DEFAULT, false ], [ VALUE_REQUIRED, false ], [ VALUE_OPTIONAL, false ], [ 'aaa', true, 'aaa' ], [ [VALUE_OPTIONAL], true, 'Array: ' . VALUE_OPTIONAL ], [ -1000, true, -1000 ], ]; } /** * Tests the constructor for the $required parameter validation. * * @dataProvider required_param_provider * @param int $required The required param being tested. * @param bool $debuggingexpected Whether debugging is expected. * @param mixed $requiredstr The string value of the $required param in the debugging message. * @return void */ public function test_required_param_validation($required, $debuggingexpected, $requiredstr = ''): void { $externalvalue = new external_value(PARAM_INT, 'Cool description', $required); if ($debuggingexpected) { $this->assertDebuggingCalled("Invalid \$required parameter value: '{$requiredstr}' . It must be either VALUE_DEFAULT, VALUE_REQUIRED, or VALUE_OPTIONAL", DEBUG_DEVELOPER); } $this->assertEquals(PARAM_INT, $externalvalue->type); $this->assertEquals('Cool description', $externalvalue->desc); $this->assertEquals($required, $externalvalue->required); } } PK>\ [[tests/privacy/provider_test.phpnu[. /** * Data provider tests. * * @package core_external * @category test * @copyright 2018 Frédéric Massart * @author Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_external\privacy; use core_external\privacy\provider; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\approved_userlist; use core_privacy\local\request\transform; use core_privacy\local\request\writer; use core_privacy\tests\provider_testcase; /** * External subsytem testcase class. * * @package core_external * @category test * @copyright 2018 Frédéric Massart * @author Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class provider_test extends provider_testcase { public function setUp(): void { parent::setUp(); $this->resetAfterTest(); } /** * Test the external service get_contexts_for_userid function. * * @covers \core_external\privacy\provider::get_contexts_for_userid */ public function test_get_contexts_for_userid(): void { $dg = $this->getDataGenerator(); $u1 = $dg->create_user(); $u2 = $dg->create_user(); $u3 = $dg->create_user(); $u4 = $dg->create_user(); $u5 = $dg->create_user(); $u1ctx = \context_user::instance($u1->id); $u2ctx = \context_user::instance($u2->id); $u3ctx = \context_user::instance($u3->id); $u5ctx = \context_user::instance($u5->id); $s = $this->create_service(); $this->create_token(['userid' => $u1->id]); $this->create_token(['userid' => $u1->id]); $this->create_token(['userid' => $u2->id, 'creatorid' => $u3->id]); $this->create_service_user(['externalserviceid' => $s->id, 'userid' => $u5->id]); $contextids = provider::get_contexts_for_userid($u1->id)->get_contextids(); $this->assertCount(1, $contextids); $this->assertTrue(in_array($u1ctx->id, $contextids)); $contextids = provider::get_contexts_for_userid($u2->id)->get_contextids(); $this->assertCount(1, $contextids); $this->assertTrue(in_array($u2ctx->id, $contextids)); $contextids = provider::get_contexts_for_userid($u3->id)->get_contextids(); $this->assertCount(1, $contextids); $this->assertTrue(in_array($u2ctx->id, $contextids)); $contextids = provider::get_contexts_for_userid($u4->id)->get_contextids(); $this->assertCount(0, $contextids); $contextids = provider::get_contexts_for_userid($u5->id)->get_contextids(); $this->assertCount(1, $contextids); $this->assertTrue(in_array($u5ctx->id, $contextids)); } /** * Test delete_data_for_user * * @covers \core_external\privacy\provider::delete_data_for_user */ public function test_delete_data_for_user(): void { global $DB; $dg = $this->getDataGenerator(); $u1 = $dg->create_user(); $u2 = $dg->create_user(); $u1ctx = \context_user::instance($u1->id); $u2ctx = \context_user::instance($u2->id); $s = $this->create_service(); $this->create_token(['userid' => $u1->id, 'creatorid' => $u2->id]); $this->create_token(['userid' => $u1->id]); $this->create_token(['userid' => $u2->id]); $this->create_service_user(['externalserviceid' => $s->id, 'userid' => $u1->id]); $this->create_service_user(['externalserviceid' => $s->id, 'userid' => $u2->id]); $this->assertEquals(2, $DB->count_records('external_tokens', ['userid' => $u1->id])); $this->assertEquals(1, $DB->count_records('external_tokens', ['userid' => $u2->id])); $this->assertTrue($DB->record_exists('external_services_users', ['userid' => $u1->id])); $this->assertTrue($DB->record_exists('external_services_users', ['userid' => $u2->id])); // Delete in another context, nothing happens. provider::delete_data_for_user(new approved_contextlist($u2, 'core_external', [$u1ctx->id])); $this->assertEquals(2, $DB->count_records('external_tokens', ['userid' => $u1->id])); $this->assertEquals(1, $DB->count_records('external_tokens', ['userid' => $u2->id])); $this->assertTrue($DB->record_exists('external_services_users', ['userid' => $u1->id])); $this->assertTrue($DB->record_exists('external_services_users', ['userid' => $u2->id])); // Delete in my context. provider::delete_data_for_user(new approved_contextlist($u2, 'core_external', [$u2ctx->id])); $this->assertEquals(2, $DB->count_records('external_tokens', ['userid' => $u1->id])); $this->assertEquals(0, $DB->count_records('external_tokens', ['userid' => $u2->id])); $this->assertTrue($DB->record_exists('external_services_users', ['userid' => $u1->id])); $this->assertFalse($DB->record_exists('external_services_users', ['userid' => $u2->id])); } /** * Test delete_data_for_all_users_in_context * * @covers \core_external\privacy\provider::delete_data_for_all_users_in_context */ public function test_delete_data_for_all_users_in_context(): void { global $DB; $dg = $this->getDataGenerator(); $u1 = $dg->create_user(); $u2 = $dg->create_user(); $u1ctx = \context_user::instance($u1->id); $u2ctx = \context_user::instance($u2->id); $s = $this->create_service(); $this->create_token(['userid' => $u1->id, 'creatorid' => $u2->id]); $this->create_token(['userid' => $u1->id]); $this->create_token(['userid' => $u2->id]); $this->create_service_user(['externalserviceid' => $s->id, 'userid' => $u1->id]); $this->create_service_user(['externalserviceid' => $s->id, 'userid' => $u2->id]); $this->assertEquals(2, $DB->count_records('external_tokens', ['userid' => $u1->id])); $this->assertEquals(1, $DB->count_records('external_tokens', ['userid' => $u2->id])); $this->assertTrue($DB->record_exists('external_services_users', ['userid' => $u1->id])); $this->assertTrue($DB->record_exists('external_services_users', ['userid' => $u2->id])); provider::delete_data_for_all_users_in_context($u2ctx); $this->assertEquals(2, $DB->count_records('external_tokens', ['userid' => $u1->id])); $this->assertEquals(0, $DB->count_records('external_tokens', ['userid' => $u2->id])); $this->assertTrue($DB->record_exists('external_services_users', ['userid' => $u1->id])); $this->assertFalse($DB->record_exists('external_services_users', ['userid' => $u2->id])); provider::delete_data_for_all_users_in_context($u1ctx); $this->assertEquals(0, $DB->count_records('external_tokens', ['userid' => $u1->id])); $this->assertEquals(0, $DB->count_records('external_tokens', ['userid' => $u2->id])); $this->assertFalse($DB->record_exists('external_services_users', ['userid' => $u1->id])); $this->assertFalse($DB->record_exists('external_services_users', ['userid' => $u2->id])); } /** * Test the export_user_data function. * @covers \core_external\privacy\provider::export_user_data */ public function test_export_data_for_user(): void { global $DB; $dg = $this->getDataGenerator(); $u1 = $dg->create_user(); $u2 = $dg->create_user(); $u1ctx = \context_user::instance($u1->id); $u2ctx = \context_user::instance($u2->id); $path = [get_string('services', 'core_external')]; $yearago = time() - YEARSECS; $hourago = time() - HOURSECS; $s = $this->create_service(['name' => 'Party time!']); $this->create_token(['userid' => $u1->id, 'timecreated' => $yearago]); $this->create_token([ 'userid' => $u1->id, 'creatorid' => $u2->id, 'iprestriction' => '127.0.0.1', 'lastaccess' => $hourago, ]); $this->create_token([ 'userid' => $u2->id, 'iprestriction' => '192.168.1.0/24', 'lastaccess' => $yearago, 'externalserviceid' => $s->id, ]); $this->create_service_user(['externalserviceid' => $s->id, 'userid' => $u2->id]); // User 1 exporting user 2 context does not give anything. writer::reset(); provider::export_user_data(new approved_contextlist($u1, 'core_external', [$u2ctx->id])); $data = writer::with_context($u1ctx)->get_data($path); $this->assertEmpty($data); $data = writer::with_context($u1ctx)->get_related_data($path, 'created_by_you'); $this->assertEmpty($data); $data = writer::with_context($u2ctx)->get_data($path); $this->assertEmpty($data); $data = writer::with_context($u2ctx)->get_related_data($path, 'created_by_you'); $this->assertEmpty($data); // User 1 exporting their context. writer::reset(); provider::export_user_data(new approved_contextlist($u1, 'core_external', [$u1ctx->id, $u2ctx->id])); $data = writer::with_context($u1ctx)->get_data($path); $this->assertFalse(isset($data->services_user)); $this->assertCount(2, $data->tokens); $this->assertEquals(transform::datetime($yearago), $data->tokens[0]['created_on']); $this->assertEquals(null, $data->tokens[0]['ip_restriction']); $this->assertEquals(transform::datetime($hourago), $data->tokens[1]['last_access']); $this->assertEquals('127.0.0.1', $data->tokens[1]['ip_restriction']); $data = writer::with_context($u1ctx)->get_related_data($path, 'created_by_you'); $this->assertEmpty($data); $data = writer::with_context($u2ctx)->get_data($path); $this->assertEmpty($data); $data = writer::with_context($u2ctx)->get_related_data($path, 'created_by_you'); $this->assertEmpty($data); // User 2 exporting their context. writer::reset(); provider::export_user_data(new approved_contextlist($u2, 'core_external', [$u1ctx->id, $u2ctx->id])); $data = writer::with_context($u2ctx)->get_data($path); $this->assertCount(1, $data->tokens); $this->assertEquals('Party time!', $data->tokens[0]['external_service']); $this->assertEquals(transform::datetime($yearago), $data->tokens[0]['last_access']); $this->assertEquals('192.168.1.0/24', $data->tokens[0]['ip_restriction']); $this->assertCount(1, $data->services_user); $this->assertEquals('Party time!', $data->services_user[0]['external_service']); $data = writer::with_context($u1ctx)->get_related_data($path, 'created_by_you'); $this->assertCount(1, $data->tokens); $this->assertEquals(transform::datetime($hourago), $data->tokens[0]['last_access']); $this->assertEquals('127.0.0.1', $data->tokens[0]['ip_restriction']); $data = writer::with_context($u1ctx)->get_data($path); $this->assertEmpty($data); $data = writer::with_context($u2ctx)->get_related_data($path, 'created_by_you'); $this->assertEmpty($data); } /** * Test that only users with a user context are fetched. * * @covers \core_external\privacy\provider::get_users_in_context */ public function test_get_users_in_context(): void { $component = 'core_external'; // Create user u1. $u1 = $this->getDataGenerator()->create_user(); $u1ctx = \context_user::instance($u1->id); // Create user u2. $u2 = $this->getDataGenerator()->create_user(); $u2ctx = \context_user::instance($u2->id); // Create user u3. $u3 = $this->getDataGenerator()->create_user(); $u3ctx = \context_user::instance($u3->id); // Create user u4. $u4 = $this->getDataGenerator()->create_user(); $u4ctx = \context_user::instance($u4->id); // Create user u5. $u5 = $this->getDataGenerator()->create_user(); $u5ctx = \context_user::instance($u5->id); // The lists of users for each user context ($u1ctx, $u2ctx, etc.) should be empty. // Related user data have not been created yet. $userlist1 = new \core_privacy\local\request\userlist($u1ctx, $component); provider::get_users_in_context($userlist1); $this->assertCount(0, $userlist1); $userlist2 = new \core_privacy\local\request\userlist($u2ctx, $component); provider::get_users_in_context($userlist2); $this->assertCount(0, $userlist2); $userlist3 = new \core_privacy\local\request\userlist($u3ctx, $component); provider::get_users_in_context($userlist3); $this->assertCount(0, $userlist3); $userlist4 = new \core_privacy\local\request\userlist($u4ctx, $component); provider::get_users_in_context($userlist4); $this->assertCount(0, $userlist4); $userlist5 = new \core_privacy\local\request\userlist($u5ctx, $component); provider::get_users_in_context($userlist5); $this->assertCount(0, $userlist5); // Create a service. $s = $this->create_service(); // Create a ws token for u1. $this->create_token(['userid' => $u1->id]); // Create a ws token for u2, and u3 as the creator of the token. $this->create_token(['userid' => $u2->id, 'creatorid' => $u3->id]); // Create a service user (u4). $this->create_service_user(['externalserviceid' => $s->id, 'userid' => $u4->id]); // The list of users for userlist1 should return one user (u1). provider::get_users_in_context($userlist1); $this->assertCount(1, $userlist1); $expected = [$u1->id]; $actual = $userlist1->get_userids(); $this->assertEquals($expected, $actual); // The list of users for userlist2 should return one user (u2). provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); $expected = [$u2->id]; $actual = $userlist2->get_userids(); $this->assertEquals($expected, $actual); // The list of users for userlist3 should return one user (u3). provider::get_users_in_context($userlist3); $this->assertCount(1, $userlist3); $expected = [$u3->id]; $actual = $userlist3->get_userids(); $this->assertEquals($expected, $actual); // The list of users for userlist4 should return one user (u4). provider::get_users_in_context($userlist4); $this->assertCount(1, $userlist4); $expected = [$u4->id]; $actual = $userlist4->get_userids(); $this->assertEquals($expected, $actual); // The list of users for userlist5 should not return any users. provider::get_users_in_context($userlist5); $this->assertCount(0, $userlist5); // The list of users should only return users in the user context. $systemcontext = \context_system::instance(); $userlist6 = new \core_privacy\local\request\userlist($systemcontext, $component); provider::get_users_in_context($userlist6); $this->assertCount(0, $userlist6); } /** * Test that data for users in approved userlist is deleted. * * @covers \core_external\privacy\provider::delete_data_for_users */ public function test_delete_data_for_users(): void { $component = 'core_external'; // Create user u1. $u1 = $this->getDataGenerator()->create_user(); $u1ctx = \context_user::instance($u1->id); // Create user u2. $u2 = $this->getDataGenerator()->create_user(); $u2ctx = \context_user::instance($u2->id); // Create user u3. $u3 = $this->getDataGenerator()->create_user(); $u3ctx = \context_user::instance($u3->id); // Create user u4. $u4 = $this->getDataGenerator()->create_user(); $u4ctx = \context_user::instance($u4->id); // Create user u5. $u5 = $this->getDataGenerator()->create_user(); $u5ctx = \context_user::instance($u5->id); // Create a service. $s = $this->create_service(); // Create a ws token for u1. $this->create_token(['userid' => $u1->id]); // Create a ws token for u2, and u3 as the creator of the token. $this->create_token(['userid' => $u2->id, 'creatorid' => $u3->id]); // Create a service user (u4). $this->create_service_user(['externalserviceid' => $s->id, 'userid' => $u4->id]); // Create a service user (u5). $this->create_service_user(['externalserviceid' => $s->id, 'userid' => $u5->id]); // The list of users for u1ctx should return one user (u1). $userlist1 = new \core_privacy\local\request\userlist($u1ctx, $component); provider::get_users_in_context($userlist1); $this->assertCount(1, $userlist1); // The list of users for u2ctx should return one user (u2). $userlist2 = new \core_privacy\local\request\userlist($u2ctx, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); // The list of users for u3ctx should return one user (u3). $userlist3 = new \core_privacy\local\request\userlist($u3ctx, $component); provider::get_users_in_context($userlist3); $this->assertCount(1, $userlist3); // The list of users for u4ctx should return one user (u4). $userlist4 = new \core_privacy\local\request\userlist($u4ctx, $component); provider::get_users_in_context($userlist4); $this->assertCount(1, $userlist4); $approvedlist = new approved_userlist($u1ctx, $component, $userlist1->get_userids()); // Delete using delete_data_for_user. provider::delete_data_for_users($approvedlist); // Re-fetch users in u1ctx - the user data should now be empty. $userlist1 = new \core_privacy\local\request\userlist($u1ctx, $component); provider::get_users_in_context($userlist1); $this->assertCount(0, $userlist1); $approvedlist = new approved_userlist($u2ctx, $component, $userlist2->get_userids()); // Delete using delete_data_for_user. provider::delete_data_for_users($approvedlist); // Re-fetch users in u2ctx - the user data should now be empty. $userlist2 = new \core_privacy\local\request\userlist($u2ctx, $component); provider::get_users_in_context($userlist2); $this->assertCount(0, $userlist2); $approvedlist = new approved_userlist($u3ctx, $component, $userlist3->get_userids()); // Delete using delete_data_for_user. provider::delete_data_for_users($approvedlist); // Re-fetch users in u3ctx - the user data should now be empty. $userlist3 = new \core_privacy\local\request\userlist($u3ctx, $component); provider::get_users_in_context($userlist3); $this->assertCount(0, $userlist3); $approvedlist = new approved_userlist($u4ctx, $component, $userlist3->get_userids()); // Delete using delete_data_for_user. provider::delete_data_for_users($approvedlist); // Re-fetch users in u4ctx - the user data should now be empty. $userlist4 = new \core_privacy\local\request\userlist($u4ctx, $component); provider::get_users_in_context($userlist4); $this->assertCount(0, $userlist4); // The list of users for u5ctx should still return one user (u5). $userlist5 = new \core_privacy\local\request\userlist($u5ctx, $component); provider::get_users_in_context($userlist5); $this->assertCount(1, $userlist5); // User data should only be removed in the user context. $systemcontext = \context_system::instance(); $approvedlist = new approved_userlist($systemcontext, $component, $userlist5->get_userids()); // Delete using delete_data_for_user. provider::delete_data_for_users($approvedlist); // Re-fetch users in u5ctx - the user data should still be present. $userlist5 = new \core_privacy\local\request\userlist($u5ctx, $component); provider::get_users_in_context($userlist5); $this->assertCount(1, $userlist5); } /** * Create a service. * * @param array $params The params. * @return \stdClass */ protected function create_service(array $params = []) { global $DB; static $i = 0; $record = (object) array_merge([ 'name' => 'Some service', 'enabled' => '1', 'requiredcapability' => '', 'restrictedusers' => '0', 'component' => 'core_external', 'timecreated' => time(), 'timemodified' => time(), 'shortname' => 'service' . $i, 'downloadfiles' => '1', 'uploadfiles' => '1', ], $params); $record->id = $DB->insert_record('external_services', $record); return $record; } /** * Create a service user. * * @param array $params The params. * @return \stdClass */ protected function create_service_user(array $params) { global $DB, $USER; static $i = 0; $record = (object) array_merge([ 'externalserviceid' => null, 'userid' => $USER->id, 'validuntil' => time() + YEARSECS, 'iprestriction' => '', 'timecreated' => time(), ], $params); $record->id = $DB->insert_record('external_services_users', $record); return $record; } /** * Create a token. * * @param array $params The params. * @return stdClass */ protected function create_token(array $params) { global $DB, $USER; $service = $DB->get_record('external_services', ['shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE]); $record = (object) array_merge([ 'token' => random_string(64), 'privatetoken' => random_string(64), 'tokentype' => EXTERNAL_TOKEN_PERMANENT, 'contextid' => SYSCONTEXTID, 'externalserviceid' => $service->id, 'userid' => $USER->id, 'validuntil' => time() + YEARSECS, 'iprestriction' => null, 'sid' => null, 'timecreated' => time(), 'lastaccess' => time(), 'creatorid' => $USER->id, ], $params); $record->id = $DB->insert_record('external_tokens', $record); return $record; } } PK>\RU tests/external_settings_test.phpnu[. namespace core_external; /** * Unit tests for core_external\external_settings. * * @package core_external * @category test * @copyright 2022 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @covers \core_external\external_settings */ final class external_settings_test extends \advanced_testcase { /** * Reset the singleton between tests. */ public function tearDown(): void { external_settings::reset(); parent::tearDown(); } /** * Tests for external_settings class. * * @covers \core_external\external_settings::get_instance */ public function test_external_settings(): void { $settings = external_settings::get_instance(); $this->assertInstanceOf(external_settings::class, $settings); } /** * Check external_settings defaults. * * @covers \core_external\external_settings::get_instance */ public function test_external_settings_defaults(): void { $settings = external_settings::get_instance(); $currentraw = $settings->get_raw(); $currentfilter = $settings->get_filter(); $currentfile = $settings->get_file(); $currentfileurl = $settings->get_fileurl(); $this->assertInstanceOf(external_settings::class, $settings); // Check apis. $settings->set_file('plugin.php'); $this->assertEquals('plugin.php', $settings->get_file()); $settings->set_filter(false); $this->assertFalse($settings->get_filter()); $settings->set_fileurl(false); $this->assertFalse($settings->get_fileurl()); $settings->set_raw(true); $this->assertTrue($settings->get_raw()); } /** * Check external_settings file API calls. * * @covers \core_external\external_settings::set_file * @covers \core_external\external_settings::get_file */ public function test_external_settings_set_file(): void { $settings = external_settings::get_instance(); $settings->set_file('plugin.php'); $this->assertEquals('plugin.php', $settings->get_file()); } /** * Check external_settings filter API calls. * * @covers \core_external\external_settings::set_filter * @covers \core_external\external_settings::get_filter */ public function test_external_settings_set_filter(): void { $settings = external_settings::get_instance(); $settings->set_filter(false); $this->assertFalse($settings->get_filter()); } /** * Check external_settings file API calls. * * @covers \core_external\external_settings::set_fileurl * @covers \core_external\external_settings::get_fileurl */ public function test_external_settings_set_fileurl(): void { $settings = external_settings::get_instance(); $settings->set_fileurl(false); $this->assertFalse($settings->get_fileurl()); } /** * Check external_settings raw API calls. * * @covers \core_external\external_settings::set_raw * @covers \core_external\external_settings::get_raw */ public function test_external_settings_set_raw(): void { $settings = external_settings::get_instance(); $settings->set_raw(true); $this->assertTrue($settings->get_raw()); } } PK>\A%==tests/plugin_checks_test.phpnu[. namespace core_external; // phpcs:disable moodle.PHPUnit.TestCaseProvider.dataProviderSyntaxMethodNotFound /** * Detect common problems in plugin external API. * * @group plugin_checks * @package core_external * @copyright 2025 Petr Skoda * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class plugin_checks_test extends \core\tests\plugin_checks_testcase { /** * Verify plugin external API definition files. * * @dataProvider all_plugins_provider * @coversNothing * * @param string $component * @param string $plugintype * @param string $pluginname * @param string $dir */ public function test_db_services_file(string $component, string $plugintype, string $pluginname, string $dir): void { $file = "$dir/db/services.php"; $functions = $this->fetch_array_from_file($file, 'functions'); if (!$functions) { $this->expectNotToPerformAssertions(); return; } foreach ($functions as $wsname => $definition) { $this->assertStringStartsWith($component . '_', $wsname); } } } PK>\=nj&tests/classes/externallib_testcase.phpnu[. namespace core_external\tests; use core\context; use core\exception\coding_exception; use core_external\external_settings; use filter_manager; // phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod /** * Helper base class for external tests. Helpfull to test capabilities. * * @package core_external * @copyright 2012 Jerome Mouneyrac * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class externallib_testcase extends \advanced_testcase { /** * Assign a capability to $USER * The function creates a student $USER if $USER->id is empty * * @param string $capability capability name * @param int|context $contextid * @param int $roleid * @return int the role id - mainly returned for creation, so calling function can reuse it */ public static function assignUserCapability($capability, $contextid, $roleid = null) { global $USER; // Create a new student $USER if $USER doesn't exist. if (empty($USER->id)) { $user = self::getDataGenerator()->create_user(); self::setUser($user); } if (empty($roleid)) { $roleid = create_role('Dummy role', 'dummyrole', 'dummy role description'); } assign_capability($capability, CAP_ALLOW, $roleid, $contextid); role_assign($roleid, $USER->id, $contextid); accesslib_clear_all_caches_for_unit_testing(); return $roleid; } /** * Configure some filters for external tests. * * @param array $filters Filters to enable. Each filter should contain: * - name: name of the filter. * - state: the state of the filter. * - move: -1 means up, 0 means the same, 1 means down. * - applytostrings: true to apply the filter to content and headings, false for just content. */ public static function configure_filters($filters) { global $CFG; $filterstrings = false; // Enable the filters. foreach ($filters as $filter) { $filter = (array) $filter; filter_set_global_state($filter['name'], $filter['state'], $filter['move']); filter_set_applies_to_strings($filter['name'], $filter['applytostrings']); $filterstrings = $filterstrings || $filter['applytostrings']; } // Set WS filtering. $wssettings = external_settings::get_instance(); $wssettings->set_filter(true); // Reset filter caches. $filtermanager = filter_manager::instance(); $filtermanager->reset_caches(); if ($filterstrings) { // Don't strip tags in strings. $CFG->formatstringstriptags = false; } } /** * Unassign a capability to $USER. * * @param string $capability capability name. * @param int $contextid set the context id if you used assignUserCapability. * @param int $roleid set the role id if you used assignUserCapability. * @param int $courseid set the course id if you used getDataGenerator->enrol_users. * @param string $enrol set the enrol plugin name if you used * getDataGenerator->enrol_users with a different plugin than 'manual'. */ public static function unassignUserCapability( $capability, $contextid = null, $roleid = null, $courseid = null, $enrol = 'manual', ) { global $DB; if (!empty($courseid)) { // Retrieve the role id. $instances = $DB->get_records('enrol', ['courseid' => $courseid, 'enrol' => $enrol]); if (count($instances) != 1) { throw new coding_exception('No found enrol instance for courseid: ' . $courseid . ' and enrol: ' . $enrol); } $instance = reset($instances); if (is_null($roleid) && $instance->roleid) { $roleid = $instance->roleid; } } else { if (empty($contextid) || empty($roleid)) { throw new coding_exception('unassignUserCapaibility requires contextid/roleid or courseid'); } } unassign_capability($capability, $roleid, $contextid); accesslib_clear_all_caches_for_unit_testing(); } } PK>\f..#tests/external_externallib_test.phpnu[. namespace core; use core_external\external_api; use externallib_advanced_testcase; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/lib/external/externallib.php'); require_once($CFG->dirroot . '/webservice/tests/helpers.php'); /** * External library functions unit tests * * @package core * @category phpunit * @copyright 2012 Jerome Mouneyrac * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class external_externallib_test extends externallib_advanced_testcase { /** * Test get_string */ public function test_get_string(): void { $this->resetAfterTest(true); $service = new \stdClass(); $service->name = 'Dummy Service'; $service->id = 12; // String with two parameters. $returnedstring = \core_external::get_string('addservice', 'webservice', null, array(array('name' => 'name', 'value' => $service->name), array('name' => 'id', 'value' => $service->id))); // We need to execute the return values cleaning process to simulate the web service server. $returnedstring = external_api::clean_returnvalue(\core_external::get_string_returns(), $returnedstring); $corestring = get_string('addservice', 'webservice', $service); $this->assertSame($corestring, $returnedstring); // String with one parameter. $acapname = 'A capability name'; $returnedstring = \core_external::get_string('missingrequiredcapability', 'webservice', null, array(array('value' => $acapname))); // We need to execute the return values cleaning process to simulate the web service server. $returnedstring = external_api::clean_returnvalue(\core_external::get_string_returns(), $returnedstring); $corestring = get_string('missingrequiredcapability', 'webservice', $acapname); $this->assertSame($corestring, $returnedstring); // String without parameters. $returnedstring = \core_external::get_string('missingpassword', 'webservice'); // We need to execute the return values cleaning process to simulate the web service server. $returnedstring = external_api::clean_returnvalue(\core_external::get_string_returns(), $returnedstring); $corestring = get_string('missingpassword', 'webservice'); $this->assertSame($corestring, $returnedstring); // String with two parameter but one is invalid (not named). $this->expectException('moodle_exception'); $returnedstring = \core_external::get_string('addservice', 'webservice', null, array(array('value' => $service->name), array('name' => 'id', 'value' => $service->id))); } /** * Test get_string with HTML. */ public function test_get_string_containing_html(): void { $result = \core_external::get_string('registrationinfo'); $actual = external_api::clean_returnvalue(\core_external::get_string_returns(), $result); $expected = get_string('registrationinfo', 'moodle'); $this->assertSame($expected, $actual); } /** * Test get_string with arguments containing HTML. */ public function test_get_string_with_args_containing_html(): void { $result = \core_external::get_string('added', 'moodle', null, [['value' => 'Test']]); $actual = external_api::clean_returnvalue(\core_external::get_string_returns(), $result); $expected = get_string('added', 'moodle', 'Test'); $this->assertSame($expected, $actual); } /** * Test get_strings */ public function test_get_strings(): void { $this->resetAfterTest(true); $stringmanager = get_string_manager(); $service = new \stdClass(); $service->name = 'Dummy Service'; $service->id = 12; $returnedstrings = \core_external::get_strings( array( array( 'stringid' => 'addservice', 'component' => 'webservice', 'stringparams' => array(array('name' => 'name', 'value' => $service->name), array('name' => 'id', 'value' => $service->id) ), 'lang' => 'en' ), array('stringid' => 'addaservice', 'component' => 'webservice', 'lang' => 'en') )); // We need to execute the return values cleaning process to simulate the web service server. $returnedstrings = external_api::clean_returnvalue(\core_external::get_strings_returns(), $returnedstrings); foreach($returnedstrings as $returnedstring) { $corestring = $stringmanager->get_string($returnedstring['stringid'], $returnedstring['component'], $service, 'en'); $this->assertSame($corestring, $returnedstring['string']); } } /** * Test get_strings with HTML. */ public function test_get_strings_containing_html(): void { $result = \core_external::get_strings([['stringid' => 'registrationinfo'], ['stringid' => 'loginaspasswordexplain']]); $actual = external_api::clean_returnvalue(\core_external::get_strings_returns(), $result); $this->assertSame(get_string('registrationinfo', 'moodle'), $actual[0]['string']); $this->assertSame(get_string('loginaspasswordexplain', 'moodle'), $actual[1]['string']); } /** * Test get_strings with arguments containing HTML. */ public function test_get_strings_with_args_containing_html(): void { $result = \core_external::get_strings([ ['stringid' => 'added', 'stringparams' => [['value' => 'Test']]], ['stringid' => 'loggedinas', 'stringparams' => [['value' => 'Test']]]] ); $actual = external_api::clean_returnvalue(\core_external::get_strings_returns(), $result); $this->assertSame(get_string('added', 'moodle', 'Test'), $actual[0]['string']); $this->assertSame(get_string('loggedinas', 'moodle', 'Test'), $actual[1]['string']); } /** * Test get_component_strings */ public function test_get_component_strings(): void { global $USER; $this->resetAfterTest(true); $stringmanager = get_string_manager(); $wsstrings = $stringmanager->load_component_strings('webservice', current_language()); $componentstrings = \core_external::get_component_strings('webservice'); // We need to execute the return values cleaning process to simulate the web service server. $componentstrings = external_api::clean_returnvalue(\core_external::get_component_strings_returns(), $componentstrings); $this->assertEquals(count($componentstrings), count($wsstrings)); foreach($componentstrings as $string) { $this->assertSame($string['string'], $wsstrings[$string['stringid']]); } } /** * Test update_inplace_editable() */ public function test_update_inplace_editable(): void { $this->resetAfterTest(true); // Call service for component that does not have inplace_editable callback. try { \core_external::update_inplace_editable('tool_log', 'itemtype', 1, 'newvalue'); $this->fail('Exception expected'); } catch (\moodle_exception $e) { $this->assertEquals('Error calling update processor', $e->getMessage()); } // This is a very basic test for the return value of the external function. // More detailed test for tag updating can be found in core_tag component. $this->setAdminUser(); $tag = $this->getDataGenerator()->create_tag(); $res = \core_external::update_inplace_editable('core_tag', 'tagname', $tag->id, 'new tag name'); $res = external_api::clean_returnvalue(\core_external::update_inplace_editable_returns(), $res); $this->assertEquals('new tag name', $res['value']); } /** * Test update_inplace_editable with mathjax. */ public function test_update_inplace_editable_with_mathjax(): void { $this->resetAfterTest(true); $this->setAdminUser(); // Enable MathJax filter in content and headings. $this->configure_filters([ ['name' => 'mathjaxloader', 'state' => TEXTFILTER_ON, 'move' => -1, 'applytostrings' => true], ]); // Create a forum. $course = $this->getDataGenerator()->create_course(); $forum = self::getDataGenerator()->create_module('forum', array('course' => $course->id, 'name' => 'forum name')); // Change the forum name. $newname = 'New forum name $$(a+b)=2$$'; $res = \core_external::update_inplace_editable('core_course', 'activityname', $forum->cmid, $newname); $res = external_api::clean_returnvalue(\core_external::update_inplace_editable_returns(), $res); // Format original data. $context = \context_module::instance($forum->cmid); $newname = \core_external\util::format_string($newname, $context); $editlabel = get_string('newactivityname', '', $newname); // Check editlabel is the same and has mathjax. $this->assertStringContainsString('', $res['editlabel']); $this->assertEquals($editlabel, $res['editlabel']); } public function test_get_user_dates(): void { $this->resetAfterTest(); $this->setAdminUser(); // Set default timezone to Australia/Perth, else time calculated // will not match expected values. $this->setTimezone(99, 'Australia/Perth'); $context = \context_system::instance(); $request = [ [ 'timestamp' => 1293876000, 'format' => '%A, %d %B %Y, %I:%M' ], [ 'timestamp' => 1293876000, 'format' => '%d %m %Y' ], [ 'timestamp' => 1293876000, 'format' => '%d %m %Y', 'type' => 'gregorian' ], [ 'timestamp' => 1293876000, 'format' => 'some invalid format' ], ]; $result = \core_external::get_user_dates($context->id, null, null, $request); $result = external_api::clean_returnvalue(\core_external::get_user_dates_returns(), $result); $this->assertEquals('Saturday, 1 January 2011, 6:00', $result['dates'][0]); $this->assertEquals('1 01 2011', $result['dates'][1]); $this->assertEquals('1 01 2011', $result['dates'][2]); $this->assertEquals('some invalid format', $result['dates'][3]); } } PK>\2tests/external_files_test.phpnu[. namespace core_external; /** * Unit tests for core_external\external_files. * * @package core_external * @category test * @copyright 2022 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @covers \core_external\external_files */ final class external_files_test extends \advanced_testcase { /** * Text external files structure. * * @covers \core_external\external_files */ public function test_files_structure(): void { $description = new external_files(); // First check that the expected default values and keys are returned. $expectedkeys = array_flip([ 'filename', 'filepath', 'filesize', 'fileurl', 'timemodified', 'mimetype', 'isexternalfile', 'repositorytype', 'icon', ]); $returnedkeys = array_flip(array_keys($description->content->keys)); $this->assertEquals($expectedkeys, $returnedkeys); $this->assertEquals('List of files.', $description->desc); $this->assertEquals(VALUE_REQUIRED, $description->required); foreach ($description->content->keys as $key) { $this->assertEquals(VALUE_OPTIONAL, $key->required); } } } PK>\dݹEEtests/util_test.phpnu[. namespace core_external; /** * Unit tests for core_external\util. * * @package core_external * @category test * @copyright 2022 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @covers \core_external\util */ final class util_test extends \advanced_testcase { /** @var \moodle_database The database connection */ protected $db; /** * Store the global DB for restore between tests. */ public function setUp(): void { global $DB; parent::setUp(); $this->db = $DB; external_settings::reset(); } /** * A helper to include the legacy external functions. */ protected function include_legacy_functions(): void { global $CFG; $this->assertTrue( $this->isInIsolation(), 'Inclusion of the legacy test functions requires the test to be run in isolation.', ); // Note: This is retained for testing of the old functions. require_once("{$CFG->libdir}/externallib.php"); } /** * Reset the global DB between tests. */ public function tearDown(): void { global $DB; if ($this->db !== null) { $DB = $this->db; } external_settings::reset(); parent::tearDown(); } /** * Validate courses, but still return courses even if they fail validation. * * @covers \core_external\util::validate_courses */ public function test_validate_courses_keepfails(): void { $this->resetAfterTest(true); $c1 = $this->getDataGenerator()->create_course(); $c2 = $this->getDataGenerator()->create_course(); $c3 = $this->getDataGenerator()->create_course(); $u1 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $courseids = [$c1->id, $c2->id, $c3->id]; $this->setUser($u1); [$courses, $warnings] = util::validate_courses($courseids, [], false, true); $this->assertCount(2, $warnings); $this->assertEquals($c2->id, $warnings[0]['itemid']); $this->assertEquals($c3->id, $warnings[1]['itemid']); $this->assertCount(3, $courses); $this->assertTrue($courses[$c1->id]->contextvalidated); $this->assertFalse($courses[$c2->id]->contextvalidated); $this->assertFalse($courses[$c3->id]->contextvalidated); } /** * Validate courses can re-use an array of prefetched courses. * * @covers \core_external\util::validate_courses */ public function test_validate_courses_prefetch(): void { $this->resetAfterTest(true); $c1 = $this->getDataGenerator()->create_course(); $c2 = $this->getDataGenerator()->create_course(); $c3 = $this->getDataGenerator()->create_course(); $c4 = $this->getDataGenerator()->create_course(); $u1 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $this->getDataGenerator()->enrol_user($u1->id, $c2->id); $courseids = [$c1->id, $c2->id, $c3->id]; $courses = [$c2->id => $c2, $c3->id => $c3, $c4->id => $c4]; $this->setUser($u1); [$courses, $warnings] = util::validate_courses($courseids, $courses); $this->assertCount(2, $courses); $this->assertCount(1, $warnings); $this->assertArrayHasKey($c1->id, $courses); $this->assertSame($c2, $courses[$c2->id]); $this->assertArrayNotHasKey($c3->id, $courses); // The extra course passed is not returned. $this->assertArrayNotHasKey($c4->id, $courses); } /** * Test the Validate courses standard functionality. * * @covers \core_external\util::validate_courses */ public function test_validate_courses(): void { $this->resetAfterTest(true); $c1 = $this->getDataGenerator()->create_course(); $c2 = $this->getDataGenerator()->create_course(); $c3 = $this->getDataGenerator()->create_course(); $u1 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $courseids = [$c1->id, $c2->id, $c3->id]; $this->setAdminUser(); [$courses, $warnings] = util::validate_courses($courseids); $this->assertEmpty($warnings); $this->assertCount(3, $courses); $this->assertArrayHasKey($c1->id, $courses); $this->assertArrayHasKey($c2->id, $courses); $this->assertArrayHasKey($c3->id, $courses); $this->assertEquals($c1->id, $courses[$c1->id]->id); $this->assertEquals($c2->id, $courses[$c2->id]->id); $this->assertEquals($c3->id, $courses[$c3->id]->id); $this->setUser($u1); [$courses, $warnings] = util::validate_courses($courseids); $this->assertCount(2, $warnings); $this->assertEquals($c2->id, $warnings[0]['itemid']); $this->assertEquals($c3->id, $warnings[1]['itemid']); $this->assertCount(1, $courses); $this->assertArrayHasKey($c1->id, $courses); $this->assertArrayNotHasKey($c2->id, $courses); $this->assertArrayNotHasKey($c3->id, $courses); $this->assertEquals($c1->id, $courses[$c1->id]->id); } /** * Text util::get_area_files * * @covers \core_external\util::get_area_files */ public function test_get_area_files(): void { global $CFG, $DB; $this->db = $DB; $DB = $this->getMockBuilder('moodle_database')->getMock(); $content = base64_encode("Let us create a nice simple file."); $timemodified = 102030405; $itemid = 42; $filesize = strlen($content); $DB->method('get_records_sql')->willReturn([ (object) [ 'filename' => 'example.txt', 'filepath' => '/', 'mimetype' => 'text/plain', 'filesize' => $filesize, 'timemodified' => $timemodified, 'itemid' => $itemid, 'pathnamehash' => sha1('/example.txt'), ], ]); $component = 'mod_foo'; $filearea = 'area'; $context = 12345; $expectedfiles = [[ 'filename' => 'example.txt', 'filepath' => '/', 'fileurl' => "{$CFG->wwwroot}/webservice/pluginfile.php/{$context}/{$component}/{$filearea}/{$itemid}/example.txt", 'timemodified' => $timemodified, 'filesize' => $filesize, 'mimetype' => 'text/plain', 'isexternalfile' => false, 'icon' => 'f/text', ], ]; // Get all the files for the area. $files = util::get_area_files($context, $component, $filearea, false); $this->assertEquals($expectedfiles, $files); $DB->method('get_in_or_equal')->willReturn([ '= :mock1', ['mock1' => $itemid], ]); // Get just the file indicated by $itemid. $files = util::get_area_files($context, $component, $filearea, $itemid); $this->assertEquals($expectedfiles, $files); } /** * Test default time for user created tokens. * * @covers \core_external\util::generate_token_for_current_user */ public function test_user_created_tokens_duration(): void { global $CFG, $DB; $this->resetAfterTest(true); $CFG->enablewebservices = 1; $CFG->enablemobilewebservice = 1; $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $service = $DB->get_record('external_services', ['shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE, 'enabled' => 1]); $this->setUser($user1); $timenow = time(); $token = util::generate_token_for_current_user($service); $this->assertGreaterThanOrEqual($timenow + $CFG->tokenduration, $token->validuntil); // Change token default time. $this->setUser($user2); set_config('tokenduration', DAYSECS); $token = util::generate_token_for_current_user($service); $timenow = time(); $this->assertLessThanOrEqual($timenow + DAYSECS, $token->validuntil); } /** * Test the format_text function. * * @covers \core_external\util::format_text * @runInSeparateProcess */ public function test_format_text(): void { $this->include_legacy_functions(); $settings = external_settings::get_instance(); $settings->set_raw(true); $settings->set_filter(false); $context = \context_system::instance(); $test = '$$ \pi $$'; $testformat = FORMAT_MARKDOWN; $correct = [$test, $testformat]; $this->assertSame($correct, util::format_text($test, $testformat, $context, 'core', '', 0)); // Function external_format_text should work with context id or context instance. $this->assertSame(external_format_text($test, $testformat, $context->id, 'core', '', 0), $correct); $this->assertSame(external_format_text($test, $testformat, $context, 'core', '', 0), $correct); $settings->set_raw(false); $settings->set_filter(true); $test = '$$ \pi $$'; $testformat = FORMAT_MARKDOWN; $correct = ['

$$ \pi $$

', FORMAT_HTML, ]; $this->assertSame(util::format_text($test, $testformat, $context, 'core', '', 0), $correct); // Function external_format_text should work with context id or context instance. $this->assertSame(external_format_text($test, $testformat, $context->id, 'core', '', 0), $correct); $this->assertSame(external_format_text($test, $testformat, $context, 'core', '', 0), $correct); // Filters can be opted out from by the developer. $test = '$$ \pi $$'; $testformat = FORMAT_MARKDOWN; $correct = ['

$$ \pi $$

', FORMAT_HTML, ]; $this->assertSame(util::format_text($test, $testformat, $context, 'core', '', 0, ['filter' => false]), $correct); // Function external_format_text should work with context id or context instance. $this->assertSame(external_format_text($test, $testformat, $context->id, 'core', '', 0, ['filter' => false]), $correct); $this->assertSame(external_format_text($test, $testformat, $context, 'core', '', 0, ['filter' => false]), $correct); $test = '

Text

'; $testformat = FORMAT_HTML; $correct = [$test, FORMAT_HTML]; $options = ['allowid' => true]; $this->assertSame(util::format_text($test, $testformat, $context, 'core', '', 0, $options), $correct); // Function external_format_text should work with context id or context instance. $this->assertSame(external_format_text($test, $testformat, $context->id, 'core', '', 0, $options), $correct); $this->assertSame(external_format_text($test, $testformat, $context, 'core', '', 0, $options), $correct); $test = '

Text

'; $testformat = FORMAT_HTML; $correct = ['

Text

', FORMAT_HTML]; $options = new \stdClass(); $options->allowid = false; $this->assertSame(util::format_text($test, $testformat, $context, 'core', '', 0, $options), $correct); // Function external_format_text should work with context id or context instance. $this->assertSame(external_format_text($test, $testformat, $context->id, 'core', '', 0, $options), $correct); $this->assertSame(external_format_text($test, $testformat, $context, 'core', '', 0, $options), $correct); $test = '

Text

' . "\n" . 'Newline'; $testformat = FORMAT_MOODLE; $correct = ['

Text

Newline', FORMAT_HTML]; $options = new \stdClass(); $options->newlines = false; $this->assertSame(util::format_text($test, $testformat, $context, 'core', '', 0, $options), $correct); // Function external_format_text should work with context id or context instance. $this->assertSame(external_format_text($test, $testformat, $context->id, 'core', '', 0, $options), $correct); $this->assertSame(external_format_text($test, $testformat, $context, 'core', '', 0, $options), $correct); $test = '

Text

'; $testformat = FORMAT_MOODLE; $correct = ['
' . $test . '
', FORMAT_HTML]; $options = new \stdClass(); $options->para = true; $this->assertSame(util::format_text($test, $testformat, $context, 'core', '', 0, $options), $correct); // Function external_format_text should work with context id or context instance. $this->assertSame(external_format_text($test, $testformat, $context->id, 'core', '', 0, $options), $correct); $this->assertSame(external_format_text($test, $testformat, $context, 'core', '', 0, $options), $correct); $test = '

Text

'; $testformat = FORMAT_MOODLE; $correct = [$test, FORMAT_HTML]; $options = new \stdClass(); $options->context = $context; $this->assertSame(util::format_text($test, $testformat, $context, 'core', '', 0, $options), $correct); // Function external_format_text should work with context id or context instance. $this->assertSame(external_format_text($test, $testformat, $context->id, 'core', '', 0, $options), $correct); $this->assertSame(external_format_text($test, $testformat, $context, 'core', '', 0, $options), $correct); } /** * Teset the format_string function. * * @covers \core_external\util::format_string * @runInSeparateProcess */ public function test_external_format_string(): void { $this->resetAfterTest(); $this->include_legacy_functions(); $settings = external_settings::get_instance(); // Enable multilang filter to on content and heading. filter_set_global_state('multilang', TEXTFILTER_ON); filter_set_applies_to_strings('multilang', 1); $filtermanager = \filter_manager::instance(); $filtermanager->reset_caches(); $settings->set_raw(true); $settings->set_filter(true); $context = \context_system::instance(); $test = 'ENFR '; $test .= '

there

!'; $correct = $test; $this->assertSame($correct, util::format_string($test, $context)); // Function external_format_string should work with context id or context instance. $this->assertSame($correct, external_format_string($test, $context)); $this->assertSame($correct, external_format_string($test, $context->id)); $settings->set_raw(false); $settings->set_filter(false); $test = 'ENFR '; $test .= '

there

?'; $correct = 'ENFR hi there?'; $this->assertSame($correct, util::format_string($test, $context)); // Function external_format_string should work with context id or context instance. $this->assertSame($correct, external_format_string($test, $context)); $this->assertSame($correct, external_format_string($test, $context->id)); $settings->set_filter(true); $test = 'ENFR '; $test .= '

there

@'; $correct = 'EN hi there@'; $this->assertSame($correct, util::format_string($test, $context)); // Function external_format_string should work with context id or context instance. $this->assertSame($correct, external_format_string($test, $context)); $this->assertSame($correct, external_format_string($test, $context->id)); // Filters can be opted out. $test = 'ENFR '; $test .= '

there

%'; $correct = 'ENFR hi there%'; $this->assertSame($correct, util::format_string($test, $context, false, ['filter' => false])); // Function external_format_string should work with context id or context instance. $this->assertSame($correct, external_format_string($test, $context->id, false, ['filter' => false])); $this->assertSame($correct, external_format_string($test, $context, false, ['filter' => false])); $this->assertSame("& < > \" '", format_string("& < > \" '", true, ['escape' => false])); } } PK|\!Y  get_available_in_context.phpnu[. /** * This is the external API for the filter component. * * @package core_filters * @copyright 2017 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_filters\external; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; use Exception; /** * This is the external API for the filter component. * * @copyright 2017 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class get_available_in_context extends external_api { /** * Returns description of get_available_in_context parameters. * * @return external_function_parameters * @since Moodle 3.4 */ public static function execute_parameters() { return new external_function_parameters([ 'contexts' => new external_multiple_structure( new external_single_structure([ 'contextlevel' => new external_value( PARAM_ALPHA, 'The context level where the filters are: (coursecat, course, module)', ), 'instanceid' => new external_value(PARAM_INT, 'The instance id of item associated with the context.'), ]), 'The list of contexts to check.' ), ]); } /** * Returns the filters available in the given contexts. * * @param array $contexts the list of contexts to check * @return array with the filters information and warnings * @since Moodle 3.4 */ public static function execute($contexts) { global $CFG; require_once($CFG->libdir . '/filterlib.php'); $params = self::validate_parameters(self::execute_parameters(), ['contexts' => $contexts]); $filters = $warnings = []; foreach ($params['contexts'] as $contextinfo) { try { $context = self::get_context_from_params($contextinfo); self::validate_context($context); $contextinfo['contextid'] = $context->id; } catch (Exception $e) { $warnings[] = [ 'item' => 'context', 'itemid' => $contextinfo['instanceid'], 'warningcode' => $e->getCode(), 'message' => $e->getMessage(), ]; continue; } $contextfilters = filter_get_available_in_context($context); foreach ($contextfilters as $filter) { $filters[] = array_merge($contextinfo, (array) $filter); } } return [ 'filters' => $filters, 'warnings' => $warnings, ]; } /** * Returns description of get_available_in_context result value. * * @return external_single_structure * @since Moodle 3.4 */ public static function execute_returns() { return new external_single_structure([ 'filters' => new external_multiple_structure( new external_single_structure([ 'contextlevel' => new external_value( PARAM_ALPHA, 'The context level where the filters are: (coursecat, course, module).', ), 'instanceid' => new external_value(PARAM_INT, 'The instance id of item associated with the context.'), 'contextid' => new external_value(PARAM_INT, 'The context id.'), 'filter' => new external_value(PARAM_PLUGIN, 'Filter plugin name.'), 'localstate' => new external_value(PARAM_INT, 'Filter state: 1 for on, -1 for off, 0 if inherit.'), 'inheritedstate' => new external_value(PARAM_INT, '1 or 0 to use when localstate is set to inherit.'), ]), 'Available filters' ), 'warnings' => new external_warnings(), ]); } } PK|\G$߻get_all_states.phpnu[. namespace core_filters\external; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_single_structure; use core_external\external_multiple_structure; use core_external\external_value; use core_external\external_warnings; use context; /** * External function for getting all filter states. * * @package core_filters * @copyright 2024 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 4.4 */ class get_all_states extends external_api { /** * Webservice parameters. * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters([]); } /** * Main method of the external function. * * @return array containing all filter states. */ public static function execute(): array { global $CFG; require_once($CFG->libdir . '/filterlib.php'); $system = \context_system::instance(); external_api::validate_context($system); $filterstates = $warnings = []; $states = filter_get_all_states(); foreach ($states as $state) { $context = context::instance_by_id($state->contextid); $classname = \core\context_helper::parse_external_level($context->contextlevel); $filterstates[] = [ 'contextlevel' => $classname::get_short_name(), 'instanceid' => $context->instanceid, 'contextid' => $state->contextid, 'filter' => $state->filter, 'state' => $state->active, 'sortorder' => $state->sortorder, ]; } return [ 'filters' => $filterstates, 'warnings' => $warnings, ]; } /** * Webservice returns. * * @return external_single_structure */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'filters' => new external_multiple_structure( new external_single_structure([ 'contextlevel' => new external_value( PARAM_ALPHA, 'The context level where the filters are: (coursecat, course, module).', ), 'instanceid' => new external_value(PARAM_INT, 'The instance id of item associated with the context.'), 'contextid' => new external_value(PARAM_INT, 'The context id.'), 'filter' => new external_value(PARAM_PLUGIN, 'Filter plugin name.'), 'state' => new external_value(PARAM_INT, 'Filter state: 1 for on, -1 for off, -9999 if disabled.'), 'sortorder' => new external_value(PARAM_INT, 'Execution order.'), ]), 'All filters states' ), 'warnings' => new external_warnings(), ]); } } PK:}\`> *prepare_private_files_for_edition_test.phpnu[. namespace core_user\external; use core_external\external_api; /** * Tests for the prepare_private_files_for_edition class. * * @package core_user * @category external * @copyright 2024 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \core_user\external\prepare_private_files_for_edition */ final class prepare_private_files_for_edition_test extends \advanced_testcase { public function test_execute(): void { global $USER; $this->resetAfterTest(); $this->setAdminUser(); // Create some files in the user private file area. $filename = 'faketxt.txt'; $filerecordinline = [ 'contextid' => \context_user::instance($USER->id)->id, 'component' => 'user', 'filearea' => 'private', 'itemid' => 0, 'filepath' => '/', 'filename' => $filename, ]; $fs = get_file_storage(); $fs->create_file_from_string($filerecordinline, 'fake txt contents.'); $result = prepare_private_files_for_edition::execute(); $result = external_api::clean_returnvalue(prepare_private_files_for_edition::execute_returns(), $result); $draftitemid = $result['draftitemid']; $this->assertGreaterThan(0, $draftitemid); $this->assertCount(5, $result['areaoptions']); $this->assertEmpty($result['warnings']); // Check we get the expected user private files in the draft area. $files = file_get_drafarea_files($draftitemid); $this->assertCount(1, $files->list); $this->assertEquals($filename, $files->list[0]->filename); } /** * Test missing capabilities. */ public function test_execute_missing_capabilities(): void { global $CFG, $DB; $this->resetAfterTest(); $authrole = $DB->get_record('role', ['id' => $CFG->defaultuserroleid]); unassign_capability('moodle/user:manageownfiles', $authrole->id); accesslib_clear_all_caches_for_unit_testing(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $this->expectException('moodle_exception'); $result = prepare_private_files_for_edition::execute(0); } } PK:}\u_ppupdate_private_files_test.phpnu[. namespace core_user\external; use core_external\external_api; /** * Tests for the \core_user\external\update_private_files class. * * @package core_user * @category external * @copyright 2024 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \core_user\external\update_private_files */ final class update_private_files_test extends \advanced_testcase { /** * Test base cases. */ public function test_execute(): void { global $CFG; require_once($CFG->dirroot . '/files/externallib.php'); $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $anotheruser = $this->getDataGenerator()->create_user(); $this->setUser($user); $context = \context_user::instance($user->id); // Create one file in the user private file area. $filename = 'faketxt.txt'; $filerecordinline = [ 'contextid' => $context->id, 'component' => 'user', 'filearea' => 'private', 'itemid' => 0, 'filepath' => '/', 'filename' => $filename, ]; $fs = get_file_storage(); $fs->create_file_from_string($filerecordinline, 'fake txt contents.'); // Retrieve draft area with existing files. $result = prepare_private_files_for_edition::execute(); $result = external_api::clean_returnvalue(prepare_private_files_for_edition::execute_returns(), $result); $draftitemid = $result['draftitemid']; // Add one file to the draft area reusing previous structure. $newfilename = 'newfaketxt.txt'; $filerecordinline['itemid'] = $draftitemid; $filerecordinline['filearea'] = 'draft'; $filerecordinline['filename'] = $newfilename; $fs->create_file_from_string($filerecordinline, 'new fake txt contents.'); $files = file_get_drafarea_files($draftitemid); $this->assertCount(2, $files->list); // 2 files in the draft area. // Update the private files with the new files. $result = update_private_files::execute($draftitemid); $result = external_api::clean_returnvalue(update_private_files::execute_returns(), $result); $this->assertTrue($result['status']); $this->assertEmpty($result['warnings']); // Check that the new files are in the private file area. $files = \core_files_external::get_files($context->id, 'user', 'private', 0, '/', ''); $this->assertCount(2, $files['files']); // 2 files in the private area. // Now, try deleting one. $result = prepare_private_files_for_edition::execute(); $result = external_api::clean_returnvalue(prepare_private_files_for_edition::execute_returns(), $result); $draftitemid = $result['draftitemid']; \core_files\external\delete\draft::execute($draftitemid, [ [ 'filepath' => '/', 'filename' => $newfilename, ], ]); // Update to force deletion. $result = update_private_files::execute($draftitemid); $result = external_api::clean_returnvalue(update_private_files::execute_returns(), $result); $this->assertTrue($result['status']); $this->assertEmpty($result['warnings']); // Check we only have one now. $files = \core_files_external::get_files($context->id, 'user', 'private', 0, '/', ''); $this->assertCount(1, $files['files']); $this->assertEquals($filename, $files['files'][0]['filename']); // Use other's user draft item area. $result = prepare_private_files_for_edition::execute(); $result = external_api::clean_returnvalue(prepare_private_files_for_edition::execute_returns(), $result); $draftitemid = $result['draftitemid']; $this->setUser($anotheruser); $this->expectException('moodle_exception'); update_private_files::execute($draftitemid); } /** * Test invalid draftitemid. */ public function test_execute_invalid_draftitemid(): void { $this->resetAfterTest(); $this->setAdminUser(); $this->expectException('moodle_exception'); update_private_files::execute(0); } /** * Test quota reached. */ public function test_execute_quota_reached(): void { global $CFG; $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $context = \context_user::instance($user->id); // Force the quota so we are sure it won't be space to add the new file. $fileareainfo = file_get_file_area_info($context->id, 'user', 'private'); $CFG->userquota = 1; $result = prepare_private_files_for_edition::execute(); $result = external_api::clean_returnvalue(prepare_private_files_for_edition::execute_returns(), $result); $draftitemid = $result['draftitemid']; $filerecordinline = [ 'contextid' => $context->id, 'component' => 'user', 'filearea' => 'draft', 'itemid' => $draftitemid, 'filepath' => '/', 'filename' => 'faketxt.txt', ]; $fs = get_file_storage(); $fs->create_file_from_string($filerecordinline, 'new fake txt contents.'); $result = update_private_files::execute($draftitemid); $result = external_api::clean_returnvalue(update_private_files::execute_returns(), $result); // We should get a warning as because of quota we can add files. $this->assertFalse($result['status']); $this->assertNotEmpty($result['warnings']); } /** * Test missing capabilities. */ public function test_execute_missing_capabilities(): void { global $CFG, $DB; $this->resetAfterTest(); $authrole = $DB->get_record('role', ['id' => $CFG->defaultuserroleid]); unassign_capability('moodle/user:manageownfiles', $authrole->id); accesslib_clear_all_caches_for_unit_testing(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $this->expectException('moodle_exception'); $result = update_private_files::execute(0); } } PK:}\(1T&update_user_device_public_key_test.phpnu[. namespace core_user\external; use core_external\external_api; use stdClass; /** * Tests for the devicekey class. * * @package core_user * @covers \core_user\external\update_user_device_public_key */ final class update_user_device_public_key_test extends \advanced_testcase { /** * Helper to create a device record. * * @return stdClass */ protected function create_device_record(): stdClass { global $USER, $DB; $device = (object) [ 'appid' => 'com.moodle.moodlemobile', 'name' => 'occam', 'model' => 'Nexus 4', 'platform' => 'Android', 'version' => '4.2.2', 'pushid' => 'apushdkasdfj4835', 'uuid' => 'ABCDE3723ksdfhasfaasef859', 'userid' => $USER->id, 'timecreated' => time(), 'timemodified' => time(), ]; $device->id = $DB->insert_record('user_devices', $device); return $device; } public function test_execute(): void { $this->resetAfterTest(); $this->setAdminUser(); $device = $this->create_device_record(); $devicekeypair = sodium_crypto_box_keypair(); $publickey = sodium_bin2base64( sodium_crypto_box_publickey($devicekeypair), SODIUM_BASE64_VARIANT_ORIGINAL ); // Test sending a key to a valid device. $result = update_user_device_public_key::execute( $device->uuid, $device->appid, $publickey, ); $result = external_api::clean_returnvalue(update_user_device_public_key::execute_returns(), $result); $this->assertTrue($result['status']); $this->assertEmpty($result['warnings']); } public function test_execute_with_invalid_device_appid(): void { $this->resetAfterTest(); $this->setAdminUser(); $device = $this->create_device_record(); $devicekeypair = sodium_crypto_box_keypair(); $publickey = sodium_bin2base64( sodium_crypto_box_publickey($devicekeypair), SODIUM_BASE64_VARIANT_ORIGINAL ); // Invalid appid. $result = update_user_device_public_key::execute( $device->uuid, 'invalidappid', $publickey, ); $result = external_api::clean_returnvalue(update_user_device_public_key::execute_returns(), $result); $this->assertFalse($result['status']); $this->assertNotEmpty($result['warnings']); } public function test_execute_with_invalid_device_uuid(): void { $this->resetAfterTest(); $this->setAdminUser(); $device = $this->create_device_record(); $devicekeypair = sodium_crypto_box_keypair(); $publickey = sodium_bin2base64( sodium_crypto_box_publickey($devicekeypair), SODIUM_BASE64_VARIANT_ORIGINAL ); // Invalid appid. $result = update_user_device_public_key::execute( 'invaliduuid', $device->appid, $publickey, ); $result = external_api::clean_returnvalue(update_user_device_public_key::execute_returns(), $result); $this->assertFalse($result['status']); $this->assertNotEmpty($result['warnings']); } } PK\{//stored_file_exporter_test.phpnu[. namespace core_files\external; use advanced_testcase; use context_user; /** * Unit tests for stored file exporter * * @package core_files * @covers \core_files\external\stored_file_exporter * @copyright 2023 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class stored_file_exporter_test extends advanced_testcase { /** * Test exported data structure */ public function test_export(): void { global $PAGE, $USER, $CFG; $this->resetAfterTest(); $this->setAdminUser(); $contextuser = context_user::instance($USER->id); $file = get_file_storage()->create_file_from_string([ 'contextid' => $contextuser->id, 'userid' => $USER->id, 'component' => 'user', 'filearea' => 'draft', 'itemid' => file_get_unused_draft_itemid(), 'filepath' => '/', 'filename' => 'Hi.txt', ], 'Hello'); $exporter = new stored_file_exporter($file, ['context' => $contextuser]); $export = $exporter->export($PAGE->get_renderer('core')); $this->assertEquals((object) [ 'contextid' => $file->get_contextid(), 'component' => $file->get_component(), 'filearea' => $file->get_filearea(), 'itemid' => $file->get_itemid(), 'filepath' => $file->get_filepath(), 'filename' => $file->get_filename(), 'isdir' => false, 'isimage' => false, 'timemodified' => $file->get_timemodified(), 'timecreated' => $file->get_timecreated(), 'filesize' => $file->get_filesize(), 'author' => $file->get_author(), 'license' => $file->get_license(), 'filenameshort' => $file->get_filename(), 'filesizeformatted' => display_size($file->get_filesize()), 'icon' => 'f/text', 'timecreatedformatted' => userdate($file->get_timecreated()), 'timemodifiedformatted' => userdate($file->get_timemodified()), 'url' => "{$CFG->wwwroot}/pluginfile.php/{$contextuser->id}/user/draft/{$file->get_itemid()}/Hi.txt?forcedownload=1", ], $export); } /** * Data provider for {@see test_export_filenameshort} * * @return array[] */ public static function export_filenameshort_provider(): array { return [ // Long filenames (30 characters), with extensions of varying length. ['Lorem ipsum dolor sit amet sit.c', 'Lorem ipsum dolor sit...c'], ['Lorem ipsum dolor sit amet sit.txt', 'Lorem ipsum dolor s...txt'], ['Lorem ipsum dolor sit amet sit.docx', 'Lorem ipsum dolor ...docx'], // Multi-byte filenames. ['Мазитов А.З. практика тусур.py', 'Мазитов А.З. практик...py'], ]; } /** * Test exporting shortened filename * * @param string $filename * @param string $expected * * @dataProvider export_filenameshort_provider */ public function test_export_filenameshort(string $filename, string $expected): void { global $PAGE, $USER; $this->resetAfterTest(); $this->setAdminUser(); $contextuser = context_user::instance($USER->id); $file = get_file_storage()->create_file_from_string([ 'contextid' => $contextuser->id, 'userid' => $USER->id, 'component' => 'user', 'filearea' => 'draft', 'itemid' => file_get_unused_draft_itemid(), 'filepath' => '/', 'filename' => $filename, ], 'Hello'); $exporter = new stored_file_exporter($file, ['context' => $contextuser]); $export = $exporter->export($PAGE->get_renderer('core')); $this->assertEquals($expected, $export->filenameshort); } } PK/\texternal_test.phpnu[. /** * External tests. * * @package core_competency * @copyright 2016 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency\external; use core\invalid_persistent_exception; use core_competency\api; use core_competency\course_competency_settings; use core_competency\external; use core_competency\plan; use core_competency\plan_competency; use core_competency\related_competency; use core_competency\template; use core_competency\template_competency; use core_competency\user_competency; use core_competency\user_competency_plan; use core_external\external_api; use externallib_advanced_testcase; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); /** * External testcase. * * @package core_competency * @copyright 2016 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class external_test extends externallib_advanced_testcase { /** @var \stdClass $creator User with enough permissions to create insystem context. */ protected $creator = null; /** @var \stdClass $learningplancreator User with enough permissions to create incategory context. */ protected $catcreator = null; /** @var \stdClass $category Category */ protected $category = null; /** @var \stdClass $user User with enough permissions to view insystem context */ protected $user = null; /** @var \stdClass $catuser User with enough permissions to view incategory context */ protected $catuser = null; /** @var int Creator role id */ protected $creatorrole = null; /** @var int User role id */ protected $userrole = null; /** @var \stdClass $scale1 Scale */ protected $scale1 = null; /** @var \stdClass $scale2 Scale */ protected $scale2 = null; /** @var \stdClass $scale3 Scale */ protected $scale3 = null; /** @var \stdClass $scale4 Scale */ protected $scale4 = null; /** @var string scaleconfiguration */ protected $scaleconfiguration1 = null; /** @var string scaleconfiguration */ protected $scaleconfiguration2 = null; /** @var string catscaleconfiguration */ protected $scaleconfiguration3 = null; /** @var string category scale configuration. */ protected $scaleconfiguration4 = null; /** @var \core_course_category course category record. */ protected $othercategory = null; /** * Setup function- we will create a course and add an assign instance to it. */ protected function setUp(): void { global $DB, $CFG; parent::setUp(); $this->resetAfterTest(true); // Create some users. $creator = $this->getDataGenerator()->create_user(); $user = $this->getDataGenerator()->create_user(); $catuser = $this->getDataGenerator()->create_user(); $category = $this->getDataGenerator()->create_category(); $othercategory = $this->getDataGenerator()->create_category(); $catcreator = $this->getDataGenerator()->create_user(); $syscontext = \context_system::instance(); $catcontext = \context_coursecat::instance($category->id); $othercatcontext = \context_coursecat::instance($othercategory->id); // Fetching default authenticated user role. $authrole = $DB->get_record('role', array('id' => $CFG->defaultuserroleid)); // Reset all default authenticated users permissions. unassign_capability('moodle/competency:competencygrade', $authrole->id); unassign_capability('moodle/competency:competencymanage', $authrole->id); unassign_capability('moodle/competency:competencyview', $authrole->id); unassign_capability('moodle/competency:planmanage', $authrole->id); unassign_capability('moodle/competency:planmanagedraft', $authrole->id); unassign_capability('moodle/competency:planmanageown', $authrole->id); unassign_capability('moodle/competency:planview', $authrole->id); unassign_capability('moodle/competency:planviewdraft', $authrole->id); unassign_capability('moodle/competency:planviewown', $authrole->id); unassign_capability('moodle/competency:planviewowndraft', $authrole->id); unassign_capability('moodle/competency:templatemanage', $authrole->id); unassign_capability('moodle/competency:templateview', $authrole->id); unassign_capability('moodle/cohort:manage', $authrole->id); unassign_capability('moodle/competency:coursecompetencyconfigure', $authrole->id); // Creating specific roles. $this->creatorrole = create_role('Creator role', 'creatorrole', 'learning plan creator role description'); $this->userrole = create_role('User role', 'userrole', 'learning plan user role description'); assign_capability('moodle/competency:competencymanage', CAP_ALLOW, $this->creatorrole, $syscontext->id); assign_capability('moodle/competency:coursecompetencyconfigure', CAP_ALLOW, $this->creatorrole, $syscontext->id); assign_capability('moodle/competency:competencyview', CAP_ALLOW, $this->userrole, $syscontext->id); assign_capability('moodle/competency:planmanage', CAP_ALLOW, $this->creatorrole, $syscontext->id); assign_capability('moodle/competency:planmanagedraft', CAP_ALLOW, $this->creatorrole, $syscontext->id); assign_capability('moodle/competency:planmanageown', CAP_ALLOW, $this->creatorrole, $syscontext->id); assign_capability('moodle/competency:planview', CAP_ALLOW, $this->creatorrole, $syscontext->id); assign_capability('moodle/competency:planviewdraft', CAP_ALLOW, $this->creatorrole, $syscontext->id); assign_capability('moodle/competency:templatemanage', CAP_ALLOW, $this->creatorrole, $syscontext->id); assign_capability('moodle/competency:competencygrade', CAP_ALLOW, $this->creatorrole, $syscontext->id); assign_capability('moodle/cohort:manage', CAP_ALLOW, $this->creatorrole, $syscontext->id); assign_capability('moodle/competency:templateview', CAP_ALLOW, $this->userrole, $syscontext->id); assign_capability('moodle/competency:planviewown', CAP_ALLOW, $this->userrole, $syscontext->id); assign_capability('moodle/competency:planviewowndraft', CAP_ALLOW, $this->userrole, $syscontext->id); role_assign($this->creatorrole, $creator->id, $syscontext->id); role_assign($this->creatorrole, $catcreator->id, $catcontext->id); role_assign($this->userrole, $user->id, $syscontext->id); role_assign($this->userrole, $catuser->id, $catcontext->id); $this->creator = $creator; $this->catcreator = $catcreator; $this->user = $user; $this->catuser = $catuser; $this->category = $category; $this->othercategory = $othercategory; $this->scale1 = $this->getDataGenerator()->create_scale(array("scale" => "value1, value2")); $this->scale2 = $this->getDataGenerator()->create_scale(array("scale" => "value3, value4")); $this->scale3 = $this->getDataGenerator()->create_scale(array("scale" => "value5, value6")); $this->scale4 = $this->getDataGenerator()->create_scale(array("scale" => "value7, value8")); $this->scaleconfiguration1 = '[{"scaleid":"'.$this->scale1->id.'"},' . '{"name":"value1","id":1,"scaledefault":1,"proficient":0},' . '{"name":"value2","id":2,"scaledefault":0,"proficient":1}]'; $this->scaleconfiguration2 = '[{"scaleid":"'.$this->scale2->id.'"},' . '{"name":"value3","id":1,"scaledefault":1,"proficient":0},' . '{"name":"value4","id":2,"scaledefault":0,"proficient":1}]'; $this->scaleconfiguration3 = '[{"scaleid":"'.$this->scale3->id.'"},' . '{"name":"value5","id":1,"scaledefault":1,"proficient":0},' . '{"name":"value6","id":2,"scaledefault":0,"proficient":1}]'; $this->scaleconfiguration4 = '[{"scaleid":"'.$this->scale4->id.'"},'. '{"name":"value8","id":1,"scaledefault":1,"proficient":0},' . '{"name":"value8","id":2,"scaledefault":0,"proficient":1}]'; accesslib_clear_all_caches_for_unit_testing(); } protected function create_competency_framework($number = 1, $system = true) { $scalename = 'scale' . $number; $scalepropname = 'scaleconfiguration' . $number; $framework = array( 'shortname' => 'shortname' . $number, 'idnumber' => 'idnumber' . $number, 'description' => 'description' . $number, 'descriptionformat' => FORMAT_HTML, 'scaleid' => $this->$scalename->id, 'scaleconfiguration' => $this->$scalepropname, 'visible' => true, 'contextid' => $system ? \context_system::instance()->id : \context_coursecat::instance($this->category->id)->id ); $result = external::create_competency_framework($framework); return (object) external_api::clean_returnvalue(external::create_competency_framework_returns(), $result); } protected function create_plan($number, $userid, $templateid, $status, $duedate) { $plan = array( 'name' => 'name' . $number, 'description' => 'description' . $number, 'descriptionformat' => FORMAT_HTML, 'userid' => $userid, 'templateid' => empty($templateid) ? null : $templateid, 'status' => $status, 'duedate' => $duedate ); $result = external::create_plan($plan); return (object) external_api::clean_returnvalue(external::create_plan_returns(), $result); } protected function create_template($number, $system) { $template = array( 'shortname' => 'shortname' . $number, 'description' => 'description' . $number, 'descriptionformat' => FORMAT_HTML, 'duedate' => 0, 'visible' => true, 'contextid' => $system ? \context_system::instance()->id : \context_coursecat::instance($this->category->id)->id ); $result = external::create_template($template); return (object) external_api::clean_returnvalue(external::create_template_returns(), $result); } protected function update_template($templateid, $number) { $template = array( 'id' => $templateid, 'shortname' => 'shortname' . $number, 'description' => 'description' . $number, 'descriptionformat' => FORMAT_HTML, 'visible' => true ); $result = external::update_template($template); return external_api::clean_returnvalue(external::update_template_returns(), $result); } protected function update_plan($planid, $number, $userid, $templateid, $status, $duedate) { $plan = array( 'id' => $planid, 'name' => 'name' . $number, 'description' => 'description' . $number, 'descriptionformat' => FORMAT_HTML, 'userid' => $userid, 'templateid' => $templateid, 'status' => $status, 'duedate' => $duedate ); $result = external::update_plan($plan); return external_api::clean_returnvalue(external::update_plan_returns(), $result); } protected function update_competency_framework($id, $number = 1, $system = true) { $scalename = 'scale' . $number; $scalepropname = 'scaleconfiguration' . $number; $framework = array( 'id' => $id, 'shortname' => 'shortname' . $number, 'idnumber' => 'idnumber' . $number, 'description' => 'description' . $number, 'descriptionformat' => FORMAT_HTML, 'scaleid' => $this->$scalename->id, 'scaleconfiguration' => $this->$scalepropname, 'visible' => true, 'contextid' => $system ? \context_system::instance()->id : \context_coursecat::instance($this->category->id)->id ); $result = external::update_competency_framework($framework); return external_api::clean_returnvalue(external::update_competency_framework_returns(), $result); } protected function create_competency($number, $frameworkid) { $competency = array( 'shortname' => 'shortname' . $number, 'idnumber' => 'idnumber' . $number, 'description' => 'description' . $number, 'descriptionformat' => FORMAT_HTML, 'competencyframeworkid' => $frameworkid ); $result = external::create_competency($competency); return (object) external_api::clean_returnvalue(external::create_competency_returns(), $result); } protected function update_competency($id, $number) { $competency = array( 'id' => $id, 'shortname' => 'shortname' . $number, 'idnumber' => 'idnumber' . $number, 'description' => 'description' . $number, 'descriptionformat' => FORMAT_HTML ); $result = external::update_competency($competency); return external_api::clean_returnvalue(external::update_competency_returns(), $result); } /** * Test we can't create a competency framework with only read permissions. */ public function test_create_competency_frameworks_with_read_permissions(): void { $this->setUser($this->user); $this->expectException(\required_capability_exception::class); $result = $this->create_competency_framework(1, true); } /** * Test we can't create a competency framework with only read permissions. */ public function test_create_competency_frameworks_with_read_permissions_in_category(): void { $this->setUser($this->catuser); $this->expectException(\required_capability_exception::class); $result = $this->create_competency_framework(1, false); } /** * Test we can create a competency framework with manage permissions. */ public function test_create_competency_frameworks_with_manage_permissions(): void { $this->setUser($this->creator); $result = $this->create_competency_framework(1, true); $this->assertGreaterThan(0, $result->timecreated); $this->assertGreaterThan(0, $result->timemodified); $this->assertEquals($this->creator->id, $result->usermodified); $this->assertEquals('shortname1', $result->shortname); $this->assertEquals('idnumber1', $result->idnumber); $this->assertEquals('description1', $result->description); $this->assertEquals(FORMAT_HTML, $result->descriptionformat); $this->assertEquals($this->scale1->id, $result->scaleid); $this->assertEquals($this->scaleconfiguration1, $result->scaleconfiguration); $this->assertEquals(true, $result->visible); } /** * Test we can create a competency framework with manage permissions. */ public function test_create_competency_frameworks_with_manage_permissions_in_category(): void { $this->setUser($this->catcreator); $result = $this->create_competency_framework(1, false); $this->assertGreaterThan(0, $result->timecreated); $this->assertGreaterThan(0, $result->timemodified); $this->assertEquals($this->catcreator->id, $result->usermodified); $this->assertEquals('shortname1', $result->shortname); $this->assertEquals('idnumber1', $result->idnumber); $this->assertEquals('description1', $result->description); $this->assertEquals(FORMAT_HTML, $result->descriptionformat); $this->assertEquals($this->scale1->id, $result->scaleid); $this->assertEquals($this->scaleconfiguration1, $result->scaleconfiguration); $this->assertEquals(true, $result->visible); try { $result = $this->create_competency_framework(1, true); $this->fail('User cannot create a framework at system level.'); } catch (\required_capability_exception $e) { // All good. } } /** * Test we cannot create a competency framework with nasty data. */ public function test_create_competency_frameworks_with_nasty_data(): void { $this->setUser($this->creator); $framework = array( 'shortname' => 'short', 'idnumber' => 'id;"number', 'description' => 'de<>\\..scription', 'descriptionformat' => FORMAT_HTML, 'scaleid' => $this->scale1->id, 'scaleconfiguration' => $this->scaleconfiguration1, 'visible' => true, 'contextid' => \context_system::instance()->id ); $this->expectException(\invalid_parameter_exception::class); $result = external::create_competency_framework($framework); } /** * Test we can read a competency framework with manage permissions. */ public function test_read_competency_frameworks_with_manage_permissions(): void { $this->setUser($this->creator); $result = $this->create_competency_framework(1, true); $id = $result->id; $result = external::read_competency_framework($id); $result = (object) external_api::clean_returnvalue(external::read_competency_framework_returns(), $result); $this->assertGreaterThan(0, $result->timecreated); $this->assertGreaterThan(0, $result->timemodified); $this->assertEquals($this->creator->id, $result->usermodified); $this->assertEquals('shortname1', $result->shortname); $this->assertEquals('idnumber1', $result->idnumber); $this->assertEquals('description1', $result->description); $this->assertEquals(FORMAT_HTML, $result->descriptionformat); $this->assertEquals($this->scale1->id, $result->scaleid); $this->assertEquals($this->scaleconfiguration1, $result->scaleconfiguration); $this->assertEquals(true, $result->visible); } /** * Test we can read a competency framework with manage permissions. */ public function test_read_competency_frameworks_with_manage_permissions_in_category(): void { $this->setUser($this->creator); $insystem = $this->create_competency_framework(1, true); $incat = $this->create_competency_framework(2, false); $this->setUser($this->catcreator); $id = $incat->id; $result = external::read_competency_framework($id); $result = (object) external_api::clean_returnvalue(external::read_competency_framework_returns(), $result); $this->assertGreaterThan(0, $result->timecreated); $this->assertGreaterThan(0, $result->timemodified); $this->assertEquals($this->creator->id, $result->usermodified); $this->assertEquals('shortname2', $result->shortname); $this->assertEquals('idnumber2', $result->idnumber); $this->assertEquals('description2', $result->description); $this->assertEquals(FORMAT_HTML, $result->descriptionformat); $this->assertEquals($this->scale2->id, $result->scaleid); $this->assertEquals($this->scaleconfiguration2, $result->scaleconfiguration); $this->assertEquals(true, $result->visible); try { $id = $insystem->id; $result = external::read_competency_framework($id); $result = (object) external_api::clean_returnvalue(external::read_competency_framework_returns(), $result); $this->fail('User cannot read a framework at system level.'); } catch (\required_capability_exception $e) { // All good. } } /** * Test we can read a competency framework with read permissions. */ public function test_read_competency_frameworks_with_read_permissions(): void { $this->setUser($this->creator); $result = $this->create_competency_framework(1, true); // Switch users to someone with less permissions. $this->setUser($this->user); $id = $result->id; $result = external::read_competency_framework($id); $result = (object) external_api::clean_returnvalue(external::read_competency_framework_returns(), $result); $this->assertGreaterThan(0, $result->timecreated); $this->assertGreaterThan(0, $result->timemodified); $this->assertEquals($this->creator->id, $result->usermodified); $this->assertEquals('shortname1', $result->shortname); $this->assertEquals('idnumber1', $result->idnumber); $this->assertEquals('description1', $result->description); $this->assertEquals(FORMAT_HTML, $result->descriptionformat); $this->assertEquals($this->scale1->id, $result->scaleid); $this->assertEquals($this->scaleconfiguration1, $result->scaleconfiguration); $this->assertEquals(true, $result->visible); } /** * Test we can read a competency framework with read permissions. */ public function test_read_competency_frameworks_with_read_permissions_in_category(): void { $this->setUser($this->creator); $insystem = $this->create_competency_framework(1, true); $incat = $this->create_competency_framework(2, false); // Switch users to someone with less permissions. $this->setUser($this->catuser); $id = $incat->id; $result = external::read_competency_framework($id); $result = (object) external_api::clean_returnvalue(external::read_competency_framework_returns(), $result); $this->assertGreaterThan(0, $result->timecreated); $this->assertGreaterThan(0, $result->timemodified); $this->assertEquals($this->creator->id, $result->usermodified); $this->assertEquals('shortname2', $result->shortname); $this->assertEquals('idnumber2', $result->idnumber); $this->assertEquals('description2', $result->description); $this->assertEquals(FORMAT_HTML, $result->descriptionformat); $this->assertEquals($this->scale2->id, $result->scaleid); $this->assertEquals($this->scaleconfiguration2, $result->scaleconfiguration); $this->assertEquals(true, $result->visible); // Switching to user with no permissions. try { $result = external::read_competency_framework($insystem->id); $this->fail('Current user cannot should not be able to read the framework.'); } catch (\required_capability_exception $e) { // All good. } } /** * Test we can delete a competency framework with manage permissions. */ public function test_delete_competency_frameworks_with_manage_permissions(): void { $this->setUser($this->creator); $result = $this->create_competency_framework(1, true); $id = $result->id; $result = external::delete_competency_framework($id); $result = external_api::clean_returnvalue(external::delete_competency_framework_returns(), $result); $this->assertTrue($result); } /** * Test we can delete a competency framework with manage permissions. */ public function test_delete_competency_frameworks_with_manage_permissions_in_category(): void { $this->setUser($this->creator); $insystem = $this->create_competency_framework(1, true); $incat = $this->create_competency_framework(2, false); $this->setUser($this->catcreator); $id = $incat->id; $result = external::delete_competency_framework($id); $result = external_api::clean_returnvalue(external::delete_competency_framework_returns(), $result); $this->assertTrue($result); try { $id = $insystem->id; $result = external::delete_competency_framework($id); $result = external_api::clean_returnvalue(external::delete_competency_framework_returns(), $result); $this->fail('Current user cannot should not be able to delete the framework.'); } catch (\required_capability_exception $e) { // All good. } } /** * Test we can delete a competency framework with read permissions. */ public function test_delete_competency_frameworks_with_read_permissions(): void { $this->setUser($this->creator); $result = $this->create_competency_framework(1, true); $id = $result->id; // Switch users to someone with less permissions. $this->setUser($this->user); $this->expectException(\required_capability_exception::class); $result = external::delete_competency_framework($id); } /** * Test we can update a competency framework with manage permissions. */ public function test_update_competency_frameworks_with_manage_permissions(): void { $this->setUser($this->creator); $result = $this->create_competency_framework(1, true); $result = $this->update_competency_framework($result->id, 2, true); $this->assertTrue($result); } /** * Test we can update a competency framework with manage permissions. */ public function test_update_competency_frameworks_with_manage_permissions_in_category(): void { $this->setUser($this->creator); $insystem = $this->create_competency_framework(1, true); $incat = $this->create_competency_framework(2, false); $this->setUser($this->catcreator); $id = $incat->id; $result = $this->update_competency_framework($incat->id, 3, false); $this->assertTrue($result); try { $result = $this->update_competency_framework($insystem->id, 4, true); $this->fail('Current user should not be able to update the framework.'); } catch (\required_capability_exception $e) { // All good. } } public function test_update_framework_scale(): void { $this->setUser($this->creator); $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); $s1 = $this->getDataGenerator()->create_scale(); $f1 = $lpg->create_framework(array('scaleid' => $s1->id)); $f2 = $lpg->create_framework(array('scaleid' => $s1->id)); $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get('id'))); $c2 = $lpg->create_competency(array('competencyframeworkid' => $f2->get('id'))); $this->assertEquals($s1->id, $f1->get('scaleid')); // Make the scale of f2 being used. $lpg->create_user_competency(array('userid' => $this->user->id, 'competencyid' => $c2->get('id'))); // Changing the framework where the scale is not used. $result = $this->update_competency_framework($f1->get('id'), 3, true); $f1 = new \core_competency\competency_framework($f1->get('id')); $this->assertEquals($this->scale3->id, $f1->get('scaleid')); // Changing the framework where the scale is used. try { $result = $this->update_competency_framework($f2->get('id'), 4, true); $this->fail('The scale cannot be changed once used.'); } catch (invalid_persistent_exception $e) { $this->assertMatchesRegularExpression('/scaleid/', $e->getMessage()); } } /** * Test we can update a competency framework with read permissions. */ public function test_update_competency_frameworks_with_read_permissions(): void { $this->setUser($this->creator); $result = $this->create_competency_framework(1, true); $this->setUser($this->user); $this->expectException(\required_capability_exception::class); $result = $this->update_competency_framework($result->id, 2, true); } /** * Test we can list and count competency frameworks with manage permissions. */ public function test_list_and_count_competency_frameworks_with_manage_permissions(): void { $this->setUser($this->creator); $result = $this->create_competency_framework(1, true); $result = $this->create_competency_framework(2, true); $result = $this->create_competency_framework(3, true); $result = $this->create_competency_framework(4, false); $result = external::count_competency_frameworks(array('contextid' => \context_system::instance()->id), 'self'); $result = external_api::clean_returnvalue(external::count_competency_frameworks_returns(), $result); $this->assertEquals($result, 3); $result = external::list_competency_frameworks('shortname', 'ASC', 0, 10, array('contextid' => \context_system::instance()->id), 'self', false); $result = external_api::clean_returnvalue(external::list_competency_frameworks_returns(), $result); $this->assertEquals(count($result), 3); $result = (object) $result[0]; $this->assertGreaterThan(0, $result->timecreated); $this->assertGreaterThan(0, $result->timemodified); $this->assertEquals($this->creator->id, $result->usermodified); $this->assertEquals('shortname1', $result->shortname); $this->assertEquals('idnumber1', $result->idnumber); $this->assertEquals('description1', $result->description); $this->assertEquals(FORMAT_HTML, $result->descriptionformat); $this->assertEquals($this->scale1->id, $result->scaleid); $this->assertEquals($this->scaleconfiguration1, $result->scaleconfiguration); $this->assertEquals(true, $result->visible); } public function test_list_competency_frameworks_with_query(): void { $this->setUser($this->creator); $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); $framework1 = $lpg->create_framework(array( 'shortname' => 'shortname_beetroot', 'idnumber' => 'idnumber_cinnamon', 'description' => 'description', 'descriptionformat' => FORMAT_HTML, 'visible' => true, 'contextid' => \context_system::instance()->id )); $framework2 = $lpg->create_framework(array( 'shortname' => 'shortname_citrus', 'idnumber' => 'idnumber_beer', 'description' => 'description', 'descriptionformat' => FORMAT_HTML, 'visible' => true, 'contextid' => \context_system::instance()->id )); // Search on both ID number and shortname. $result = external::list_competency_frameworks('shortname', 'ASC', 0, 10, array('contextid' => \context_system::instance()->id), 'self', false, 'bee'); $result = external_api::clean_returnvalue(external::list_competency_frameworks_returns(), $result); $this->assertCount(2, $result); $f = (object) array_shift($result); $this->assertEquals($framework1->get('id'), $f->id); $f = (object) array_shift($result); $this->assertEquals($framework2->get('id'), $f->id); // Search on ID number. $result = external::list_competency_frameworks('shortname', 'ASC', 0, 10, array('contextid' => \context_system::instance()->id), 'self', false, 'beer'); $result = external_api::clean_returnvalue(external::list_competency_frameworks_returns(), $result); $this->assertCount(1, $result); $f = (object) array_shift($result); $this->assertEquals($framework2->get('id'), $f->id); // Search on shortname. $result = external::list_competency_frameworks('shortname', 'ASC', 0, 10, array('contextid' => \context_system::instance()->id), 'self', false, 'cinnamon'); $result = external_api::clean_returnvalue(external::list_competency_frameworks_returns(), $result); $this->assertCount(1, $result); $f = (object) array_shift($result); $this->assertEquals($framework1->get('id'), $f->id); // No match. $result = external::list_competency_frameworks('shortname', 'ASC', 0, 10, array('contextid' => \context_system::instance()->id), 'self', false, 'pwnd!'); $result = external_api::clean_returnvalue(external::list_competency_frameworks_returns(), $result); $this->assertCount(0, $result); } /** * Test we can list and count competency frameworks with read permissions. */ public function test_list_and_count_competency_frameworks_with_read_permissions(): void { $this->setUser($this->creator); $result = $this->create_competency_framework(1, true); $result = $this->create_competency_framework(2, true); $result = $this->create_competency_framework(3, true); $result = $this->create_competency_framework(4, false); $this->setUser($this->user); $result = external::count_competency_frameworks(array('contextid' => \context_system::instance()->id), 'self'); $result = external_api::clean_returnvalue(external::count_competency_frameworks_returns(), $result); $this->assertEquals($result, 3); $result = external::list_competency_frameworks('shortname', 'ASC', 0, 10, array('contextid' => \context_system::instance()->id), 'self', false); $result = external_api::clean_returnvalue(external::list_competency_frameworks_returns(), $result); $this->assertEquals(count($result), 3); $result = (object) $result[0]; $this->assertGreaterThan(0, $result->timecreated); $this->assertGreaterThan(0, $result->timemodified); $this->assertEquals($this->creator->id, $result->usermodified); $this->assertEquals('shortname1', $result->shortname); $this->assertEquals('idnumber1', $result->idnumber); $this->assertEquals('description1', $result->description); $this->assertEquals(FORMAT_HTML, $result->descriptionformat); $this->assertEquals($this->scale1->id, $result->scaleid); $this->assertEquals($this->scaleconfiguration1, $result->scaleconfiguration); $this->assertEquals(true, $result->visible); } /** * Test we can't create a competency with only read permissions. */ public function test_create_competency_with_read_permissions(): void { $framework = $this->getDataGenerator()->get_plugin_generator('core_competency')->create_framework(); $this->setUser($this->user); $this->expectException(\required_capability_exception::class); $competency = $this->create_competency(1, $framework->get('id')); } /** * Test we can create a competency with manage permissions. */ public function test_create_competency_with_manage_permissions(): void { $this->setUser($this->creator); $framework = $this->create_competency_framework(1, true); $competency = $this->create_competency(1, $framework->id); $this->assertGreaterThan(0, $competency->timecreated); $this->assertGreaterThan(0, $competency->timemodified); $this->assertEquals($this->creator->id, $competency->usermodified); $this->assertEquals('shortname1', $competency->shortname); $this->assertEquals('idnumber1', $competency->idnumber); $this->assertEquals('description1', $competency->description); $this->assertEquals(FORMAT_HTML, $competency->descriptionformat); $this->assertEquals(0, $competency->parentid); $this->assertEquals($framework->id, $competency->competencyframeworkid); } /** * Test we can create a competency with manage permissions. */ public function test_create_competency_with_manage_permissions_in_category(): void { $this->setUser($this->creator); $insystem = $this->create_competency_framework(1, true); $incat = $this->create_competency_framework(2, false); $this->setUser($this->catcreator); $competency = $this->create_competency(1, $incat->id); $this->assertGreaterThan(0, $competency->timecreated); $this->assertGreaterThan(0, $competency->timemodified); $this->assertEquals($this->catcreator->id, $competency->usermodified); $this->assertEquals('shortname1', $competency->shortname); $this->assertEquals('idnumber1', $competency->idnumber); $this->assertEquals('description1', $competency->description); $this->assertEquals(FORMAT_HTML, $competency->descriptionformat); $this->assertEquals(0, $competency->parentid); $this->assertEquals($incat->id, $competency->competencyframeworkid); try { $competency = $this->create_competency(2, $insystem->id); $this->fail('User should not be able to create a competency in system context.'); } catch (\required_capability_exception $e) { // All good. } } /** * Test we cannot create a competency with nasty data. */ public function test_create_competency_with_nasty_data(): void { $this->setUser($this->creator); $framework = $this->create_competency_framework(1, true); $competency = array( 'shortname' => 'shortname', 'idnumber' => 'id;"number', 'description' => 'de<>\\..scription', 'descriptionformat' => FORMAT_HTML, 'competencyframeworkid' => $framework->id, 'sortorder' => 0 ); $this->expectException(\invalid_parameter_exception::class); $this->expectExceptionMessage('Invalid external api parameter'); $result = external::create_competency($competency); } /** * Test we can read a competency with manage permissions. */ public function test_read_competencies_with_manage_permissions(): void { $this->setUser($this->creator); $framework = $this->create_competency_framework(1, true); $competency = $this->create_competency(1, $framework->id); $id = $competency->id; $result = external::read_competency($id); $result = (object) external_api::clean_returnvalue(external::read_competency_returns(), $result); $this->assertGreaterThan(0, $result->timecreated); $this->assertGreaterThan(0, $result->timemodified); $this->assertEquals($this->creator->id, $result->usermodified); $this->assertEquals('shortname1', $result->shortname); $this->assertEquals('idnumber1', $result->idnumber); $this->assertEquals('description1', $result->description); $this->assertEquals(FORMAT_HTML, $result->descriptionformat); $this->assertEquals(0, $result->parentid); $this->assertEquals($framework->id, $result->competencyframeworkid); } /** * Test we can read a competency with manage permissions. */ public function test_read_competencies_with_manage_permissions_in_category(): void { $this->setUser($this->creator); $sysframework = $this->create_competency_framework(1, true); $insystem = $this->create_competency(1, $sysframework->id); $catframework = $this->create_competency_framework(2, false); $incat = $this->create_competency(2, $catframework->id); $this->setUser($this->catcreator); $id = $incat->id; $result = external::read_competency($id); $result = (object) external_api::clean_returnvalue(external::read_competency_returns(), $result); $this->assertGreaterThan(0, $result->timecreated); $this->assertGreaterThan(0, $result->timemodified); $this->assertEquals($this->creator->id, $result->usermodified); $this->assertEquals('shortname2', $result->shortname); $this->assertEquals('idnumber2', $result->idnumber); $this->assertEquals('description2', $result->description); $this->assertEquals(FORMAT_HTML, $result->descriptionformat); $this->assertEquals(0, $result->parentid); $this->assertEquals($catframework->id, $result->competencyframeworkid); try { external::read_competency($insystem->id); $this->fail('User should not be able to read a competency in system context.'); } catch (\required_capability_exception $e) { // All good. } } /** * Test we can read a competency with read permissions. */ public function test_read_competencies_with_read_permissions(): void { $this->setUser($this->creator); $framework = $this->create_competency_framework(1, true); $competency = $this->create_competency(1, $framework->id); // Switch users to someone with less permissions. $this->setUser($this->user); $id = $competency->id; $result = external::read_competency($id); $result = (object) external_api::clean_returnvalue(external::read_competency_returns(), $result); $this->assertGreaterThan(0, $result->timecreated); $this->assertGreaterThan(0, $result->timemodified); $this->assertEquals($this->creator->id, $result->usermodified); $this->assertEquals('shortname1', $result->shortname); $this->assertEquals('idnumber1', $result->idnumber); $this->assertEquals('description1', $result->description); $this->assertEquals(FORMAT_HTML, $result->descriptionformat); $this->assertEquals(0, $result->parentid); $this->assertEquals($framework->id, $result->competencyframeworkid); } /** * Test we can read a competency with read permissions. */ public function test_read_competencies_with_read_permissions_in_category(): void { $this->setUser($this->creator); $sysframework = $this->create_competency_framework(1, true); $insystem = $this->create_competency(1, $sysframework->id); $catframework = $this->create_competency_framework(2, false); $incat = $this->create_competency(2, $catframework->id); // Switch users to someone with less permissions. $this->setUser($this->catuser); $id = $incat->id; $result = external::read_competency($id); $result = (object) external_api::clean_returnvalue(external::read_competency_returns(), $result); $this->assertGreaterThan(0, $result->timecreated); $this->assertGreaterThan(0, $result->timemodified); $this->assertEquals($this->creator->id, $result->usermodified); $this->assertEquals('shortname2', $result->shortname); $this->assertEquals('idnumber2', $result->idnumber); $this->assertEquals('description2', $result->description); $this->assertEquals(FORMAT_HTML, $result->descriptionformat); $this->assertEquals(0, $result->parentid); $this->assertEquals($catframework->id, $result->competencyframeworkid); try { external::read_competency($insystem->id); $this->fail('User should not be able to read a competency in system context.'); } catch (\required_capability_exception $e) { // All good. } } /** * Test we can delete a competency with manage permissions. */ public function test_delete_competency_with_manage_permissions(): void { $this->setUser($this->creator); $framework = $this->create_competency_framework(1, true); $result = $this->create_competency(1, $framework->id); $id = $result->id; $result = external::delete_competency($id); $result = external_api::clean_returnvalue(external::delete_competency_returns(), $result); $this->assertTrue($result); } /** * Test we can delete a competency with manage permissions. */ public function test_delete_competency_with_manage_permissions_in_category(): void { $this->setUser($this->creator); $sysframework = $this->create_competency_framework(1, true); $insystem = $this->create_competency(1, $sysframework->id); $catframework = $this->create_competency_framework(2, false); $incat = $this->create_competency(2, $catframework->id); $this->setUser($this->catcreator); $id = $incat->id; $result = external::delete_competency($id); $result = external_api::clean_returnvalue(external::delete_competency_returns(), $result); $this->assertTrue($result); try { $result = external::delete_competency($insystem->id); $this->fail('User should not be able to delete a competency in system context.'); } catch (\required_capability_exception $e) { // All good. } } /** * Test we can delete a competency with read permissions. */ public function test_delete_competency_with_read_permissions(): void { $this->setUser($this->creator); $framework = $this->create_competency_framework(1, true); $result = $this->create_competency(1, $framework->id); $id = $result->id; // Switch users to someone with less permissions. $this->setUser($this->user); $this->expectException(\required_capability_exception::class); $result = external::delete_competency($id); } /** * Test we can update a competency with manage permissions. */ public function test_update_competency_with_manage_permissions(): void { $this->setUser($this->creator); $framework = $this->create_competency_framework(1, true); $result = $this->create_competency(1, $framework->id); $result = $this->update_competency($result->id, 2); $this->assertTrue($result); } /** * Test we can update a competency with manage permissions. */ public function test_update_competency_with_manage_permissions_in_category(): void { $this->setUser($this->creator); $sysframework = $this->create_competency_framework(1, true); $insystem = $this->create_competency(1, $sysframework->id); $catframework = $this->create_competency_framework(2, false); $incat = $this->create_competency(2, $catframework->id); $this->setUser($this->catcreator); $result = $this->update_competency($incat->id, 2); $this->assertTrue($result); try { $result = $this->update_competency($insystem->id, 3); $this->fail('User should not be able to update a competency in system context.'); } catch (\required_capability_exception $e) { // All good. } } /** * Test we can update a competency with read permissions. */ public function test_update_competency_with_read_permissions(): void { $this->setUser($this->creator); $framework = $this->create_competency_framework(1, true); $result = $this->create_competency(1, $framework->id); $this->setUser($this->user); $this->expectException(\required_capability_exception::class); $result = $this->update_competency($result->id, 2); } /** * Test count competencies with filters. */ public function test_count_competencies_with_filters(): void { $this->setUser($this->creator); $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); $f1 = $lpg->create_framework(); $f2 = $lpg->create_framework(); $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get('id'))); $c2 = $lpg->create_competency(array('competencyframeworkid' => $f1->get('id'), 'shortname' => 'A')); $c3 = $lpg->create_competency(array('competencyframeworkid' => $f1->get('id'))); $c4 = $lpg->create_competency(array('competencyframeworkid' => $f2->get('id'))); $c5 = $lpg->create_competency(array('competencyframeworkid' => $f2->get('id'))); $result = external::count_competencies(array(array('column' => 'competencyframeworkid', 'value' => $f2->get('id')))); $result = external_api::clean_returnvalue(external::count_competencies_returns(), $result); $this->assertEquals(2, $result); $result = external::count_competencies(array(array('column' => 'competencyframeworkid', 'value' => $f1->get('id')))); $result = external_api::clean_returnvalue(external::count_competencies_returns(), $result); $this->assertEquals(3, $result); $result = external::count_competencies(array(array('column' => 'shortname', 'value' => 'A'))); $result = external_api::clean_returnvalue(external::count_competencies_returns(), $result); $this->assertEquals(1, $result); } /** * Test we can list and count competencies with manage permissions. */ public function test_list_and_count_competencies_with_manage_permissions(): void { $this->setUser($this->creator); $framework = $this->create_competency_framework(1, true); $result = $this->create_competency(1, $framework->id); $result = $this->create_competency(2, $framework->id); $result = $this->create_competency(3, $framework->id); $result = external::count_competencies(array()); $result = external_api::clean_returnvalue(external::count_competencies_returns(), $result); $this->assertEquals($result, 3); array('id' => $result = external::list_competencies(array(), 'shortname', 'ASC', 0, 10, \context_system::instance()->id)); $result = external_api::clean_returnvalue(external::list_competencies_returns(), $result); $this->assertEquals(count($result), 3); $result = (object) $result[0]; $this->assertGreaterThan(0, $result->timecreated); $this->assertGreaterThan(0, $result->timemodified); $this->assertEquals($this->creator->id, $result->usermodified); $this->assertEquals('shortname1', $result->shortname); $this->assertEquals('idnumber1', $result->idnumber); $this->assertEquals('description1', $result->description); } /** * Test we can list and count competencies with read permissions. */ public function test_list_and_count_competencies_with_read_permissions(): void { $this->setUser($this->creator); $framework = $this->create_competency_framework(1, true); $result = $this->create_competency(1, $framework->id); $result = $this->create_competency(2, $framework->id); $result = $this->create_competency(3, $framework->id); $this->setUser($this->user); $result = external::count_competencies(array()); $result = external_api::clean_returnvalue(external::count_competencies_returns(), $result); $this->assertEquals($result, 3); array('id' => $result = external::list_competencies(array(), 'shortname', 'ASC', 0, 10, \context_system::instance()->id)); $result = external_api::clean_returnvalue(external::list_competencies_returns(), $result); $this->assertEquals(count($result), 3); $result = (object) $result[0]; $this->assertGreaterThan(0, $result->timecreated); $this->assertGreaterThan(0, $result->timemodified); $this->assertEquals($this->creator->id, $result->usermodified); $this->assertEquals('shortname1', $result->shortname); $this->assertEquals('idnumber1', $result->idnumber); $this->assertEquals('description1', $result->description); } /** * Test we can search for competencies. */ public function test_search_competencies_with_read_permissions(): void { $this->setUser($this->creator); $framework = $this->create_competency_framework(1, true); $result = $this->create_competency(1, $framework->id); $result = $this->create_competency(2, $framework->id); $result = $this->create_competency(3, $framework->id); $this->setUser($this->user); $result = external::search_competencies('short', $framework->id); $result = external_api::clean_returnvalue(external::search_competencies_returns(), $result); $this->assertEquals(count($result), 3); $result = (object) $result[0]; $this->assertGreaterThan(0, $result->timecreated); $this->assertGreaterThan(0, $result->timemodified); $this->assertEquals($this->creator->id, $result->usermodified); $this->assertEquals('shortname1', $result->shortname); $this->assertEquals('idnumber1', $result->idnumber); $this->assertEquals('description1', $result->description); } /** * Test plans creation and updates. */ public function test_create_and_update_plans(): void { $syscontext = \context_system::instance(); $this->setUser($this->creator); $plan0 = $this->create_plan(1, $this->creator->id, 0, plan::STATUS_ACTIVE, 0); $this->setUser($this->user); try { $plan1 = $this->create_plan(2, $this->user->id, 0, plan::STATUS_DRAFT, 0); $this->fail('Exception expected due to not permissions to create draft plans'); } catch (\moodle_exception $e) { $this->assertEquals('nopermissions', $e->errorcode); } assign_capability('moodle/competency:planmanageowndraft', CAP_ALLOW, $this->userrole, $syscontext->id); accesslib_clear_all_caches_for_unit_testing(); $this->setUser($this->user); $plan2 = $this->create_plan(3, $this->user->id, 0, plan::STATUS_DRAFT, 0); // Basic update on the plan. $this->assertNotEquals('Updated plan 2 name', $plan2->name); $plan2 = external::update_plan(['id' => $plan2->id, 'name' => 'Updated plan 2 name']); $this->assertEquals('Updated plan 2 name', $plan2->name); try { $plan3 = $this->create_plan(4, $this->user->id, 0, plan::STATUS_ACTIVE, 0); $this->fail('Exception expected due to not permissions to create active plans'); } catch (\moodle_exception $e) { $this->assertEquals('nopermissions', $e->errorcode); } try { $plan3 = $this->update_plan($plan2->id, 4, $this->user->id, 0, plan::STATUS_COMPLETE, 0); $this->fail('We cannot complete a plan using api::update_plan().'); } catch (\coding_exception $e) { $this->assertTrue(true); } assign_capability('moodle/competency:planmanageown', CAP_ALLOW, $this->userrole, $syscontext->id); accesslib_clear_all_caches_for_unit_testing(); $plan3 = $this->create_plan(4, $this->user->id, 0, plan::STATUS_ACTIVE, 0); try { $plan4 = $this->create_plan(6, $this->creator->id, 0, plan::STATUS_COMPLETE, 0); $this->fail('Plans cannot be created as complete.'); } catch (\coding_exception $e) { $this->assertMatchesRegularExpression('/A plan cannot be created as complete./', $e->getMessage()); } try { $plan0 = $this->update_plan($plan0->id, 1, $this->user->id, 0, plan::STATUS_ACTIVE, 0); } catch (\moodle_exception $e) { $this->assertEquals('nopermissions', $e->errorcode); } unassign_capability('moodle/competency:planmanageown', $this->userrole, $syscontext->id); unassign_capability('moodle/competency:planmanageowndraft', $this->userrole, $syscontext->id); accesslib_clear_all_caches_for_unit_testing(); try { // Cannot be updated even if they created it. $this->update_plan($plan2->id, 1, $this->user->id, 0, plan::STATUS_ACTIVE, 0); $this->fail('The user can not update their own plan without permissions.'); } catch (\required_capability_exception $e) { $this->assertMatchesRegularExpression('/Manage learning plans./', $e->getMessage()); } } /** * Test complete plan. */ public function test_complete_plan(): void { $syscontext = \context_system::instance(); $this->setUser($this->creator); $this->setUser($this->user); assign_capability('moodle/competency:planmanageowndraft', CAP_ALLOW, $this->userrole, $syscontext->id); assign_capability('moodle/competency:planmanageown', CAP_ALLOW, $this->userrole, $syscontext->id); accesslib_clear_all_caches_for_unit_testing(); $this->setUser($this->user); $plan = $this->create_plan(1, $this->user->id, 0, plan::STATUS_ACTIVE, 0); $result = external::complete_plan($plan->id); $this->assertTrue($result); } /** * Test reopen plan. */ public function test_reopen_plan(): void { $syscontext = \context_system::instance(); $this->setUser($this->creator); $this->setUser($this->user); assign_capability('moodle/competency:planmanageowndraft', CAP_ALLOW, $this->userrole, $syscontext->id); assign_capability('moodle/competency:planmanageown', CAP_ALLOW, $this->userrole, $syscontext->id); accesslib_clear_all_caches_for_unit_testing(); $this->setUser($this->user); $plan = $this->create_plan(1, $this->user->id, 0, plan::STATUS_ACTIVE, 0); external::complete_plan($plan->id); $result = external::reopen_plan($plan->id); $this->assertTrue($result); } /** * Test that we can read plans. */ public function test_read_plans(): void { global $OUTPUT; $this->setUser($this->creator); $syscontext = \context_system::instance(); $plan1 = $this->create_plan(1, $this->user->id, 0, plan::STATUS_DRAFT, 0); $plan2 = $this->create_plan(2, $this->user->id, 0, plan::STATUS_ACTIVE, 0); $plan3 = $this->create_plan(3, $this->user->id, 0, plan::STATUS_ACTIVE, 0); external::complete_plan($plan3->id); $plan3 = (object) external::read_plan($plan3->id); $data = external::read_plan($plan1->id); $this->assertEquals((array)$plan1, external::read_plan($plan1->id)); $data = external::read_plan($plan2->id); $this->assertEquals((array)$plan2, external::read_plan($plan2->id)); $data = external::read_plan($plan3->id); $this->assertEquals((array)$plan3, external::read_plan($plan3->id)); $this->setUser($this->user); // The normal user can not edit these plans. $plan1->canmanage = false; $plan2->canmanage = false; $plan3->canmanage = false; $plan1->canbeedited = false; $plan2->canbeedited = false; $plan3->canbeedited = false; $plan1->canrequestreview = true; $plan2->canrequestreview = true; $plan3->canrequestreview = true; $plan1->canreview = false; $plan2->canreview = false; $plan3->canreview = false; $plan1->iscompleteallowed = false; $plan2->iscompleteallowed = false; $plan3->iscompleteallowed = false; $plan1->isrequestreviewallowed = true; $plan2->isrequestreviewallowed = true; $plan3->isrequestreviewallowed = true; $plan1->isapproveallowed = false; $plan2->isapproveallowed = false; $plan3->isapproveallowed = false; $plan1->isunapproveallowed = false; $plan2->isunapproveallowed = false; $plan3->isunapproveallowed = false; $plan3->isreopenallowed = false; $plan1->commentarea['canpost'] = false; $plan1->commentarea['canview'] = true; // Prevent the user from seeing their own non-draft plans. assign_capability('moodle/competency:plancommentown', CAP_PROHIBIT, $this->userrole, $syscontext->id, true); assign_capability('moodle/competency:planviewown', CAP_PROHIBIT, $this->userrole, $syscontext->id, true); assign_capability('moodle/competency:planviewowndraft', CAP_ALLOW, $this->userrole, $syscontext->id, true); accesslib_clear_all_caches_for_unit_testing(); $this->assertEquals((array)$plan1, external::read_plan($plan1->id)); try { external::read_plan($plan2->id); $this->fail('Exception expected due to not permissions to read plan'); } catch (\moodle_exception $e) { $this->assertEquals('nopermissions', $e->errorcode); } try { external::read_plan($plan3->id); $this->fail('Exception expected due to not permissions to read plan'); } catch (\moodle_exception $e) { $this->assertEquals('nopermissions', $e->errorcode); } // Allow user to see their plan. assign_capability('moodle/competency:plancommentown', CAP_ALLOW, $this->userrole, $syscontext->id, true); assign_capability('moodle/competency:planviewown', CAP_ALLOW, $this->userrole, $syscontext->id, true); assign_capability('moodle/competency:planmanageowndraft', CAP_PROHIBIT, $this->userrole, $syscontext->id, true); accesslib_clear_all_caches_for_unit_testing(); $plan1->commentarea['canpost'] = true; $plan1->commentarea['canview'] = true; $plan2->commentarea['canpost'] = true; $plan2->isrequestreviewallowed = false; $plan3->commentarea['canpost'] = true; $plan3->isrequestreviewallowed = false; $plan1->commentarea['canpostorhascomments'] = true; $plan2->commentarea['canpostorhascomments'] = true; $plan3->commentarea['canpostorhascomments'] = true; $this->assertEquals((array)$plan1, external::read_plan($plan1->id)); $this->assertEquals((array)$plan2, external::read_plan($plan2->id)); $this->assertEquals((array)$plan3, external::read_plan($plan3->id)); // Allow use to manage their own draft plan. assign_capability('moodle/competency:planviewown', CAP_PROHIBIT, $this->userrole, $syscontext->id, true); assign_capability('moodle/competency:planmanageown', CAP_PROHIBIT, $this->userrole, $syscontext->id, true); assign_capability('moodle/competency:planmanageowndraft', CAP_ALLOW, $this->userrole, $syscontext->id, true); accesslib_clear_all_caches_for_unit_testing(); $plan1->canmanage = true; $plan1->canbeedited = true; $plan1->canrequestreview = true; $plan1->isrequestreviewallowed = true; $this->assertEquals((array)$plan1, external::read_plan($plan1->id)); try { external::read_plan($plan2->id); $this->fail('Exception expected due to not permissions to read plan'); } catch (\moodle_exception $e) { $this->assertEquals('nopermissions', $e->errorcode); } try { external::read_plan($plan3->id); $this->fail('Exception expected due to not permissions to read plan'); } catch (\moodle_exception $e) { $this->assertEquals('nopermissions', $e->errorcode); } // Allow use to manage their plan. assign_capability('moodle/competency:planviewown', CAP_PROHIBIT, $this->userrole, $syscontext->id, true); assign_capability('moodle/competency:planmanageowndraft', CAP_PROHIBIT, $this->userrole, $syscontext->id, true); assign_capability('moodle/competency:planmanageown', CAP_ALLOW, $this->userrole, $syscontext->id, true); accesslib_clear_all_caches_for_unit_testing(); $plan1->canmanage = false; $plan1->canbeedited = false; $plan1->canrequestreview = true; $plan1->canreview = true; $plan1->isrequestreviewallowed = true; $plan1->isapproveallowed = true; $plan1->iscompleteallowed = false; $plan2->canmanage = true; $plan2->canbeedited = true; $plan2->canreview = true; $plan2->iscompleteallowed = true; $plan2->isunapproveallowed = true; $plan3->canmanage = true; $plan3->canreview = true; $plan3->isreopenallowed = true; $this->assertEquals((array)$plan1, external::read_plan($plan1->id)); $this->assertEquals((array)$plan2, external::read_plan($plan2->id)); $this->assertEquals((array)$plan3, external::read_plan($plan3->id)); } public function test_delete_plans(): void { $this->setUser($this->creator); $syscontext = \context_system::instance(); $plan1 = $this->create_plan(1, $this->user->id, 0, plan::STATUS_ACTIVE, 0); $plan2 = $this->create_plan(2, $this->user->id, 0, plan::STATUS_ACTIVE, 0); $plan3 = $this->create_plan(3, $this->creator->id, 0, plan::STATUS_ACTIVE, 0); $this->assertTrue(external::delete_plan($plan1->id)); unassign_capability('moodle/competency:planmanage', $this->creatorrole, $syscontext->id); accesslib_clear_all_caches_for_unit_testing(); try { external::delete_plan($plan2->id); $this->fail('Exception expected due to not permissions to manage plans'); } catch (\moodle_exception $e) { $this->assertEquals('nopermissions', $e->errorcode); } $this->setUser($this->user); // Can not delete plans created by other users. try { external::delete_plan($plan2->id); $this->fail('Exception expected due to not permissions to manage plans'); } catch (\moodle_exception $e) { $this->assertEquals('nopermissions', $e->errorcode); } assign_capability('moodle/competency:planmanageown', CAP_ALLOW, $this->userrole, $syscontext->id); accesslib_clear_all_caches_for_unit_testing(); $this->assertTrue(external::delete_plan($plan2->id)); // Can not delete plans created for other users. try { external::delete_plan($plan3->id); $this->fail('Exception expected due to not permissions to manage plans'); } catch (\moodle_exception $e) { $this->assertEquals('nopermissions', $e->errorcode); } $plan4 = $this->create_plan(4, $this->user->id, 0, plan::STATUS_ACTIVE, 0); $this->assertTrue(external::delete_plan($plan4->id)); } public function test_delete_plan_removes_relations(): void { $this->setAdminUser(); $dg = $this->getDataGenerator(); $lpg = $dg->get_plugin_generator('core_competency'); $user = $dg->create_user(); $plan = $lpg->create_plan(array('userid' => $user->id)); $framework = $lpg->create_framework(); $comp1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $comp2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $comp3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $pc1 = $lpg->create_plan_competency(array('planid' => $plan->get('id'), 'competencyid' => $comp1->get('id'))); $pc2 = $lpg->create_plan_competency(array('planid' => $plan->get('id'), 'competencyid' => $comp2->get('id'))); $pc3 = $lpg->create_plan_competency(array('planid' => $plan->get('id'), 'competencyid' => $comp3->get('id'))); // Complete the plan to generate user_competency_plan entries. api::complete_plan($plan); // Confirm the data we have. $this->assertEquals(3, plan_competency::count_records(array('planid' => $plan->get('id')))); $this->assertEquals(3, user_competency_plan::count_records(array('planid' => $plan->get('id'), 'userid' => $user->id))); // Delete the plan now. api::delete_plan($plan->get('id')); $this->assertEquals(0, plan_competency::count_records(array('planid' => $plan->get('id')))); $this->assertEquals(0, user_competency_plan::count_records(array('planid' => $plan->get('id'), 'userid' => $user->id))); } public function test_list_plan_competencies(): void { $this->setUser($this->creator); $dg = $this->getDataGenerator(); $lpg = $dg->get_plugin_generator('core_competency'); $f1 = $lpg->create_framework(); $f2 = $lpg->create_framework(); $c1a = $lpg->create_competency(array('competencyframeworkid' => $f1->get('id'))); $c1b = $lpg->create_competency(array('competencyframeworkid' => $f1->get('id'))); $c1c = $lpg->create_competency(array('competencyframeworkid' => $f1->get('id'))); $c2a = $lpg->create_competency(array('competencyframeworkid' => $f2->get('id'))); $c2b = $lpg->create_competency(array('competencyframeworkid' => $f2->get('id'))); $tpl = $lpg->create_template(); $lpg->create_template_competency(array('templateid' => $tpl->get('id'), 'competencyid' => $c1a->get('id'))); $lpg->create_template_competency(array('templateid' => $tpl->get('id'), 'competencyid' => $c1c->get('id'))); $lpg->create_template_competency(array('templateid' => $tpl->get('id'), 'competencyid' => $c2b->get('id'))); $plan = $lpg->create_plan(array('userid' => $this->user->id, 'templateid' => $tpl->get('id'))); $uc1a = $lpg->create_user_competency(array('userid' => $this->user->id, 'competencyid' => $c1a->get('id'), 'status' => user_competency::STATUS_IN_REVIEW, 'reviewerid' => $this->creator->id)); $uc1b = $lpg->create_user_competency(array('userid' => $this->user->id, 'competencyid' => $c1b->get('id'))); $uc2b = $lpg->create_user_competency(array('userid' => $this->user->id, 'competencyid' => $c2b->get('id'), 'grade' => 2, 'proficiency' => 1)); $ux1a = $lpg->create_user_competency(array('userid' => $this->creator->id, 'competencyid' => $c1a->get('id'))); $result = external::list_plan_competencies($plan->get('id')); $result = external::clean_returnvalue(external::list_plan_competencies_returns(), $result); $this->assertCount(3, $result); $this->assertEquals($c1a->get('id'), $result[0]['competency']['id']); $this->assertEquals($this->user->id, $result[0]['usercompetency']['userid']); $this->assertArrayNotHasKey('usercompetencyplan', $result[0]); $this->assertEquals($c1c->get('id'), $result[1]['competency']['id']); $this->assertEquals($this->user->id, $result[1]['usercompetency']['userid']); $this->assertArrayNotHasKey('usercompetencyplan', $result[1]); $this->assertEquals($c2b->get('id'), $result[2]['competency']['id']); $this->assertEquals($this->user->id, $result[2]['usercompetency']['userid']); $this->assertArrayNotHasKey('usercompetencyplan', $result[2]); $this->assertEquals(user_competency::STATUS_IN_REVIEW, $result[0]['usercompetency']['status']); $this->assertEquals(null, $result[1]['usercompetency']['grade']); $this->assertEquals(2, $result[2]['usercompetency']['grade']); $this->assertEquals(1, $result[2]['usercompetency']['proficiency']); // Check the return values when the plan status is complete. $completedplan = $lpg->create_plan(array('userid' => $this->user->id, 'templateid' => $tpl->get('id'), 'status' => plan::STATUS_COMPLETE)); $uc1a = $lpg->create_user_competency_plan(array('userid' => $this->user->id, 'competencyid' => $c1a->get('id'), 'planid' => $completedplan->get('id'))); $uc1b = $lpg->create_user_competency_plan(array('userid' => $this->user->id, 'competencyid' => $c1c->get('id'), 'planid' => $completedplan->get('id'))); $uc2b = $lpg->create_user_competency_plan(array('userid' => $this->user->id, 'competencyid' => $c2b->get('id'), 'planid' => $completedplan->get('id'), 'grade' => 2, 'proficiency' => 1)); $ux1a = $lpg->create_user_competency_plan(array('userid' => $this->creator->id, 'competencyid' => $c1a->get('id'), 'planid' => $completedplan->get('id'))); $result = external::list_plan_competencies($completedplan->get('id')); $result = external::clean_returnvalue(external::list_plan_competencies_returns(), $result); $this->assertCount(3, $result); $this->assertEquals($c1a->get('id'), $result[0]['competency']['id']); $this->assertEquals($this->user->id, $result[0]['usercompetencyplan']['userid']); $this->assertArrayNotHasKey('usercompetency', $result[0]); $this->assertEquals($c1c->get('id'), $result[1]['competency']['id']); $this->assertEquals($this->user->id, $result[1]['usercompetencyplan']['userid']); $this->assertArrayNotHasKey('usercompetency', $result[1]); $this->assertEquals($c2b->get('id'), $result[2]['competency']['id']); $this->assertEquals($this->user->id, $result[2]['usercompetencyplan']['userid']); $this->assertArrayNotHasKey('usercompetency', $result[2]); $this->assertEquals(null, $result[1]['usercompetencyplan']['grade']); $this->assertEquals(2, $result[2]['usercompetencyplan']['grade']); $this->assertEquals(1, $result[2]['usercompetencyplan']['proficiency']); } public function test_add_competency_to_template(): void { $this->setUser($this->creator); $syscontext = \context_system::instance(); // Create a template. $template = $this->create_template(1, true); // Create a competency. $framework = $this->create_competency_framework(1, true); $competency = $this->create_competency(1, $framework->id); // Add the competency. external::add_competency_to_template($template->id, $competency->id); // Check that it was added. $this->assertEquals(1, external::count_competencies_in_template($template->id)); // Unassign capability. unassign_capability('moodle/competency:templatemanage', $this->creatorrole, $syscontext->id); accesslib_clear_all_caches_for_unit_testing(); // Check we can not add the competency now. try { external::add_competency_to_template($template->id, $competency->id); $this->fail('Exception expected due to not permissions to manage template competencies'); } catch (\moodle_exception $e) { $this->assertEquals('nopermissions', $e->errorcode); } } public function test_remove_competency_from_template(): void { $syscontext = \context_system::instance(); $this->setUser($this->creator); $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); // Create a template. $template = $this->create_template(1, true); // Create a competency. $framework = $lpg->create_framework(); $competency = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); // Add the competency. external::add_competency_to_template($template->id, $competency->get('id')); // Check that it was added. $this->assertEquals(1, external::count_competencies_in_template($template->id)); // Check that we can remove the competency. external::remove_competency_from_template($template->id, $competency->get('id')); // Check that it was removed. $this->assertEquals(0, external::count_competencies_in_template($template->id)); // Unassign capability. unassign_capability('moodle/competency:templatemanage', $this->creatorrole, $syscontext->id); accesslib_clear_all_caches_for_unit_testing(); // Check we can not remove the competency now. try { external::add_competency_to_template($template->id, $competency->get('id')); $this->fail('Exception expected due to not permissions to manage template competencies'); } catch (\moodle_exception $e) { $this->assertEquals('nopermissions', $e->errorcode); } } /** * Test we can re-order competency frameworks. */ public function test_reorder_template_competencies(): void { $this->setUser($this->creator); $syscontext = \context_system::instance(); $onehour = time() + 60 * 60; // Create a template. $template = $this->create_template(1, true); // Create a competency framework. $framework = $this->create_competency_framework(1, true); // Create multiple competencies. $competency1 = $this->create_competency(1, $framework->id); $competency2 = $this->create_competency(2, $framework->id); $competency3 = $this->create_competency(3, $framework->id); $competency4 = $this->create_competency(4, $framework->id); // Add the competencies. external::add_competency_to_template($template->id, $competency1->id); external::add_competency_to_template($template->id, $competency2->id); external::add_competency_to_template($template->id, $competency3->id); external::add_competency_to_template($template->id, $competency4->id); // Test if removing competency from template don't create sortorder holes. external::remove_competency_from_template($template->id, $competency3->id); $templcomp4 = template_competency::get_record(array( 'templateid' => $template->id, 'competencyid' => $competency4->id )); $this->assertEquals(2, $templcomp4->get('sortorder')); // This is a move up. external::reorder_template_competency($template->id, $competency4->id, $competency2->id); $result = external::list_competencies_in_template($template->id); $result = external_api::clean_returnvalue(external::list_competencies_in_template_returns(), $result); $r1 = (object) $result[0]; $r2 = (object) $result[1]; $r3 = (object) $result[2]; $this->assertEquals($competency1->id, $r1->id); $this->assertEquals($competency4->id, $r2->id); $this->assertEquals($competency2->id, $r3->id); // This is a move down. external::reorder_template_competency($template->id, $competency1->id, $competency4->id); $result = external::list_competencies_in_template($template->id); $result = external_api::clean_returnvalue(external::list_competencies_in_template_returns(), $result); $r1 = (object) $result[0]; $r2 = (object) $result[1]; $r3 = (object) $result[2]; $this->assertEquals($competency4->id, $r1->id); $this->assertEquals($competency1->id, $r2->id); $this->assertEquals($competency2->id, $r3->id); $this->expectException('\required_capability_exception'); $this->setUser($this->user); external::reorder_template_competency($template->id, $competency1->id, $competency2->id); } /** * Test we can duplicate learning plan template. */ public function test_duplicate_learning_plan_template(): void { $this->setUser($this->creator); $syscontext = \context_system::instance(); $onehour = time() + 60 * 60; // Create a template. $template = $this->create_template(1, true); // Create a competency framework. $framework = $this->create_competency_framework(1, true); // Create multiple competencies. $competency1 = $this->create_competency(1, $framework->id); $competency2 = $this->create_competency(2, $framework->id); $competency3 = $this->create_competency(3, $framework->id); // Add the competencies. external::add_competency_to_template($template->id, $competency1->id); external::add_competency_to_template($template->id, $competency2->id); external::add_competency_to_template($template->id, $competency3->id); // Duplicate the learning plan template. $duplicatedtemplate = external::duplicate_template($template->id); $result = external::list_competencies_in_template($template->id); $resultduplicated = external::list_competencies_in_template($duplicatedtemplate->id); $this->assertEquals(count($result), count($resultduplicated)); $this->assertStringContainsString($template->shortname, $duplicatedtemplate->shortname); $this->assertEquals($duplicatedtemplate->description, $template->description); $this->assertEquals($duplicatedtemplate->descriptionformat, $template->descriptionformat); $this->assertEquals($duplicatedtemplate->visible, $template->visible); } /** * Test that we can return scale values for a scale with the scale ID. */ public function test_get_scale_values(): void { global $DB; $this->setUser($this->creator); // Create a scale. $record = new \stdClass(); $record->courseid = 0; $record->userid = $this->creator->id; $record->name = 'Test scale'; $record->scale = 'Poor, Not good, Okay, Fine, Excellent'; $record->description = '

Test scale description.

'; $record->descriptionformat = 1; $record->timemodified = time(); $scaleid = $DB->insert_record('scale', $record); // Expected return value. $expected = array(array( 'id' => 1, 'name' => 'Poor' ), array( 'id' => 2, 'name' => 'Not good' ), array( 'id' => 3, 'name' => 'Okay' ), array( 'id' => 4, 'name' => 'Fine' ), array( 'id' => 5, 'name' => 'Excellent' ) ); // Call the webservice. $result = external::get_scale_values($scaleid); $this->assertEquals($expected, $result); } /** * Create a template. */ public function test_create_template(): void { $syscontextid = \context_system::instance()->id; $catcontextid = \context_coursecat::instance($this->category->id)->id; // A user without permission. $this->setUser($this->user); try { $result = $this->create_template(1, true); $this->fail('Invalid permissions'); } catch (\required_capability_exception $e) { // All good. } // A user without permission in a category. $this->setUser($this->catuser); try { $result = $this->create_template(1, false); $this->fail('Invalid permissions'); } catch (\required_capability_exception $e) { // All good. } // A user with permissions in the system. $this->setUser($this->creator); $result = $this->create_template(1, true); $this->assertEquals('shortname1', $result->shortname); $this->assertEquals($syscontextid, $result->contextid); $this->assertNotEmpty($result->id); $result = $this->create_template(2, false); $this->assertEquals('shortname2', $result->shortname); $this->assertEquals($catcontextid, $result->contextid); $this->assertNotEmpty($result->id); // A user with permissions in the category. $this->setUser($this->catcreator); try { $result = $this->create_template(3, true); $this->fail('Invalid permissions'); } catch (\required_capability_exception $e) { // All good. } $result = $this->create_template(3, false); $this->assertEquals('shortname3', $result->shortname); $this->assertEquals($catcontextid, $result->contextid); $this->assertNotEmpty($result->id); } /** * Read a template. */ public function test_read_template(): void { $syscontextid = \context_system::instance()->id; $catcontextid = \context_coursecat::instance($this->category->id)->id; // Set a due date for the next year. $date = new \DateTime('now'); $date->modify('+1 year'); $duedate = $date->getTimestamp(); // Creating two templates. $this->setUser($this->creator); $systemplate = $this->create_template(1, true); $cattemplate = $this->create_template(2, false); // User without permissions to read in system. assign_capability('moodle/competency:templateview', CAP_PROHIBIT, $this->userrole, $syscontextid, true); accesslib_clear_all_caches_for_unit_testing(); $this->setUser($this->user); $this->assertFalse(has_capability('moodle/competency:templateview', \context_system::instance())); try { external::read_template($systemplate->id); $this->fail('Invalid permissions'); } catch (\required_capability_exception $e) { // All good. } try { external::read_template($cattemplate->id); $this->fail('Invalid permissions'); } catch (\required_capability_exception $e) { // All good. } // User with permissions to read in a category. assign_capability('moodle/competency:templateview', CAP_PREVENT, $this->userrole, $syscontextid, true); assign_capability('moodle/competency:templateview', CAP_ALLOW, $this->userrole, $catcontextid, true); accesslib_clear_all_caches_for_unit_testing(); $this->assertFalse(has_capability('moodle/competency:templateview', \context_system::instance())); $this->assertTrue(has_capability('moodle/competency:templateview', \context_coursecat::instance($this->category->id))); try { external::read_template($systemplate->id); $this->fail('Invalid permissions'); } catch (\required_capability_exception $e) { // All good. } $result = external::read_template($cattemplate->id); $result = external_api::clean_returnvalue(external::read_template_returns(), $result); $this->assertEquals($cattemplate->id, $result['id']); $this->assertEquals('shortname2', $result['shortname']); $this->assertEquals('description2', $result['description']); $this->assertEquals(FORMAT_HTML, $result['descriptionformat']); $this->assertEquals(1, $result['visible']); $this->assertEquals(0, $result['duedate']); $this->assertEquals(userdate(0), $result['duedateformatted']); // User with permissions to read in the system. assign_capability('moodle/competency:templateview', CAP_ALLOW, $this->userrole, $syscontextid, true); accesslib_clear_all_caches_for_unit_testing(); $this->assertTrue(has_capability('moodle/competency:templateview', \context_system::instance())); $result = external::read_template($systemplate->id); $result = external_api::clean_returnvalue(external::read_template_returns(), $result); $this->assertEquals($systemplate->id, $result['id']); $this->assertEquals('shortname1', $result['shortname']); $this->assertEquals('description1', $result['description']); $this->assertEquals(FORMAT_HTML, $result['descriptionformat']); $this->assertEquals(true, $result['visible']); $this->assertEquals(0, $result['duedate']); $this->assertEquals(userdate(0), $result['duedateformatted']); $result = external::read_template($cattemplate->id); $result = external_api::clean_returnvalue(external::read_template_returns(), $result); $this->assertEquals($cattemplate->id, $result['id']); $this->assertEquals('shortname2', $result['shortname']); $this->assertEquals('description2', $result['description']); $this->assertEquals(FORMAT_HTML, $result['descriptionformat']); $this->assertEquals(true, $result['visible']); $this->assertEquals(0, $result['duedate']); $this->assertEquals(userdate(0), $result['duedateformatted']); } /** * Update a template. */ public function test_update_template(): void { $syscontextid = \context_system::instance()->id; $catcontextid = \context_coursecat::instance($this->category->id)->id; // Set a due date for the next year. $date = new \DateTime('now'); $date->modify('+1 year'); $duedate = $date->getTimestamp(); // Creating two templates. $this->setUser($this->creator); $systemplate = $this->create_template(1, true); $cattemplate = $this->create_template(2, false); // Trying to update in a without permissions. $this->setUser($this->user); try { $this->update_template($systemplate->id, 3); $this->fail('Invalid permissions'); } catch (\required_capability_exception $e) { // All good. } try { $this->update_template($cattemplate->id, 3); $this->fail('Invalid permissions'); } catch (\required_capability_exception $e) { // All good. } // User with permissions to update in category. $this->setUser($this->catcreator); try { $this->update_template($systemplate->id, 3); $this->fail('Invalid permissions'); } catch (\required_capability_exception $e) { // All good. } $result = $this->update_template($cattemplate->id, 3); $this->assertTrue($result); $result = external::read_template($cattemplate->id); $result = external_api::clean_returnvalue(external::read_template_returns(), $result); $this->assertEquals($cattemplate->id, $result['id']); $this->assertEquals('shortname3', $result['shortname']); $this->assertEquals("description3", $result['description']); $this->assertEquals(FORMAT_HTML, $result['descriptionformat']); $this->assertEquals(true, $result['visible']); $this->assertEquals(0, $result['duedate']); $this->assertEquals(userdate(0), $result['duedateformatted']); // User with permissions to update in the system. $this->setUser($this->creator); $result = $this->update_template($systemplate->id, 4); $this->assertTrue($result); $result = external::read_template($systemplate->id); $result = external_api::clean_returnvalue(external::read_template_returns(), $result); $this->assertEquals($systemplate->id, $result['id']); $this->assertEquals('shortname4', $result['shortname']); $this->assertEquals('description4', $result['description']); $this->assertEquals(FORMAT_HTML, $result['descriptionformat']); $this->assertEquals(true, $result['visible']); $this->assertEquals(0, $result['duedate']); $this->assertEquals(userdate(0), $result['duedateformatted']); $result = $this->update_template($cattemplate->id, 5); $this->assertTrue($result); $result = external::read_template($cattemplate->id); $result = external_api::clean_returnvalue(external::read_template_returns(), $result); $this->assertEquals($cattemplate->id, $result['id']); $this->assertEquals('shortname5', $result['shortname']); $this->assertEquals('description5', $result['description']); $this->assertEquals(FORMAT_HTML, $result['descriptionformat']); $this->assertEquals(1, $result['visible']); $this->assertEquals(0, $result['duedate']); $this->assertEquals(userdate(0), $result['duedateformatted']); } /** * Delete a template. */ public function test_delete_template(): void { global $DB; $syscontextid = \context_system::instance()->id; $catcontextid = \context_coursecat::instance($this->category->id)->id; // Creating a few templates. $this->setUser($this->creator); $sys1 = $this->create_template(1, true); $cat1 = $this->create_template(2, false); $cat2 = $this->create_template(3, false); $this->assertTrue($DB->record_exists(template::TABLE, array('id' => $sys1->id))); $this->assertTrue($DB->record_exists(template::TABLE, array('id' => $cat1->id))); $this->assertTrue($DB->record_exists(template::TABLE, array('id' => $cat2->id))); // User without permissions. $this->setUser($this->user); try { external::delete_template($sys1->id); $this->fail('Invalid permissions'); } catch (\required_capability_exception $e) { // All good. } try { external::delete_template($cat1->id); $this->fail('Invalid permissions'); } catch (\required_capability_exception $e) { // All good. } // User with category permissions. $this->setUser($this->catcreator); try { external::delete_template($sys1->id); $this->fail('Invalid permissions'); } catch (\required_capability_exception $e) { // All good. } $result = external::delete_template($cat1->id); $result = external_api::clean_returnvalue(external::delete_template_returns(), $result); $this->assertTrue($result); $this->assertFalse($DB->record_exists(template::TABLE, array('id' => $cat1->id))); // User with system permissions. $this->setUser($this->creator); $result = external::delete_template($sys1->id); $result = external_api::clean_returnvalue(external::delete_template_returns(), $result); $this->assertTrue($result); $result = external::delete_template($cat2->id); $result = external_api::clean_returnvalue(external::delete_template_returns(), $result); $this->assertTrue($result); $this->assertFalse($DB->record_exists(template::TABLE, array('id' => $sys1->id))); $this->assertFalse($DB->record_exists(template::TABLE, array('id' => $cat2->id))); } /** * List templates. */ public function test_list_templates(): void { $syscontextid = \context_system::instance()->id; $catcontextid = \context_coursecat::instance($this->category->id)->id; // Creating a few templates. $this->setUser($this->creator); $sys1 = $this->create_template(1, true); $sys2 = $this->create_template(2, true); $cat1 = $this->create_template(3, false); $cat2 = $this->create_template(4, false); // User without permission. $this->setUser($this->user); assign_capability('moodle/competency:templateview', CAP_PROHIBIT, $this->userrole, $syscontextid, true); accesslib_clear_all_caches_for_unit_testing(); try { external::list_templates('id', 'ASC', 0, 10, array('contextid' => $syscontextid), 'children', false); $this->fail('Invalid permissions'); } catch (\required_capability_exception $e) { // All good. } // User with category permissions. assign_capability('moodle/competency:templateview', CAP_PREVENT, $this->userrole, $syscontextid, true); assign_capability('moodle/competency:templateview', CAP_ALLOW, $this->userrole, $catcontextid, true); accesslib_clear_all_caches_for_unit_testing(); $result = external::list_templates('id', 'ASC', 0, 10, array('contextid' => $syscontextid), 'children', false); $result = external_api::clean_returnvalue(external::list_templates_returns(), $result); $this->assertCount(2, $result); $this->assertEquals($cat1->id, $result[0]['id']); $this->assertEquals($cat2->id, $result[1]['id']); // User with system permissions. assign_capability('moodle/competency:templateview', CAP_ALLOW, $this->userrole, $syscontextid, true); accesslib_clear_all_caches_for_unit_testing(); $result = external::list_templates('id', 'DESC', 0, 3, array('contextid' => $catcontextid), 'parents', false); $result = external_api::clean_returnvalue(external::list_templates_returns(), $result); $this->assertCount(3, $result); $this->assertEquals($cat2->id, $result[0]['id']); $this->assertEquals($cat1->id, $result[1]['id']); $this->assertEquals($sys2->id, $result[2]['id']); } /** * List templates using competency. */ public function test_list_templates_using_competency(): void { $this->setUser($this->creator); // Create a template. $template1 = $this->create_template(1, true); $template2 = $this->create_template(2, true); $template3 = $this->create_template(3, true); $template4 = $this->create_template(4, true); // Create a competency. $framework = $this->create_competency_framework(1, true); $competency1 = $this->create_competency(1, $framework->id); $competency2 = $this->create_competency(2, $framework->id); // Add the competency. external::add_competency_to_template($template1->id, $competency1->id); external::add_competency_to_template($template2->id, $competency1->id); external::add_competency_to_template($template3->id, $competency1->id); external::add_competency_to_template($template4->id, $competency2->id); $listcomp1 = external::list_templates_using_competency($competency1->id); $listcomp2 = external::list_templates_using_competency($competency2->id); // Test count_templates_using_competency. $counttempcomp1 = external::count_templates_using_competency($competency1->id); $counttempcomp2 = external::count_templates_using_competency($competency2->id); $comptemp1 = $listcomp1[0]; $comptemp2 = $listcomp1[1]; $comptemp3 = $listcomp1[2]; $comptemp4 = $listcomp2[0]; $this->assertCount(3, $listcomp1); $this->assertCount(1, $listcomp2); $this->assertEquals(3, $counttempcomp1); $this->assertEquals(1, $counttempcomp2); $this->assertEquals($template1->id, $comptemp1->id); $this->assertEquals($template2->id, $comptemp2->id); $this->assertEquals($template3->id, $comptemp3->id); $this->assertEquals($template4->id, $comptemp4->id); } public function test_count_templates(): void { $syscontextid = \context_system::instance()->id; $catcontextid = \context_coursecat::instance($this->category->id)->id; // Creating a few templates. $this->setUser($this->creator); $sys1 = $this->create_template(1, true); $sys2 = $this->create_template(2, true); $cat1 = $this->create_template(3, false); $cat2 = $this->create_template(4, false); $cat3 = $this->create_template(5, false); // User without permission. $this->setUser($this->user); assign_capability('moodle/competency:templateview', CAP_PROHIBIT, $this->userrole, $syscontextid, true); accesslib_clear_all_caches_for_unit_testing(); try { external::count_templates(array('contextid' => $syscontextid), 'children'); $this->fail('Invalid permissions'); } catch (\required_capability_exception $e) { // All good. } // User with category permissions. assign_capability('moodle/competency:templateview', CAP_PREVENT, $this->userrole, $syscontextid, true); assign_capability('moodle/competency:templateview', CAP_ALLOW, $this->userrole, $catcontextid, true); accesslib_clear_all_caches_for_unit_testing(); $result = external::count_templates(array('contextid' => $syscontextid), 'children'); $result = external_api::clean_returnvalue(external::count_templates_returns(), $result); $this->assertEquals(3, $result); // User with system permissions. assign_capability('moodle/competency:templateview', CAP_ALLOW, $this->userrole, $syscontextid, true); accesslib_clear_all_caches_for_unit_testing(); $result = external::count_templates(array('contextid' => $catcontextid), 'parents'); $result = external_api::clean_returnvalue(external::count_templates_returns(), $result); $this->assertEquals(5, $result); } /** * Test that we can add related competencies. * * @return void */ public function test_add_related_competency(): void { global $DB; $this->setUser($this->creator); $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); $framework = $lpg->create_framework(); $framework2 = $lpg->create_framework(); $competency1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $competency2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $competency3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $competency4 = $lpg->create_competency(array('competencyframeworkid' => $framework2->get('id'))); // The lower one always as competencyid. $result = external::add_related_competency($competency1->get('id'), $competency2->get('id')); $result = external_api::clean_returnvalue(external::add_related_competency_returns(), $result); $this->assertTrue($result); $this->assertTrue($DB->record_exists_select( related_competency::TABLE, 'competencyid = :cid AND relatedcompetencyid = :rid', array( 'cid' => $competency1->get('id'), 'rid' => $competency2->get('id') ) )); $this->assertFalse($DB->record_exists_select( related_competency::TABLE, 'competencyid = :cid AND relatedcompetencyid = :rid', array( 'cid' => $competency2->get('id'), 'rid' => $competency1->get('id') ) )); $result = external::add_related_competency($competency3->get('id'), $competency1->get('id')); $result = external_api::clean_returnvalue(external::add_related_competency_returns(), $result); $this->assertTrue($result); $this->assertTrue($DB->record_exists_select( related_competency::TABLE, 'competencyid = :cid AND relatedcompetencyid = :rid', array( 'cid' => $competency1->get('id'), 'rid' => $competency3->get('id') ) )); $this->assertFalse($DB->record_exists_select( related_competency::TABLE, 'competencyid = :cid AND relatedcompetencyid = :rid', array( 'cid' => $competency3->get('id'), 'rid' => $competency1->get('id') ) )); // We can not allow a duplicate relation, not even in the other direction. $this->assertEquals(1, $DB->count_records_select(related_competency::TABLE, 'competencyid = :cid AND relatedcompetencyid = :rid', array('cid' => $competency1->get('id'), 'rid' => $competency2->get('id')))); $this->assertEquals(0, $DB->count_records_select(related_competency::TABLE, 'competencyid = :cid AND relatedcompetencyid = :rid', array('rid' => $competency1->get('id'), 'cid' => $competency2->get('id')))); $result = external::add_related_competency($competency2->get('id'), $competency1->get('id')); $result = external_api::clean_returnvalue(external::add_related_competency_returns(), $result); $this->assertTrue($result); $this->assertEquals(1, $DB->count_records_select(related_competency::TABLE, 'competencyid = :cid AND relatedcompetencyid = :rid', array('cid' => $competency1->get('id'), 'rid' => $competency2->get('id')))); $this->assertEquals(0, $DB->count_records_select(related_competency::TABLE, 'competencyid = :cid AND relatedcompetencyid = :rid', array('rid' => $competency1->get('id'), 'cid' => $competency2->get('id')))); // Check that we cannot create links across frameworks. try { external::add_related_competency($competency1->get('id'), $competency4->get('id')); $this->fail('Exception expected due mis-use of shared competencies'); } catch (invalid_persistent_exception $e) { // Yay! } // User without permission. $this->setUser($this->user); // Check we can not add the related competency now. try { external::add_related_competency($competency1->get('id'), $competency3->get('id')); $this->fail('Exception expected due to not permissions to manage template competencies'); } catch (\moodle_exception $e) { $this->assertEquals('nopermissions', $e->errorcode); } } /** * Test that we can remove related competencies. * * @return void */ public function test_remove_related_competency(): void { $this->setUser($this->creator); $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); $framework = $lpg->create_framework(); $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $c3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $rc1 = $lpg->create_related_competency(array('competencyid' => $c1->get('id'), 'relatedcompetencyid' => $c2->get('id'))); $rc2 = $lpg->create_related_competency(array('competencyid' => $c2->get('id'), 'relatedcompetencyid' => $c3->get('id'))); $this->assertEquals(2, related_competency::count_records()); // Returns false when the relation does not exist. $result = external::remove_related_competency($c1->get('id'), $c3->get('id')); $result = external_api::clean_returnvalue(external::remove_related_competency_returns(), $result); $this->assertFalse($result); // Returns true on success. $result = external::remove_related_competency($c2->get('id'), $c3->get('id')); $result = external_api::clean_returnvalue(external::remove_related_competency_returns(), $result); $this->assertTrue($result); $this->assertEquals(1, related_competency::count_records()); // We don't need to specify competencyid and relatedcompetencyid in the right order. $result = external::remove_related_competency($c2->get('id'), $c1->get('id')); $result = external_api::clean_returnvalue(external::remove_related_competency_returns(), $result); $this->assertTrue($result); $this->assertEquals(0, related_competency::count_records()); } /** * Test that we can search and include related competencies. * * @return void */ public function test_search_competencies_including_related(): void { $this->setUser($this->creator); $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); $framework = $lpg->create_framework(); $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $c3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $c4 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $c5 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); // We have 1-2, 1-3, 2-4, and no relation between 2-3 nor 1-4 nor 5. $rc12 = $lpg->create_related_competency(array('competencyid' => $c1->get('id'), 'relatedcompetencyid' => $c2->get('id'))); $rc13 = $lpg->create_related_competency(array('competencyid' => $c1->get('id'), 'relatedcompetencyid' => $c3->get('id'))); $rc24 = $lpg->create_related_competency(array('competencyid' => $c2->get('id'), 'relatedcompetencyid' => $c4->get('id'))); $result = external::search_competencies('comp', $framework->get('id'), true); $result = external_api::clean_returnvalue(external::search_competencies_returns(), $result); $this->assertCount(5, $result); } /** * Test that we can add competency to plan if we have the right capability. * * @return void */ public function test_add_competency_to_plan(): void { $this->resetAfterTest(true); $dg = $this->getDataGenerator(); $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); $usermanage = $dg->create_user(); $user = $dg->create_user(); $syscontext = \context_system::instance(); // Creating specific roles. $managerole = $dg->create_role(array( 'name' => 'User manage', 'shortname' => 'manage' )); assign_capability('moodle/competency:planmanage', CAP_ALLOW, $managerole, $syscontext->id); assign_capability('moodle/competency:planview', CAP_ALLOW, $managerole, $syscontext->id); $dg->role_assign($managerole, $usermanage->id, $syscontext->id); $this->setUser($usermanage); $plan = array ( 'userid' => $usermanage->id, 'status' => \core_competency\plan::STATUS_ACTIVE ); $pl1 = $lpg->create_plan($plan); $framework = $lpg->create_framework(); $competency = $lpg->create_competency( array('competencyframeworkid' => $framework->get('id')) ); $this->assertTrue(external::add_competency_to_plan($pl1->get('id'), $competency->get('id'))); // A competency cannot be added to plan based on template. $template = $lpg->create_template(); $plan = array ( 'userid' => $usermanage->id, 'status' => \core_competency\plan::STATUS_ACTIVE, 'templateid' => $template->get('id') ); $pl2 = $lpg->create_plan($plan); try { external::add_competency_to_plan($pl2->get('id'), $competency->get('id')); $this->fail('A competency cannot be added to plan based on template'); } catch (\coding_exception $ex) { $this->assertTrue(true); } // User without capability cannot add competency to a plan. $this->setUser($user); try { external::add_competency_to_plan($pl1->get('id'), $competency->get('id')); $this->fail('User without capability cannot add competency to a plan'); } catch (\required_capability_exception $ex) { $this->assertTrue(true); } } /** * Test that we can add competency to plan if we have the right capability. * * @return void */ public function test_remove_competency_from_plan(): void { $this->resetAfterTest(true); $dg = $this->getDataGenerator(); $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); $usermanage = $dg->create_user(); $user = $dg->create_user(); $syscontext = \context_system::instance(); // Creating specific roles. $managerole = $dg->create_role(array( 'name' => 'User manage', 'shortname' => 'manage' )); assign_capability('moodle/competency:planmanage', CAP_ALLOW, $managerole, $syscontext->id); assign_capability('moodle/competency:planview', CAP_ALLOW, $managerole, $syscontext->id); $dg->role_assign($managerole, $usermanage->id, $syscontext->id); $this->setUser($usermanage); $plan = array ( 'userid' => $usermanage->id, 'status' => \core_competency\plan::STATUS_ACTIVE ); $pl1 = $lpg->create_plan($plan); $framework = $lpg->create_framework(); $competency = $lpg->create_competency( array('competencyframeworkid' => $framework->get('id')) ); $lpg->create_plan_competency( array( 'planid' => $pl1->get('id'), 'competencyid' => $competency->get('id') ) ); $this->assertTrue(external::remove_competency_from_plan($pl1->get('id'), $competency->get('id'))); $this->assertCount(0, $pl1->get_competencies()); } /** * Test that we can add competency to plan if we have the right capability. * * @return void */ public function test_reorder_plan_competency(): void { $this->resetAfterTest(true); $dg = $this->getDataGenerator(); $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); $usermanage = $dg->create_user(); $user = $dg->create_user(); $syscontext = \context_system::instance(); // Creating specific roles. $managerole = $dg->create_role(array( 'name' => 'User manage', 'shortname' => 'manage' )); assign_capability('moodle/competency:planmanage', CAP_ALLOW, $managerole, $syscontext->id); assign_capability('moodle/competency:planview', CAP_ALLOW, $managerole, $syscontext->id); $dg->role_assign($managerole, $usermanage->id, $syscontext->id); $this->setUser($usermanage); $plan = array ( 'userid' => $usermanage->id, 'status' => \core_competency\plan::STATUS_ACTIVE ); $pl1 = $lpg->create_plan($plan); $framework = $lpg->create_framework(); $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $c3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $c4 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $c5 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $lpg->create_plan_competency(array('planid' => $pl1->get('id'), 'competencyid' => $c1->get('id'), 'sortorder' => 1)); $lpg->create_plan_competency(array('planid' => $pl1->get('id'), 'competencyid' => $c2->get('id'), 'sortorder' => 2)); $lpg->create_plan_competency(array('planid' => $pl1->get('id'), 'competencyid' => $c3->get('id'), 'sortorder' => 3)); $lpg->create_plan_competency(array('planid' => $pl1->get('id'), 'competencyid' => $c4->get('id'), 'sortorder' => 4)); $lpg->create_plan_competency(array('planid' => $pl1->get('id'), 'competencyid' => $c5->get('id'), 'sortorder' => 5)); // Test if removing competency from plan don't create sortorder holes. external::remove_competency_from_plan($pl1->get('id'), $c4->get('id')); $plancomp5 = plan_competency::get_record(array( 'planid' => $pl1->get('id'), 'competencyid' => $c5->get('id') )); $this->assertEquals(3, $plancomp5->get('sortorder')); $this->assertTrue(external::reorder_plan_competency($pl1->get('id'), $c2->get('id'), $c5->get('id'))); $this->assertTrue(external::reorder_plan_competency($pl1->get('id'), $c3->get('id'), $c1->get('id'))); $plancompetencies = plan_competency::get_records(array('planid' => $pl1->get('id')), 'sortorder', 'ASC'); $plcmp1 = $plancompetencies[0]; $plcmp2 = $plancompetencies[1]; $plcmp3 = $plancompetencies[2]; $plcmp4 = $plancompetencies[3]; $this->assertEquals($plcmp1->get('competencyid'), $c3->get('id')); $this->assertEquals($plcmp2->get('competencyid'), $c1->get('id')); $this->assertEquals($plcmp3->get('competencyid'), $c5->get('id')); $this->assertEquals($plcmp4->get('competencyid'), $c2->get('id')); } /** * Test resolving sortorder when we creating competency. */ public function test_fix_sortorder_when_creating_competency(): void { $this->resetAfterTest(true); $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); $framework = $lpg->create_framework(); $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'), 'sortorder' => 20)); $c3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'), 'sortorder' => 1)); $this->assertEquals(0, $c1->get('sortorder')); $this->assertEquals(1, $c2->get('sortorder')); $this->assertEquals(2, $c3->get('sortorder')); } /** * Test resolving sortorder when we delete competency. */ public function test_fix_sortorder_when_delete_competency(): void { $this->resetAfterTest(true); $this->setUser($this->creator); $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); $framework = $lpg->create_framework(); $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $c2a = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'), 'parentid' => $c2->get('id'))); $c2b = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'), 'parentid' => $c2->get('id'))); $c2c = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'), 'parentid' => $c2->get('id'))); $c2d = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'), 'parentid' => $c2->get('id'))); $this->assertEquals(0, $c1->get('sortorder')); $this->assertEquals(1, $c2->get('sortorder')); $this->assertEquals(0, $c2a->get('sortorder')); $this->assertEquals(1, $c2b->get('sortorder')); $this->assertEquals(2, $c2c->get('sortorder')); $this->assertEquals(3, $c2d->get('sortorder')); $result = external::delete_competency($c1->get('id')); $result = external_api::clean_returnvalue(external::delete_competency_returns(), $result); $c2->read(); $c2a->read(); $c2b->read(); $c2c->read(); $c2d->read(); $this->assertEquals(0, $c2->get('sortorder')); $this->assertEquals(0, $c2a->get('sortorder')); $this->assertEquals(1, $c2b->get('sortorder')); $this->assertEquals(2, $c2c->get('sortorder')); $this->assertEquals(3, $c2d->get('sortorder')); $result = external::delete_competency($c2b->get('id')); $result = external_api::clean_returnvalue(external::delete_competency_returns(), $result); $c2->read(); $c2a->read(); $c2c->read(); $c2d->read(); $this->assertEquals(0, $c2->get('sortorder')); $this->assertEquals(0, $c2a->get('sortorder')); $this->assertEquals(1, $c2c->get('sortorder')); $this->assertEquals(2, $c2d->get('sortorder')); } /** * Test resolving sortorder when moving a competency. */ public function test_fix_sortorder_when_moving_competency(): void { $this->resetAfterTest(true); $this->setUser($this->creator); $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); $framework = $lpg->create_framework(); $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $c1a = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'), 'parentid' => $c1->get('id'))); $c1b = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'), 'parentid' => $c1->get('id'))); $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $c2a = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'), 'parentid' => $c2->get('id'))); $c2b = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'), 'parentid' => $c2->get('id'))); $this->assertEquals(0, $c1->get('sortorder')); $this->assertEquals(0, $c1a->get('sortorder')); $this->assertEquals(1, $c1b->get('sortorder')); $this->assertEquals(1, $c2->get('sortorder')); $this->assertEquals(0, $c2a->get('sortorder')); $this->assertEquals(1, $c2b->get('sortorder')); $result = external::set_parent_competency($c2a->get('id'), $c1->get('id')); $result = external_api::clean_returnvalue(external::set_parent_competency_returns(), $result); $c1->read(); $c1a->read(); $c1b->read(); $c2->read(); $c2a->read(); $c2b->read(); $this->assertEquals(0, $c1->get('sortorder')); $this->assertEquals(0, $c1a->get('sortorder')); $this->assertEquals(1, $c1b->get('sortorder')); $this->assertEquals(2, $c2a->get('sortorder')); $this->assertEquals(1, $c2->get('sortorder')); $this->assertEquals(0, $c2b->get('sortorder')); // Move a root node. $result = external::set_parent_competency($c2->get('id'), $c1b->get('id')); $result = external_api::clean_returnvalue(external::set_parent_competency_returns(), $result); $c1->read(); $c1a->read(); $c1b->read(); $c2->read(); $c2a->read(); $c2b->read(); $this->assertEquals(0, $c1->get('sortorder')); $this->assertEquals(0, $c1a->get('sortorder')); $this->assertEquals(1, $c1b->get('sortorder')); $this->assertEquals(0, $c2->get('sortorder')); $this->assertEquals(0, $c2b->get('sortorder')); $this->assertEquals(2, $c2a->get('sortorder')); } public function test_grade_competency(): void { global $CFG; $this->setUser($this->creator); $dg = $this->getDataGenerator(); $lpg = $dg->get_plugin_generator('core_competency'); $f1 = $lpg->create_framework(); $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get('id'))); $evidence = external::grade_competency($this->user->id, $c1->get('id'), 1, 'Evil note'); $this->assertEquals('The competency rating was manually set.', $evidence->description); $this->assertEquals('A', $evidence->gradename); $this->assertEquals('Evil note', $evidence->note); $this->setUser($this->user); $this->expectException('\required_capability_exception'); $evidence = external::grade_competency($this->user->id, $c1->get('id'), 1); } public function test_grade_competency_in_course(): void { global $CFG; $this->setUser($this->creator); $dg = $this->getDataGenerator(); $lpg = $dg->get_plugin_generator('core_competency'); $course = $dg->create_course(['fullname' => 'Evil course']); $dg->enrol_user($this->creator->id, $course->id, 'editingteacher'); $dg->enrol_user($this->user->id, $course->id, 'student'); $f1 = $lpg->create_framework(); $c1 = $lpg->create_competency(['competencyframeworkid' => $f1->get('id')]); $lpg->create_course_competency(['courseid' => $course->id, 'competencyid' => $c1->get('id')]); $evidence = external::grade_competency_in_course($course->id, $this->user->id, $c1->get('id'), 1, 'Evil note'); $this->assertEquals('The competency rating was manually set in the course \'Course: Evil course\'.', $evidence->description); $this->assertEquals('A', $evidence->gradename); $this->assertEquals('Evil note', $evidence->note); $this->setUser($this->user); $this->expectException('\required_capability_exception'); $evidence = external::grade_competency_in_course($course->id, $this->user->id, $c1->get('id'), 1); } public function test_grade_competency_in_plan(): void { global $CFG; $this->setUser($this->creator); $dg = $this->getDataGenerator(); $lpg = $dg->get_plugin_generator('core_competency'); $f1 = $lpg->create_framework(); $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get('id'))); $tpl = $lpg->create_template(); $lpg->create_template_competency(array('templateid' => $tpl->get('id'), 'competencyid' => $c1->get('id'))); $plan = $lpg->create_plan(array('userid' => $this->user->id, 'templateid' => $tpl->get('id'), 'name' => 'Evil')); $uc = $lpg->create_user_competency(array('userid' => $this->user->id, 'competencyid' => $c1->get('id'))); $evidence = external::grade_competency_in_plan($plan->get('id'), $c1->get('id'), 1, 'Evil note'); $this->assertEquals('The competency rating was manually set in the learning plan \'Evil\'.', $evidence->description); $this->assertEquals('A', $evidence->gradename); $this->assertEquals('Evil note', $evidence->note); $this->setUser($this->user); $this->expectException('\required_capability_exception'); $evidence = external::grade_competency_in_plan($plan->get('id'), $c1->get('id'), 1); } /** * Test update course competency settings. */ public function test_update_course_competency_settings(): void { $this->resetAfterTest(true); $dg = $this->getDataGenerator(); $course = $dg->create_course(); $roleid = $dg->create_role(); $noobroleid = $dg->create_role(); $context = \context_course::instance($course->id); $compmanager = $this->getDataGenerator()->create_user(); $compnoob = $this->getDataGenerator()->create_user(); assign_capability('moodle/competency:coursecompetencyconfigure', CAP_ALLOW, $roleid, $context->id, true); assign_capability('moodle/competency:coursecompetencyview', CAP_ALLOW, $roleid, $context->id, true); assign_capability('moodle/competency:coursecompetencyview', CAP_ALLOW, $noobroleid, $context->id, true); role_assign($roleid, $compmanager->id, $context->id); role_assign($noobroleid, $compnoob->id, $context->id); $dg->enrol_user($compmanager->id, $course->id, $roleid); $dg->enrol_user($compnoob->id, $course->id, $noobroleid); $this->setUser($compmanager); // Start the test. $result = external::update_course_competency_settings($course->id, array('pushratingstouserplans' => true)); $settings = course_competency_settings::get_by_courseid($course->id); $this->assertTrue((bool)$settings->get('pushratingstouserplans')); $result = external::update_course_competency_settings($course->id, array('pushratingstouserplans' => false)); $settings = course_competency_settings::get_by_courseid($course->id); $this->assertFalse((bool)$settings->get('pushratingstouserplans')); $this->setUser($compnoob); $this->expectException('\required_capability_exception'); $result = external::update_course_competency_settings($course->id, array('pushratingstouserplans' => true)); } /** * Test that we can list competencies with a filter. * * @return void */ public function test_list_competencies_with_filter(): void { $this->resetAfterTest(true); $this->setAdminUser(); $dg = $this->getDataGenerator(); $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); $framework = $lpg->create_framework(); $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $c3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $c4 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $c5 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); // Test if removing competency from plan don't create sortorder holes. $filters = []; $sort = 'id'; $order = 'ASC'; $skip = 0; $limit = 0; $result = external::list_competencies($filters, $sort, $order, $skip, $limit); $this->assertCount(5, $result); $result = external::list_competencies($filters, $sort, $order, 2, $limit); $this->assertCount(3, $result); $result = external::list_competencies($filters, $sort, $order, 2, 2); $this->assertCount(2, $result); $filter = $result[0]->shortname; $filters[0] = ['column' => 'shortname', 'value' => $filter]; $result = external::list_competencies($filters, $sort, $order, $skip, $limit); $this->assertCount(1, $result); $this->assertEquals($filter, $result[0]->shortname); } /** * Test that we can list competencies with a course module. * * @return void */ public function test_list_competencies_with_course_module(): void { $this->resetAfterTest(true); $this->setAdminUser(); $dg = $this->getDataGenerator(); $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); $course = $dg->create_course(); $framework = $lpg->create_framework(); $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $c3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $c4 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $c5 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id'))); $cc1 = api::add_competency_to_course($course->id, $c1->get('id')); $cc2 = api::add_competency_to_course($course->id, $c2->get('id')); $cc3 = api::add_competency_to_course($course->id, $c3->get('id')); $pagegenerator = $this->getDataGenerator()->get_plugin_generator('mod_page'); $page = $pagegenerator->create_instance(array('course' => $course->id)); $cm = get_coursemodule_from_instance('page', $page->id); // Add a link and list again. $ccm1 = api::add_competency_to_course_module($cm, $c1->get('id')); $ccm2 = api::add_competency_to_course_module($cm, $c2->get('id')); // Test list competencies for this course module. $total = external::count_course_module_competencies($cm->id); $result = external::list_course_module_competencies($cm->id); $this->assertCount($total, $result); // Now we should have an array and each element of the array should have a competency and // a coursemodulecompetency. foreach ($result as $instance) { $cmc = $instance['coursemodulecompetency']; $c = $instance['competency']; $this->assertEquals($cmc->competencyid, $c->id); } } } PK\Ӝcreate_gradecategories_test.phpnu[. namespace core_grades\external; use core_grades\external\create_gradecategories; use core_external\external_api; defined('MOODLE_INTERNAL') || die; global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); /** * Unit tests for the core_grades\external\create_gradecategories webservice. * * @package core_grades * @category external * @copyright 2021 Peter Burnett * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.11 */ final class create_gradecategories_test extends \externallib_advanced_testcase { /** * Test create_gradecategories. * * @return void */ public function test_create_gradecategories(): void { global $DB; $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $this->setAdminUser(); // Test the most basic gradecategory creation. $status1 = create_gradecategories::execute($course->id, [['fullname' => 'Test Category 1', 'options' => []]]); $status1 = external_api::clean_returnvalue(create_gradecategories::execute_returns(), $status1); $courseparentcat = \grade_category::fetch_course_category($course->id); $record1 = $DB->get_record('grade_categories', ['id' => $status1['categoryids'][0]]); $this->assertEquals('Test Category 1', $record1->fullname); // Confirm that the parent category for this category is the top level category for the course. $this->assertEquals($courseparentcat->id, $record1->parent); $this->assertEquals(2, $record1->depth); // Now create a category as a child of the newly created category. $status2 = create_gradecategories::execute($course->id, [['fullname' => 'Test Category 2', 'options' => ['parentcategoryid' => $record1->id]]]); $status2 = external_api::clean_returnvalue(create_gradecategories::execute_returns(), $status2); $record2 = $DB->get_record('grade_categories', ['id' => $status2['categoryids'][0]]); $this->assertEquals($record1->id, $record2->parent); $this->assertEquals(3, $record2->depth); // Check the path is correct. $this->assertEquals('/' . implode('/', [$courseparentcat->id, $record1->id, $record2->id]) . '/', $record2->path); // Now create a category with some customised data and check the returns. This customises every value. $customopts = [ 'aggregation' => GRADE_AGGREGATE_MEAN, 'aggregateonlygraded' => 0, 'aggregateoutcomes' => 1, 'droplow' => 1, 'itemname' => 'item', 'iteminfo' => 'info', 'idnumber' => 'idnumber', 'gradetype' => GRADE_TYPE_TEXT, 'grademax' => 5, 'grademin' => 2, 'gradepass' => 3, 'display' => GRADE_DISPLAY_TYPE_LETTER, // Hack. This must be -2 to use the default setting. 'decimals' => 3, 'hiddenuntil' => time(), 'locktime' => time(), 'weightoverride' => 1, 'aggregationcoef2' => 20, 'parentcategoryid' => $record2->id ]; $status3 = create_gradecategories::execute($course->id, [['fullname' => 'Test Category 3', 'options' => $customopts]]); $status3 = external_api::clean_returnvalue(create_gradecategories::execute_returns(), $status3); $cat3 = new \grade_category(['courseid' => $course->id, 'id' => $status3['categoryids'][0]], true); $cat3->load_grade_item(); // Lets check all of the data is in the right shape. $this->assertEquals(GRADE_AGGREGATE_MEAN, $cat3->aggregation); $this->assertEquals(0, $cat3->aggregateonlygraded); $this->assertEquals(1, $cat3->aggregateoutcomes); $this->assertEquals(1, $cat3->droplow); $this->assertEquals('item', $cat3->grade_item->itemname); $this->assertEquals('info', $cat3->grade_item->iteminfo); $this->assertEquals('idnumber', $cat3->grade_item->idnumber); $this->assertEquals(GRADE_TYPE_TEXT, $cat3->grade_item->gradetype); $this->assertEquals(5, $cat3->grade_item->grademax); $this->assertEquals(2, $cat3->grade_item->grademin); $this->assertEquals(3, $cat3->grade_item->gradepass); $this->assertEquals(GRADE_DISPLAY_TYPE_LETTER, $cat3->grade_item->display); $this->assertEquals(3, $cat3->grade_item->decimals); $this->assertGreaterThanOrEqual($cat3->grade_item->hidden, time()); $this->assertGreaterThanOrEqual($cat3->grade_item->locktime, time()); $this->assertEquals(1, $cat3->grade_item->weightoverride); // Coefficient is converted to percentage. $this->assertEquals(0.2, $cat3->grade_item->aggregationcoef2); $this->assertEquals($record2->id, $cat3->parent); // Now test creating 2 in parallel, and nesting them. $status4 = create_gradecategories::execute($course->id, [ [ 'fullname' => 'Test Category 4', 'options' => [ 'idnumber' => 'secondlevel' ], ], [ 'fullname' => 'Test Category 5', 'options' => [ 'idnumber' => 'thirdlevel', 'parentcategoryidnumber' => 'secondlevel' ], ], ]); $status4 = external_api::clean_returnvalue(create_gradecategories::execute_returns(), $status4); $secondlevel = $DB->get_record('grade_categories', ['id' => $status4['categoryids'][0]]); $thirdlevel = $DB->get_record('grade_categories', ['id' => $status4['categoryids'][1]]); // Confirm that the parent category for secondlevel is the top level category for the course. $this->assertEquals($courseparentcat->id, $secondlevel->parent); $this->assertEquals(2, $record1->depth); // Confirm that the parent category for thirdlevel is the secondlevel category. $this->assertEquals($secondlevel->id, $thirdlevel->parent); $this->assertEquals(3, $thirdlevel->depth); // Check the path is correct. $this->assertEquals('/' . implode('/', [$courseparentcat->id, $secondlevel->id, $thirdlevel->id]) . '/', $thirdlevel->path); } } PK\eLget_feedback_test.phpnu[. namespace core_grades\external; defined('MOODLE_INTERNAL') || die; global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); /** * Unit tests for the core_grades\external\get_feedback webservice. * * @package core_grades * @category external * @copyright 2023 Kevin Percy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 4.2 * @covers \core_grades\external\get_feedback */ final class get_feedback_test extends \externallib_advanced_testcase { /** * Test get_feedback. * * @dataProvider get_feedback_provider * @param string|null $feedback The feedback text added for the grade item. * @param array $expected The expected feedback data. * @return void */ public function test_get_feedback(?string $feedback, array $expected): void { $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(['firstname' => 'John', 'lastname' => 'Doe', 'email' => 'johndoe@example.com']); $this->getDataGenerator()->enrol_user($user->id, $course->id); $gradeitem = $this->getDataGenerator()->create_grade_item(['itemname' => 'Grade item 1', 'courseid' => $course->id]); $gradegradedata = [ 'itemid' => $gradeitem->id, 'userid' => $user->id, ]; if ($feedback) { $gradegradedata['feedback'] = $feedback; } $this->getDataGenerator()->create_grade_grade($gradegradedata); $this->setAdminUser(); $feedbackdata = get_feedback::execute($course->id, $user->id, $gradeitem->id); $this->assertEquals($expected['feedbacktext'], $feedbackdata['feedbacktext']); $this->assertEquals($expected['title'], $feedbackdata['title']); $this->assertEquals($expected['fullname'], $feedbackdata['fullname']); $this->assertEquals($expected['additionalfield'], $feedbackdata['additionalfield']); } /** * Data provider for test_get_feedback(). * * @return array */ public static function get_feedback_provider(): array { return [ 'Return when feedback is set.' => [ 'Test feedback', [ 'feedbacktext' => 'Test feedback', 'title' => 'Grade item 1', 'fullname' => 'John Doe', 'additionalfield' => 'johndoe@example.com' ] ], 'Return when feedback is not set.' => [ null, [ 'feedbacktext' => null, 'title' => 'Grade item 1', 'fullname' => 'John Doe', 'additionalfield' => 'johndoe@example.com' ] ] ]; } /** * Test get_feedback with invalid requests. * * @dataProvider get_feedback_invalid_request_provider * @param string $loggeduserrole The role of the logged user. * @param bool $feedbacknotincourse Whether to request a feedback for a grade item which is not a part of the course. * @param array $expectedexception The expected exception. * @return void */ public function test_get_feedback_invalid_request(string $loggeduserrole, bool $feedbacknotincourse, array $expectedexception = []): void { $this->resetAfterTest(true); // Create a course with a user and a grade item. $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id); $gradeitem = $this->getDataGenerator()->create_grade_item(['courseid' => $course->id]); // Add feedback for the grade item in course. $gradegradedata = [ 'itemid' => $gradeitem->id, 'userid' => $user->id, 'feedback' => 'Test feedback', ]; $this->getDataGenerator()->create_grade_grade($gradegradedata); // Set the current user as specified. if ($loggeduserrole === 'user') { $this->setUser($user); } else if ($loggeduserrole === 'guest') { $this->setGuestUser(); } else { $this->setAdminUser(); } if ($feedbacknotincourse) { // Create a new course which will be later used in the feedback request call. $course = $this->getDataGenerator()->create_course(); } $this->expectException($expectedexception['exceptionclass']); if (!empty($expectedexception['exceptionmessage'])) { $this->expectExceptionMessage($expectedexception['exceptionmessage']); } get_feedback::execute($course->id, $user->id, $gradeitem->id); } /** * Data provider for test_get_feedback_invalid_request(). * * @return array */ public static function get_feedback_invalid_request_provider(): array { return [ 'Logged user does not have permissions to view feedback.' => [ 'user', false, ['exceptionclass' => \required_capability_exception::class] ], 'Guest user cannot view feedback.' => [ 'guest', false, ['exceptionclass' => \require_login_exception::class] ], 'Request feedback for a grade item which is not a part of the course.' => [ 'admin', true, [ 'exceptionclass' => \invalid_parameter_exception::class, 'exceptionmessage' => 'Course ID and item ID mismatch', ] ] ]; } } PK\Õ""get_gradable_users_test.phpnu[. namespace core_grades\external; use core_external\external_api; defined('MOODLE_INTERNAL') || die; global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); /** * Unit tests for the core_grades\external\get_gradable_users. * * @package core_grades * @category external * @copyright 2023 Ilya Tregubov * @covers \core_grades\external\get_gradable_users */ final class get_gradable_users_test extends \externallib_advanced_testcase { /** * Test the behaviour of get_gradable_users. * * @dataProvider execute_data * @param bool $onlyactiveenrol if we should only return active enrolments * @param bool $grouprestricted if we should only return users within a group * @param array $expected expected users */ public function test_execute(bool $onlyactiveenrol, bool $grouprestricted, array $expected): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); // Create and enrol test users. $student1 = $generator->create_user(['username' => 'student1', 'firstname' => 'Apple', 'lastname' => 'Apricot']); $student2 = $generator->create_user(['username' => 'student2', 'firstname' => 'Banana', 'lastname' => 'Blueberry']); $student3 = $generator->create_user(['username' => 'student3', 'firstname' => 'Cherry', 'lastname' => 'Cranberry']); $student4 = $generator->create_user(['username' => 'student4', 'firstname' => 'Durian', 'lastname' => 'Dracontomelon']); $student5 = $generator->create_user(['username' => 'student5', 'firstname' => 'Eggplant', 'lastname' => 'Ensete']); $role = $DB->get_record('role', ['shortname' => 'student'], '*', MUST_EXIST); $generator->enrol_user($student1->id, $course->id, $role->id); $generator->enrol_user($student2->id, $course->id, $role->id); $generator->enrol_user($student3->id, $course->id, $role->id); $generator->enrol_user($student4->id, $course->id, $role->id); $generator->enrol_user($student5->id, $course->id, $role->id, 'manual', 0, 0, ENROL_USER_SUSPENDED); $group1 = $generator->create_group(['courseid' => $course->id]); $group2 = $generator->create_group(['courseid' => $course->id]); $generator->create_group_member(['userid' => $student1->id, 'groupid' => $group1->id]); $generator->create_group_member(['userid' => $student1->id, 'groupid' => $group2->id]); $generator->create_group_member(['userid' => $student2->id, 'groupid' => $group2->id]); $generator->create_group_member(['userid' => $student3->id, 'groupid' => $group2->id]); $DB->set_field('course', 'groupmode', SEPARATEGROUPS, ['id' => $course->id]); $teacher = $generator->create_user(['username' => 'teacher1']); $role = $DB->get_record('role', ['shortname' => 'editingteacher'], '*', MUST_EXIST); $generator->enrol_user($teacher->id, $course->id, $role->id); $generator->create_module('assign', ['course' => $course->id]); $groupid = $grouprestricted ? $group2->id : 0; $result = get_gradable_users::execute($course->id, $groupid, $onlyactiveenrol); $result = external_api::clean_returnvalue(get_gradable_users::execute_returns(), $result); $mapped = array_map(function($user) { return [ 'fullname' => $user['fullname'], 'firstname' => $user['firstname'], 'lastname' => $user['lastname'], 'profileimageurl' => $user['profileimageurl'], ]; }, array_values($result['users'])); $this->assertEquals($expected, $mapped); } /** * Data provider for test_execute. * * @return array */ public static function execute_data(): array { return [ 'All users' => [ false, false, [ [ 'profileimageurl' => 'https://www.example.com/moodle/theme/image.php/boost/core/1/u/f1', 'firstname' => 'Apple', 'lastname' => 'Apricot', 'fullname' => 'Apple Apricot', ], [ 'profileimageurl' => 'https://www.example.com/moodle/theme/image.php/boost/core/1/u/f1', 'firstname' => 'Banana', 'lastname' => 'Blueberry', 'fullname' => 'Banana Blueberry', ], [ 'profileimageurl' => 'https://www.example.com/moodle/theme/image.php/boost/core/1/u/f1', 'firstname' => 'Cherry', 'lastname' => 'Cranberry', 'fullname' => 'Cherry Cranberry', ], [ 'profileimageurl' => 'https://www.example.com/moodle/theme/image.php/boost/core/1/u/f1', 'firstname' => 'Durian', 'lastname' => 'Dracontomelon', 'fullname' => 'Durian Dracontomelon', ], [ 'profileimageurl' => 'https://www.example.com/moodle/theme/image.php/boost/core/1/u/f1', 'firstname' => 'Eggplant', 'lastname' => 'Ensete', 'fullname' => 'Eggplant Ensete', ], ], ], 'Only active enrolment' => [ true, false, [ [ 'profileimageurl' => 'https://www.example.com/moodle/theme/image.php/boost/core/1/u/f1', 'firstname' => 'Apple', 'lastname' => 'Apricot', 'fullname' => 'Apple Apricot', ], [ 'profileimageurl' => 'https://www.example.com/moodle/theme/image.php/boost/core/1/u/f1', 'firstname' => 'Banana', 'lastname' => 'Blueberry', 'fullname' => 'Banana Blueberry', ], [ 'profileimageurl' => 'https://www.example.com/moodle/theme/image.php/boost/core/1/u/f1', 'firstname' => 'Cherry', 'lastname' => 'Cranberry', 'fullname' => 'Cherry Cranberry', ], [ 'profileimageurl' => 'https://www.example.com/moodle/theme/image.php/boost/core/1/u/f1', 'firstname' => 'Durian', 'lastname' => 'Dracontomelon', 'fullname' => 'Durian Dracontomelon', ], ], ], 'Group restricted' => [ false, true, [ [ 'profileimageurl' => 'https://www.example.com/moodle/theme/image.php/boost/core/1/u/f1', 'firstname' => 'Apple', 'lastname' => 'Apricot', 'fullname' => 'Apple Apricot', ], [ 'profileimageurl' => 'https://www.example.com/moodle/theme/image.php/boost/core/1/u/f1', 'firstname' => 'Banana', 'lastname' => 'Blueberry', 'fullname' => 'Banana Blueberry', ], [ 'profileimageurl' => 'https://www.example.com/moodle/theme/image.php/boost/core/1/u/f1', 'firstname' => 'Cherry', 'lastname' => 'Cranberry', 'fullname' => 'Cherry Cranberry', ], ], ], ]; } } PK\Zkget_grade_tree_test.phpnu[. namespace core_grades\external; use core_external\external_api; defined('MOODLE_INTERNAL') || die; global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); /** * Unit tests for core_grades\external\get_grade_tree. * * @package core_grades * @category external * @copyright 2023 Mihail Geshoski * @covers \core_grades\external\get_grade_tree */ final class get_grade_tree_test extends \externallib_advanced_testcase { /** * Test the return value of the external function. * * @return void */ public function test_execute(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(['fullname' => 'Course']); $coursegradecategory = \grade_category::fetch_course_category($course->id); // Create a grade item 'Grade item' and grade category 'Category 1' within the course grade category. $gradeitem = $this->getDataGenerator()->create_grade_item( ['courseid' => $course->id, 'itemname' => 'Grade item']); $gradecategory1 = $this->getDataGenerator()->create_grade_category( ['courseid' => $course->id, 'fullname' => 'Category 1']); // Create a grade item 'Grade item 1' and grade category 'Category 2' within 'Category 1'. $gradeitem1 = $this->getDataGenerator()->create_grade_item( ['courseid' => $course->id, 'itemname' => 'Grade item 1', 'categoryid' => $gradecategory1->id]); $gradecategory2 = $this->getDataGenerator()->create_grade_category( ['courseid' => $course->id, 'fullname' => 'Category 2', 'parent' => $gradecategory1->id]); // Create a grade item 'Grade item 2' and grade category 'Category 3' (with no children) within 'Category 2'. $gradeitem2 = $this->getDataGenerator()->create_grade_item( ['courseid' => $course->id, 'itemname' => 'Grade item 2', 'categoryid' => $gradecategory2->id]); $gradecategory3 = $this->getDataGenerator()->create_grade_category( ['courseid' => $course->id, 'fullname' => 'Category 3', 'parent' => $gradecategory2->id]); $result = get_grade_tree::execute($course->id); $result = external_api::clean_returnvalue(get_grade_tree::execute_returns(), $result); $expected = json_encode([ 'id' => $coursegradecategory->id, 'name' => 'Course', 'iscategory' => true, 'haschildcategories' => true, 'children' => [ [ 'id' => $gradeitem->id, 'name' => 'Grade item', 'iscategory' => false, 'children' => null ], [ 'id' => $gradecategory1->id, 'name' => 'Category 1', 'iscategory' => true, 'haschildcategories' => true, 'children' => [ [ 'id' => $gradeitem1->id, 'name' => 'Grade item 1', 'iscategory' => false, 'children' => null ], [ 'id' => $gradecategory2->id, 'name' => 'Category 2', 'iscategory' => true, 'haschildcategories' => true, 'children' => [ [ 'id' => $gradeitem2->id, 'name' => 'Grade item 2', 'iscategory' => false, 'children' => null ], [ 'id' => $gradecategory3->id, 'name' => 'Category 3', 'iscategory' => true, 'haschildcategories' => false, 'children' => null ] ] ] ] ] ] ]); $this->assertEquals($expected, $result); } } PK\.O O get_gradeitems_test.phpnu[. namespace core_grades\external; use core_grades\external\get_gradeitems as get_gradeitems; use core_external\external_api; use grade_item; defined('MOODLE_INTERNAL') || die; global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); /** * Unit tests for the core_grades\external\get_gradeitems. * * @package core_grades * @category external * @copyright 2023 Mathew May * @covers \core_grades\external\get_gradeitems */ final class get_gradeitems_test extends \externallib_advanced_testcase { public function test_execute(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course->id]); $this->getDataGenerator()->create_module('assign', ['course' => $course->id, 'name' => 'Assignment & grade items']); $result = get_gradeitems::execute($course->id); $result = external_api::clean_returnvalue(get_gradeitems::execute_returns(), $result); $allgradeitems = grade_item::fetch_all(['courseid' => $course->id]); $gradeitems = array_filter($allgradeitems, function($item) { $item->itemname = $item->get_name(); $item->category = $item->get_parent_category()->get_name(); return $item->gradetype != GRADE_TYPE_NONE && !$item->is_category_item() && !$item->is_course_item(); }); // Move back from grade items into an array of arrays. $mapped = array_map(function($item) { return [ 'id' => $item->id, 'itemname' => $item->itemname, 'category' => $item->category ]; }, array_values($gradeitems)); $this->assertEquals($mapped, $result['gradeItems']); } } PK\ڦcompletion_info_exporter.phpnu[. declare(strict_types=1); namespace core_completion\external; defined('MOODLE_INTERNAL') || die(); use renderer_base; /** * Completion info exporter * * @package core_completion * @copyright 2021 Dongsheng Cai * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class completion_info_exporter extends \core\external\exporter { /** * @var object $course moodle course object */ private $course; /** * @var object|cm_info $cm course module info */ private $cminfo; /** * @var int $userid user id */ private $userid; /** * Constructor for the completion info exporter. * * @param object $course course object * @param object|cm_info $cm course module info * @param int $userid user id * @param array $related related values */ public function __construct(object $course, object $cm, int $userid, array $related = []) { $this->course = $course; $this->cminfo = \cm_info::create($cm, $userid); $this->userid = $userid; parent::__construct([], $related); } /** * Get the additional values to inject while exporting. * * @param renderer_base $output The renderer. * @return array Keys are the property names, values are their values. */ protected function get_other_values(renderer_base $output): array { $cmcompletion = \core_completion\cm_completion_details::get_instance($this->cminfo, $this->userid); $cmcompletiondetails = $cmcompletion->get_details(); $details = []; foreach ($cmcompletiondetails as $rulename => $rulevalue) { $details[] = [ 'rulename' => $rulename, 'rulevalue' => (array)$rulevalue, ]; } return [ 'state' => $cmcompletion->get_overall_completion(), 'timecompleted' => $cmcompletion->get_timemodified(), 'overrideby' => $cmcompletion->overridden_by(), 'valueused' => \core_availability\info::completion_value_used($this->course, $this->cminfo->id), 'hascompletion' => $cmcompletion->has_completion(), 'isautomatic' => $cmcompletion->is_automatic(), 'istrackeduser' => $cmcompletion->is_tracked_user(), 'uservisible' => $this->cminfo->uservisible, 'details' => $details, 'isoverallcomplete' => $cmcompletion->is_overall_complete(), ]; } /** * Return the list of additional properties used only for display. * * @return array Keys are the property names, and value their definition. */ public static function define_other_properties(): array { return [ 'state' => [ 'type' => PARAM_INT, 'description' => 'overall completion state of this course module.', ], 'timecompleted' => [ 'type' => PARAM_INT, 'description' => 'course completion timestamp.', ], 'overrideby' => [ 'type' => PARAM_INT, 'description' => 'user ID that has overridden the completion state of this activity for the user.', 'null' => NULL_ALLOWED, ], 'valueused' => [ 'type' => PARAM_BOOL, 'description' => 'True if module is used in a condition, false otherwise.', ], 'hascompletion' => [ 'type' => PARAM_BOOL, 'description' => 'Whether this activity module has completion enabled.' ], 'isautomatic' => [ 'type' => PARAM_BOOL, 'description' => 'Whether this activity module instance tracks completion automatically.' ], 'istrackeduser' => [ 'type' => PARAM_BOOL, 'description' => 'Checks whether completion is being tracked for this user.' ], 'uservisible' => [ 'type' => PARAM_BOOL, 'description' => 'Whether this activity is visible to user.' ], 'details' => [ 'multiple' => true, 'description' => 'An array of completion details containing the description and status.', 'type' => [ 'rulename' => [ 'type' => PARAM_TEXT, ], 'rulevalue' => [ 'type' => [ 'status' => [ 'type' => PARAM_INT, ], 'description' => [ 'type' => PARAM_TEXT, ] ] ] ] ], 'isoverallcomplete' => [ 'type' => PARAM_BOOL, 'description' => 'Whether the overall completion state of this course module should be marked as complete or not.', 'optional' => true, ], ]; } } PK>\LYYexternallib.phpnu[PK>\Z1'Yclasses/external_multiple_structure.phpnu[PK>\#{\%)`classes/external_single_structure.phpnu[PK>\jɓ7T2T2sfclasses/privacy/provider.phpnu[PK>\DͿ'h'hclasses/external_api.phpnu[PK>\SD!!classes/external_files.phpnu[PK>\VV(classes/external_function_parameters.phpnu[PK>\eTTclasses/external_warnings.phpnu[PK>\Pd(<#classes/restricted_context_exception.phpnu[PK>\ǭCkCk(classes/util.phpnu[PK>\hHHclasses/external_settings.phpnu[PK>\ D 7classes/external_value.phpnu[PK>\: : ![classes/external_format_value.phpnu[PK>\"O߶ classes/external_description.phpnu[PK>\O   UPGRADING.mdnu[PK>\H\\2tests/external_api_test.phpnu[PK>\ʷ  tests/external_value_test.phpnu[PK>\ [[:%tests/privacy/provider_test.phpnu[PK>\RU  tests/external_settings_test.phpnu[PK>\A%==Ytests/plugin_checks_test.phpnu[PK>\=nj&tests/classes/externallib_testcase.phpnu[PK>\f..#:tests/external_externallib_test.phpnu[PK>\2tests/external_files_test.phpnu[PK>\dݹEEwtests/util_test.phpnu[PK|\!Y  )get_available_in_context.phpnu[PK|\G$߻<get_all_states.phpnu[PK:}\`> *Kprepare_private_files_for_edition_test.phpnu[PK:}\u_ppWupdate_private_files_test.phpnu[PK:}\(1T&supdate_user_device_public_key_test.phpnu[PK\{//stored_file_exporter_test.phpnu[PK/\t external_test.phpnu[PK\Ӝcreate_gradecategories_test.phpnu[PK\eLget_feedback_test.phpnu[PK\Õ""get_gradable_users_test.phpnu[PK\Zkget_grade_tree_test.phpnu[PK\.O O  get_gradeitems_test.phpnu[PK\ڦcompletion_info_exporter.phpnu[PK%%X (