diff --git a/classes/text_filter.php b/classes/text_filter.php
new file mode 100644
index 0000000..b5db92c
--- /dev/null
+++ b/classes/text_filter.php
@@ -0,0 +1,5357 @@
+.
+
+namespace filter_filtercodes;
+
+/**
+ * Main filter code for FilterCodes.
+ *
+ * @package filter_filtercodes
+ * @copyright 2017-2024 TNG Consulting Inc. - www.tngconsulting.ca
+ * @author Michael Milette
+ * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use block_online_users\fetcher;
+use \core_table\local\filter\integer_filter;
+use \core_user\table\participants_filterset;
+use \core_user\table\participants_search;
+use Endroid\QrCode\QrCode;
+
+require_once($CFG->dirroot . '/course/renderer.php');
+
+/**
+ * Extends the moodle_text_filter class to provide plain text support for new tags.
+ *
+ * @copyright 2017-2024 TNG Consulting Inc. - www.tngconsulting.ca
+ * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class text_filter extends \core_filters\text_filter {
+ /** @var object $archetypes Object array of Moodle archetypes. */
+ public $archetypes = [];
+ /** @var array $customroles array of Roles key is shortname and value is the id */
+ private static $customroles = [];
+ /**
+ * @var array $customrolespermissions array of Roles key is shortname + context_id and the value is a boolean showing if
+ * user is allowed
+ */
+ private static $customrolespermissions = [];
+
+ /**
+ * Constructor: Get the role IDs associated with each of the archetypes.
+ */
+ public function __construct() {
+
+ // Note: This array must correspond to the one in function hasminarchetype.
+ $archetypelist = ['manager' => 1, 'coursecreator' => 2, 'editingteacher' => 3, 'teacher' => 4, 'student' => 5];
+ foreach ($archetypelist as $archetype => $level) {
+ $roleids = [];
+ // Build array of roles.
+ foreach (get_archetype_roles($archetype) as $role) {
+ $roleids[] = $role->id;
+ }
+ $this->archetypes[$archetype] = (object) ['level' => $level, 'roleids' => $roleids];
+ }
+ }
+
+ /**
+ * Determine if any of the user's roles includes specified archetype.
+ *
+ * @param string $archetype Name of archetype.
+ * @return boolean Does: true, Does not: false.
+ */
+ private function hasarchetype($archetype) {
+ // If not logged in or is just a guestuser, definitely doesn't have the archetype we want.
+ if (!isloggedin() || isguestuser()) {
+ return false;
+ }
+
+ // Handle caching of results.
+ static $archetypes = [];
+ if (isset($archetypes[$archetype])) {
+ return $archetypes[$archetype];
+ }
+
+ global $USER, $PAGE;
+ $archetypes[$archetype] = false;
+ if (is_role_switched($PAGE->course->id)) { // Has switched roles.
+ $context = \context_course::instance($PAGE->course->id);
+ $id = $USER->access['rsw'][$context->path];
+ $archetypes[$archetype] = in_array($id, $this->archetypes[$archetype]->roleids);
+ } else {
+ // For each of the roles associated with the archetype, check if the user has one of the roles.
+ foreach ($this->archetypes[$archetype]->roleids as $roleid) {
+ if (user_has_role_assignment($USER->id, $roleid, $PAGE->context->id)) {
+ $archetypes[$archetype] = true;
+ }
+ }
+ }
+ return $archetypes[$archetype];
+ }
+
+ /**
+ * Determine if the user only has a specified archetype amongst the user's role and no others.
+ * Example: Can be a student but not also be a teacher or manager.
+ *
+ * @param string $archetype Name of archetype.
+ * @return boolean Does: true, Does not: false.
+ */
+ private function hasonlyarchetype($archetype) {
+ if ($this->hasarchetype($archetype)) {
+ $archetypes = array_keys($this->archetypes);
+ foreach ($archetypes as $archetypename) {
+ if ($archetypename != $archetype && $this->hasarchetype($archetypename)) {
+ return false;
+ }
+ }
+ global $PAGE;
+ if (is_role_switched($PAGE->course->id)) {
+ // Ignore site admin status if we have switched roles.
+ return true;
+ } else {
+ return is_siteadmin();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Determine if the user has the specified archetype or one with elevated capabilities.
+ * Example: Can be a teacher, course creator, manager or Administrator but not a student.
+ *
+ * @param string $minarchetype Name of archetype.
+ * @return boolean User meets minimum archetype requirement: true, does not: false.
+ */
+ private function hasminarchetype($minarchetype) {
+ // Note: This array must start with one blank entry followed by the same list found in in __construct().
+ $archetypelist = ['', 'manager', 'coursecreator', 'editingteacher', 'teacher', 'student'];
+ // For each archetype level between the one specified and 'manager'.
+ for ($level = $this->archetypes[$minarchetype]->level; $level >= 1; $level--) {
+ // Check to see if any of the user's roles correspond to the archetype.
+ if ($this->hasarchetype($archetypelist[$level])) {
+ return true;
+ }
+ }
+ // Return true regardless of the archetype if we are an administrator and not in a switched role.
+ global $PAGE;
+ return !is_role_switched($PAGE->course->id) && is_siteadmin();
+ }
+
+ /**
+ * Checks if a user has a custom role or not within the current context.
+ *
+ * @param string $roleshortname The role's shortname.
+ * @param integer $contextid The context where the tag appears.
+ * @return boolean True if user has custom role, otherwise, false.
+ */
+ private function hascustomrole($roleshortname, $contextid = 0) {
+ $keytocheck = $roleshortname . '-' . $contextid;
+ if (!isset(self::$customrolespermissions[$keytocheck])) {
+ global $USER, $DB;
+ if (!isset(self::$customroles[$roleshortname])) {
+ self::$customroles[$roleshortname] = $DB->get_field('role', 'id', ['shortname' => $roleshortname]);
+ }
+ $hasrole = false;
+ if (self::$customroles[$roleshortname]) {
+ $hasrole = user_has_role_assignment($USER->id, self::$customroles[$roleshortname], $contextid);
+ }
+ self::$customrolespermissions[$keytocheck] = $hasrole;
+ }
+
+ return self::$customrolespermissions[$keytocheck];
+ }
+
+ /**
+ * Determine if the specified user has the specified role anywhere in the system.
+ *
+ * @param string $roleshortname Role shortname.
+ * @param integer $userid The user's ID.
+ * @return boolean True if the user has the role, false if they do not.
+ */
+ private function hasarole($roleshortname, $userid) {
+ // Cache list of user's roles.
+ static $list;
+
+ if (!isset($list)) {
+ // Not cached yet? We can take care of that.
+ $list = [];
+ if (isloggedin() && !isguestuser()) {
+ // We only track logged-in roles.
+ global $DB;
+ // Retrieve list of role names.
+ $rolenames = $DB->get_records('role');
+ // Retrieve list of my roles across all contexts.
+ $userroles = $DB->get_records('role_assignments', ['userid' => $userid]);
+ // For each of my roles, add the roll name to the list.
+ foreach ($userroles as $role) {
+ if (!empty($rolenames[$role->roleid]->shortname)) {
+ // There should always be a role name for each role id but you can't be too careful these days.
+ $list[] = $rolenames[$role->roleid]->shortname;
+ }
+ }
+ $list = array_unique($list);
+ if (is_siteadmin()) {
+ // Admin is not an actual role, but we can use our imagination for convenience.
+ $list[] = 'administrator';
+ }
+ }
+ }
+ return in_array(strtolower($roleshortname), $list);
+ }
+
+ /**
+ * Returns the URL of a blank Avatar as a square image.
+ *
+ * @param integer $size Width of desired image in pixels.
+ * @return MOODLE_URL URL to image of avatar image.
+ */
+ private function getblankavatarurl($size) {
+ global $PAGE, $CFG;
+ $img = 'u/' . ($size > 100 ? 'f3' : ($size > 35 ? 'f1' : 'f2'));
+ $renderer = $PAGE->get_renderer('core');
+ if ($CFG->branch >= 33) {
+ $url = $renderer->image_url($img);
+ } else {
+ $url = $renderer->pix_url($img); // Deprecated as of Moodle 3.3.
+ }
+ return (new \moodle_url($url))->out();
+ }
+
+ /**
+ * Retrieves the URL for the user's profile picture, if one is available.
+ *
+ * @param object $user The Moodle user object for which we want a photo.
+ * @param mixed $size Can be sm|md|lg or an integer 2|1|3 or an integer size in pixels > 3.
+ * @return string URL to the photo image file but with $1 for the size.
+ */
+ private function getprofilepictureurl($user, $size = 'md') {
+ global $PAGE;
+
+ $sizes = ['sm' => 35, '2' => 35, 'md' => 100, '1' => 100, 'lg' => 512, '3' => 512];
+ if (empty($px = $sizes[$size])) {
+ $px = $size; // Size was specified in pixels.
+ }
+ $userpicture = new user_picture($user);
+ $userpicture->size = $px; // Size in pixels.
+ $url = $userpicture->get_url($PAGE);
+ return $url;
+ }
+
+ /**
+ * Retrieves specified profile fields for a user.
+ *
+ * This method fetches and caches user profile fields from the database. If the fields have
+ * already been retrieved for the current request, the cached values are returned. This method
+ * supports fetching both core and custom user profile fields.
+ *
+ * @param object $user The user object whose profile fields are being retrieved.
+ * @param array $fields optional An array of field names to retrieve. If empty/not specified, only custom fields are retrieved.
+ * @return array An associative array of field names and their values for the specified user.
+ */
+ private function getuserprofilefields($user, $fields = []) {
+ global $DB;
+
+ static $profilefields;
+ static $lastfields;
+
+ // If we have already cached the profile fields and data, return them.
+ if (isset($profilefields) && $lastfields == $fields) {
+ return $profilefields;
+ }
+
+ $profilefields = [];
+ if (!isloggedin()) {
+ return $profilefields;
+ }
+
+ // Get custom user profile fields, their value and visibilit. Only works for authenticated users.
+ $sql = "SELECT f.shortname, f.visible, f.datatype, COALESCE(d.data, '') AS value
+ FROM {user_info_field} f
+ LEFT JOIN {user_info_data} d ON f.id = d.fieldid AND d.userid = :userid
+ ORDER BY f.shortname;";
+ $params = ['userid' => $user->id];
+ // Determine if restricted to only visible fields.
+ if (!empty(get_config('filter_filtercodes', 'ifprofilefiedonlyvisible'))) {
+ $params['visible'] = 1;
+ }
+ $profilefields = $DB->get_records_sql($sql, $params);
+
+ // Add core user profile fields.
+ foreach ($fields as $field) {
+ // Skip fields that don't exist (likely a typo).
+ if (isset($user->$field)) {
+ $profilefields[$field] = (object)['shortname' => $field, 'visible' => '1',
+ 'datatype' => 'text', 'value' => $user->$field];
+ }
+ }
+ $lastfields = $fields;
+
+ return $profilefields;
+ }
+
+ /**
+ * Retrieves the user's groupings for a course.
+ *
+ * @param integer $courseid The course ID.
+ * @param integer $userid The user ID.
+ * @return array An array of groupings for the specified user in the specified course.
+ */
+ private function getusergroupings($courseid, $userid) {
+ global $DB;
+
+ return $DB->get_records_sql('SELECT gp.id, gp.name, gp.idnumber
+ FROM {user} u
+ INNER JOIN {groups_members} gm ON u.id = gm.userid
+ INNER JOIN {groups} g ON g.id = gm.groupid
+ INNER JOIN {groupings_groups} gg ON gm.groupid = gg.groupid
+ INNER JOIN {groupings} gp ON gp.id = gg.groupingid
+ WHERE g.courseid = ? AND u.id = ?
+ GROUP BY gp.id
+ ORDER BY gp.name ASC', [$courseid, $userid]);
+ }
+
+ /**
+ * Determine if running on http or https. Same as Moodle's is_https() except that it is backwards compatible to Moodle 2.7.
+ *
+ * @return boolean true if protocol is https, false if http.
+ */
+ private function ishttps() {
+ global $CFG;
+ if ($CFG->branch >= 28) {
+ $ishttps = is_https(); // Available as of Moodle 2.8.
+ } else {
+ $ishttps = (filter_input(INPUT_SERVER, 'HTTPS') === 'on');
+ }
+ return $ishttps;
+ }
+
+ /**
+ * Determine if access is from a web service.
+ *
+ * @return boolean true if a web service, false if web browser.
+ */
+ private function iswebservice() {
+ global $ME;
+ // If this is a web service or the Moodle mobile app...
+ $isws = (WS_SERVER || (strstr($ME, "webservice/") !== false && optional_param('token', '', PARAM_ALPHANUM)));
+ return $isws;
+ }
+
+ /**
+ * Generates HTML code for a reCAPTCHA.
+ *
+ * @return string HTML Code for reCAPTCHA or blank if logged-in or Moodle reCAPTCHA is not configured.
+ */
+ private function getrecaptcha() {
+ global $CFG;
+ // Is user not logged-in or logged-in as guest?
+ if (!isloggedin() || isguestuser()) {
+ // If Moodle reCAPTCHA configured.
+ if (!empty($CFG->recaptchaprivatekey) && !empty($CFG->recaptchapublickey)) {
+ // Yes? Generate reCAPTCHA.
+ if (file_exists($CFG->libdir . '/recaptchalib_v2.php')) {
+ // For reCAPTCHA 2.0.
+ require_once($CFG->libdir . '/recaptchalib_v2.php');
+ return recaptcha_get_challenge_html(RECAPTCHA_API_URL, $CFG->recaptchapublickey);
+ } else {
+ // For reCAPTCHA 1.0.
+ require_once($CFG->libdir . '/recaptchalib.php');
+ return recaptcha_get_html($CFG->recaptchapublickey, null, $this->ishttps());
+ }
+ } else if ($CFG->debugdisplay == 1) { // If debugging is set to DEVELOPER...
+ // Show indicator that {reCAPTCHA} tag is not required.
+ return 'Warning: The reCAPTCHA tag is not required here.';
+ }
+ }
+ // Logged-in as non-guest user (reCAPTCHA is not required) or Moodle reCAPTCHA not configured.
+ // Don't generate reCAPTCHA.
+ return '';
+ }
+
+ /**
+ * Scrape HTML (callback)
+ *
+ * Extract content from another web page.
+ * Example: Can be used to extract a shared privacy policy across your websites.
+ *
+ * @param string $url URL address of content source.
+ * @param string $tag HTML tag that contains the information we want to retrieve.
+ * @param string $class (optional) HTML tag class attribute we should match.
+ * @param string $id (optional) HTML tag id attribute we should match.
+ * @param string $code (optional) any URL encoded HTML code you want to insert after the retrieved content.
+ * @return string Extracted content+optional code. If content is unavailable, returns message to contact webmaster.
+ */
+ private function scrapehtml($url, $tag = '', $class = '', $id = '', $code = '') {
+ // Retrieve content. If the URL fails, return a message.
+ $content = @file_get_contents($url);
+ if (empty($content)) {
+ return get_string('contentmissing', 'filter_filtercodes');
+ }
+
+ // Disable warnings.
+ $libxmlpreviousstate = libxml_use_internal_errors(true);
+
+ // Load content into DOM object.
+ $dom = new DOMDocument();
+ $dom->loadHTML($content);
+
+ // Clear suppressed warnings.
+ libxml_clear_errors();
+ libxml_use_internal_errors($libxmlpreviousstate);
+
+ // Scrape out the content we want. If not found, return everything.
+ $xpath = new DOMXPath($dom);
+
+ // If a tag was not specified.
+ if (empty($tag)) {
+ $tag .= '*'; // Match any tag.
+ }
+ $query = "//{$tag}";
+
+ // If a class was specified.
+ if (!empty($class)) {
+ $query .= "[@class=\"{$class}\"]";
+ }
+
+ // If an id was specified.
+ if (!empty($id)) {
+ $query .= "[@id=\"{$id}\"]";
+ }
+
+ $tag = $xpath->query($query);
+ $tag = $tag->item(0);
+
+ return $dom->saveXML($tag) . urldecode($code);
+ }
+
+ /**
+ * Convert a number of bytes (e.g. filesize) into human readable format.
+ *
+ * @param float $bytes Raw number of bytes.
+ * @return string Bytes in human readable format.
+ */
+ private function humanbytes($bytes) {
+ if ($bytes === false || $bytes < 0 || is_null($bytes) || $bytes > 1.0E+26) {
+ // If invalid number of bytes, or value is more than about 84,703.29 Yottabyte (YB), assume it is infinite.
+ $str = '∞'; // Could not determine, assume infinite.
+ } else {
+ static $unit;
+ if (!isset($unit)) {
+ $units = ['sizeb', 'sizekb', 'sizemb', 'sizegb', 'sizetb', 'sizeeb', 'sizezb', 'sizeyb'];
+ $units = get_strings($units, 'filter_filtercodes');
+ $units = array_values((array) $units);
+ }
+ $base = 1024;
+ $factor = min((int) log($bytes, $base), count($units) - 1);
+ $precision = [0, 2, 2, 1, 1, 1, 1, 0];
+ $str = sprintf("%1.{$precision[$factor]}f", $bytes / pow($base, $factor)) . ' ' . $units[$factor];
+ }
+ return $str;
+ }
+
+ /**
+ * Correctly format a list as "A, B and C".
+ *
+ * @param array $list An array of numbers or strings.
+ * @return string The formatted string.
+ */
+ private function formatlist($list) {
+ // Save and remove last item in list from array.
+ $last = array_pop($list);
+ if ($list) {
+ // Combine list using language list separator.
+ $list = implode(get_string('listsep', 'langconfig') . ' ', $list);
+ // Add last item separated by " and ".
+ $string = get_string('and', 'moodle', ['one' => $list, 'two' => $last]);
+ } else {
+ // Only one item in the list. No formatting required.
+ $string = $last;
+ }
+ return $string;
+ }
+
+ /**
+ * Convert string containg one or more attribute="value" pairs into an associative array.
+ *
+ * @param string $attrs One or more attribute="value" pairs.
+ * @return array Associative array of attributes and values.
+ */
+ private function attribstoarray($attrs) {
+ $arr = [];
+
+ if (preg_match_all('/\s*(?:([a-z0-9-]+)\s*=\s*"([^"]*)")|(?:\s+([a-z0-9-]+)(?=\s*|>|\s+[a..z0-9]+))/i', $attrs, $matches)) {
+ // For each attribute in the string, add associated value to the array.
+ for ($i = 0; $i < count($matches[0]); $i++) {
+ if ($matches[3][$i]) {
+ $arr[$matches[3][$i]] = null;
+ } else {
+ $arr[$matches[1][$i]] = $matches[2][$i];
+ }
+ }
+ }
+ return $arr;
+ }
+
+ /**
+ * Render cards for provided category.
+ *
+ * @param object $category Category object.
+ * @param boolean $categoryshowpic Set to true to display a category image. False displays no image.
+ * @return string HTML rendering of category cars.
+ */
+ private function rendercategorycard($category, $categoryshowpic) {
+ global $OUTPUT;
+
+ if (!$category->visible) {
+ $dimmed = 'opacity: 0.5;';
+ } else {
+ $dimmed = '';
+ }
+
+ $url = (new \moodle_url('/course/index.php', ['categoryid' => $category->id]))->out();
+ if ($categoryshowpic) {
+ $imgurl = $OUTPUT->get_generated_image_for_id($category->id + 65535);
+ $html = '
+
+
+ '
+ . $category->name . '
';
+ } else {
+ $html = '' .
+ '' . $category->name;
+ }
+ $html .= '' . PHP_EOL;
+ return $html;
+ }
+
+ /**
+ * Render course cards for list of course ids. Not visible for hidden courses or if it has expired.
+ *
+ * @param array $rcourseids Array of course ids.
+ * @param string $format orientation/layout of course cards.
+ * @return string HTML of course cars.
+ */
+ private function rendercoursecards($rcourseids, $format = 'vertical') {
+ global $CFG, $OUTPUT, $PAGE, $SITE;
+
+ $content = '';
+ $isadmin = (is_siteadmin() && !is_role_switched($PAGE->course->id));
+
+ foreach ($rcourseids as $courseid) {
+ if ($courseid == $SITE->id) { // Skip site.
+ continue;
+ }
+ $course = get_course($courseid);
+ $context = \context_course::instance($course->id);
+ // Course will be displayed if its visibility is set to Show AND (either has no end date OR a future end date).
+ $visible = ($course->visible && (empty($course->enddate) || time() < $course->enddate));
+ // Courses not visible will be still visible to site admins or users with viewhiddencourses capability.
+ if (!$visible && !($isadmin || has_capability('moodle/course:viewhiddencourses', $context))) {
+ // Skip if the course is not visible to user or course is the "site".
+ continue;
+ }
+
+ // Load image from course image. If none, generate a course image based on the course ID.
+ $context = \context_course::instance($courseid);
+ if ($course instanceof stdClass) {
+ $course = new \core_course_list_element($course);
+ }
+ $coursefiles = $course->get_course_overviewfiles();
+ $imgurl = '';
+ if ($CFG->branch >= 311) {
+ $imgurl = \core_course\external\course_summary_exporter::get_course_image($course);
+ } else { // Previous to Moodle 3.11.
+ foreach ($coursefiles as $file) {
+ if ($isimage = $file->is_valid_image()) {
+ // The file_encode_url() function is deprecated as per MDL-31071 but still in wide use.
+ $imgurl = file_encode_url("/pluginfile.php", '/' . $file->get_contextid() . '/'
+ . $file->get_component() . '/' . $file->get_filearea() . $file->get_filepath()
+ . $file->get_filename(), !$isimage);
+ $imgurl = (new \moodle_url($imgurl))->out();
+ break;
+ }
+ }
+ }
+ if (empty($imgurl)) {
+ $imgurl = $OUTPUT->get_generated_image_for_id($courseid);
+ }
+ $courseurl = (new \moodle_url('/course/view.php', ['id' => $courseid]))->out();
+
+ switch ($format) {
+ case 'vertical':
+ $content .= '
+
+ ';
+ break;
+ case 'horizontal':
+ global $DB;
+ $category = $DB->get_record('course_categories', ['id' => $course->category]);
+ $category = $category->name;
+
+ $summary = $course->summary == null ? '' : $course->summary;
+ $summary = substr($summary, -4) == '
' ? substr($summary, 0, strlen($summary) - 4) : $summary;
+
+ $content .= '
+
+
+
+
+
+
+ '
+ . get_string('category') . ': ' . $category .
+ '
+
+
+
'
+ . get_string('summary') . ': ' . $summary .
+ '
+
+
+
+
+ ';
+ break;
+ case 'table':
+ global $DB;
+ $category = $DB->get_record('course_categories', ['id' => $course->category]);
+ $category = $category->name;
+
+ $course->summary == null ? '' : $course->summary;
+ $summary = substr($summary, -4) == '
' ? substr($summary, 0, strlen($summary) - 4) : $summary;
+
+ $content .= '
+
+ ' . $course->get_formatted_name() . ' |
+ ' . $category . ' |
+ ' . $summary . ' |
+
+ ';
+ break;
+ }
+ }
+ return $content;
+ }
+
+ /**
+ * Get course card including format, header and footer.
+ *
+ * @param string $format card format.
+ * @return object $cards->format, $cards->header, $cards->footer
+ */
+ private function getcoursecardinfo($format = null) {
+ static $cards;
+ if (is_object($cards)) {
+ return $cards;
+ }
+ $cards = new stdClass();
+ if (empty($format)) {
+ $cards->format = get_config('filter_filtercodes', 'coursecardsformat');
+ } else {
+ $cards->format = $format;
+ }
+ switch ($cards->format) {
+ case 'table':
+ $cards->header = '
+
+
+
+ ' . get_string('course') . ' |
+ ' . get_string('category') . ' |
+ ' . get_string('description') . ' |
+
+
+
+ ';
+ $cards->footer = '
+
+
';
+ break;
+ case 'horizontal':
+ $cards->header = '';
+ break;
+ default:
+ $cards->format = 'vertical';
+ $cards->header = '';
+ $cards->footer = '
';
+ }
+ return $cards;
+ }
+
+ /**
+ * Generate a user link of a specified type if logged-in.
+ *
+ * @param string $clinktype Type of link to generate. Options include: email, message, profile, phone1.
+ * @param object $user A user object.
+ * @param string $name The name to be displayed.
+ *
+ * @return string Generated link.
+ */
+ private function userlink($clinktype, $user, $name) {
+ if (!isloggedin() || isguestuser()) {
+ $clinktype = ''; // No link, only name.
+ }
+ switch ($clinktype) {
+ case 'email':
+ $link = '' . $name . '';
+ break;
+ case 'message':
+ $link = '' . $name . '';
+ break;
+ case 'profile':
+ $link = '' . $name . '';
+ break;
+ case 'phone1':
+ if (!empty($user->phone1)) {
+ $link = '' . $name . '';
+ } else {
+ $link = $name;
+ }
+ break;
+ default:
+ $link = $name;
+ }
+ return $link;
+ }
+
+ /**
+ * Generate base64 encoded data img of QR Code.
+ *
+ * @param string $text Text to be encoded.
+ * @param string $label Label to display below QR code.
+ * @return string Base64 encoded data image.
+ */
+ private function qrcode($text, $label = '') {
+ if (empty($text)) {
+ return '';
+ }
+ global $CFG;
+ require_once($CFG->dirroot . '/filter/filtercodes/thirdparty/QrCode/src/QrCode.php');
+ $code = new QrCode();
+ $code->setText($text);
+ $code->setErrorCorrection('high');
+ $code->setPadding(0);
+ $code->setSize(480);
+ $code->setLabelFontSize(16);
+ $code->setLabel($label);
+ $src = 'data:image/png;base64,' . base64_encode($code->get('png'));
+ return $src;
+ }
+
+ /**
+ * Course completion progress percentage.
+ *
+ * @return int completion progress percentage
+ */
+ private function completionprogress() {
+ static $progresspercent;
+ if (!isset($progresspercent)) {
+ global $PAGE;
+ $course = $PAGE->course;
+ $progresspercent = -1; // Disabled: -1.
+ if (
+ $course->enablecompletion == 1
+ && isloggedin()
+ && !isguestuser()
+ && \context_system::instance() != 'page-site-index'
+ ) {
+ $progresspercent = (int) \core_completion\progress::get_course_progress_percentage($course);
+ }
+ }
+
+ return $progresspercent;
+ }
+
+ /**
+ * Generator Tags
+ *
+ * This function processes tags that generate content that could potentially include additional tags.
+ *
+ * @param string $text The unprocessed text. Passed by refernce.
+ * @return boolean True of there are more tags to be processed, otherwise false.
+ */
+ private function generatortags(&$text) {
+ global $CFG, $PAGE, $DB;
+
+ $replace = []; // Array of key/value filterobjects.
+
+ // If there are {menu...} tags.
+ if (stripos($text, '{menu') !== false) {
+ // Tag: {menuadmin}.
+ // Description: Displays a menu of useful links for site administrators when added to the custom menu.
+ // Parameters: None.
+ if (stripos($text, '{menuadmin}') !== false) {
+ $theme = $PAGE->theme->name;
+ $menu = '';
+ if ($this->hasminarchetype('editingteacher')) {
+ $menu .= '{fa fa-wrench} {getstring}admin{/getstring}' . PHP_EOL;
+ }
+ if ($this->hasminarchetype('coursecreator')) { // If a course creator or above.
+ $menu .= '-{getstring}administrationsite{/getstring}|/admin/search.php' . PHP_EOL;
+ $menu .= '-{toggleeditingmenu}' . PHP_EOL;
+ $menu .= '-Moodle Academy|https://moodle.academy/' . PHP_EOL;
+ $menu .= '-###' . PHP_EOL;
+ }
+ if ($this->hasminarchetype('manager')) { // If a manager or above.
+ $menu .= '-{getstring}user{/getstring}: {getstring:admin}usermanagement{/getstring}|/admin/user.php' . PHP_EOL;
+ $menu .= '{ifminsitemanager}' . PHP_EOL;
+ $menu .= '-{getstring}user{/getstring}: {getstring:mnet}profilefields{/getstring}|/user/profile/index.php' .
+ PHP_EOL;
+ $menu .= '-###' . PHP_EOL;
+ $menu .= '{/ifminsitemanager}' . PHP_EOL;
+ $menu .= '-{getstring}course{/getstring}: {getstring:admin}coursemgmt{/getstring}|/course/management.php' .
+ '?categoryid={categoryid}' . PHP_EOL;
+ $menu .= '-{getstring}course{/getstring}: {getstring}new{/getstring}|/course/edit.php' .
+ '?category={categoryid}&returnto=topcat' . PHP_EOL;
+ $menu .= '-{getstring}course{/getstring}: {getstring}searchcourses{/getstring}|/course/search.php' . PHP_EOL;
+ }
+ if ($this->hasminarchetype('editingteacher')) {
+ $menu .= '-{getstring}course{/getstring}: {getstring}restore{/getstring}|/backup/restorefile.php' .
+ '?contextid={coursecontextid}' . PHP_EOL;
+ $menu .= '{ifincourse}' . PHP_EOL;
+ $menu .= '-{getstring}course{/getstring}: {getstring}backup{/getstring}|/backup/backup.php?id={courseid}' .
+ PHP_EOL;
+ if (stripos($text, '{menucoursemore}') === false) {
+ $menu .= '-{getstring}course{/getstring}: {getstring}participants{/getstring}|/user/index.php?id={courseid}'
+ . PHP_EOL;
+ $menu .= '-{getstring}course{/getstring}: {getstring:badges}badges{/getstring}|/badges/view.php' .
+ '?type=2&id={courseid}' . PHP_EOL;
+ $menu .= '-{getstring}course{/getstring}: {getstring}reports{/getstring}|/course/admin.php' .
+ '?courseid={courseid}#linkcoursereports' . PHP_EOL;
+ }
+ $menu .= '-{getstring}course{/getstring}: {getstring:enrol}enrolmentinstances{/getstring}|/enrol/instances.php'
+ . '?id={courseid}' . PHP_EOL;
+ $menu .= '-{getstring}course{/getstring}: {getstring}reset{/getstring}|/course/reset.php?id={courseid}'
+ . PHP_EOL;
+ $menu .= '-Course: Layoutit|https://www.layoutit.com/build" target="popup" ' .
+ 'onclick="window.open(\'https://www.layoutit.com/build\',\'popup\',\'width=1340,height=700\');' .
+ ' return false;|Bootstrap Page Builder' . PHP_EOL;
+ $menu .= '{/ifincourse}' . PHP_EOL;
+ $menu .= '-###' . PHP_EOL;
+ }
+ if ($this->hasminarchetype('manager')) { // If a manager or above.
+ $menu .= '-{getstring}site{/getstring}: {getstring}reports{/getstring}|/admin/category.php?category=reports' .
+ PHP_EOL;
+ }
+ if (is_siteadmin() && !is_role_switched($PAGE->course->id)) { // If an administrator.
+ $menu .= '-{getstring}site{/getstring}: {getstring:admin}additionalhtml{/getstring}|/admin/settings.php' .
+ '?section=additionalhtml' . PHP_EOL;
+ $menu .= '-{getstring}site{/getstring}: {getstring:admin}frontpage{/getstring}|/admin/settings.php' .
+ '?section=frontpagesettings|Including site name' . PHP_EOL;
+ $menu .= '-{getstring}site{/getstring}: {getstring:admin}plugins{/getstring}|/admin/search.php#linkmodules' .
+ PHP_EOL;
+ $menu .= '-{getstring}site{/getstring}: {getstring:admin}supportcontact{/getstring}|/admin/settings.php' .
+ '?section=supportcontact' . PHP_EOL;
+
+ if ($CFG->branch >= 404) {
+ $label = 'themesettingsadvanced';
+ $section = 'themesettingsadvanced';
+ } else {
+ $label = 'themesettings';
+ $section = 'themesettings';
+ }
+ $menu .= '-{getstring}site{/getstring}: {getstring:admin}' . $label . '{/getstring}|/admin/settings.php' .
+ '?section=' . $section . '|Including custom menus, designer mode, theme in URL' . PHP_EOL;
+
+ if (file_exists($CFG->dirroot . '/theme/' . $theme . '/settings.php')) {
+ require_once($CFG->libdir . '/adminlib.php');
+ if (admin_get_root()->locate('theme_' . $theme)) {
+ // Settings use categories interface URL.
+ $url = '/admin/category.php?category=theme_' . $theme . PHP_EOL;
+ } else {
+ // Settings use tabs interface URL.
+ $url = '/admin/settings.php?section=themesetting' . $theme . PHP_EOL;
+ }
+ $menu .= '-{getstring}site{/getstring}: {getstring:admin}currenttheme{/getstring}|' . $url;
+ }
+ $menu .= '-{getstring}site{/getstring}: {getstring}notifications{/getstring} ({getstring}admin{/getstring})' .
+ '|/admin/index.php' . PHP_EOL;
+ }
+ $replace['/\{menuadmin\}/i'] = $menu;
+ }
+
+ // Tag: {menucoursemore}.
+ // Description: Show a "More" menu containing most of 4.x secondary menu. Useful if theme with pre-4.x style navigation.
+ // Parameters: None.
+ if (stripos($text, '{menucoursemore}') !== false) {
+ $menu = '';
+ $menu .= '{ifincourse}' . PHP_EOL;
+ if ($CFG->branch >= 400) {
+ $menu .= '{getstring}moremenu{/getstring}' . PHP_EOL;
+ } else {
+ $menu .= '{getstring:filter_filtercodes}moremenu{/getstring}' . PHP_EOL;
+ }
+ $menu .= '-{getstring}course{/getstring}|/course/view.php?id={courseid}' . PHP_EOL;
+ if ($this->hasminarchetype('editingteacher')) {
+ $menu .= '-{getstring}settings{/getstring}|/course/edit.php?id={courseid}' . PHP_EOL;
+ }
+ $menu .= '-{getstring}participants{/getstring}|/user/index.php?id={courseid}' . PHP_EOL;
+ $menu .= '-{getstring}grades{/getstring}|/grade/report/index.php?id={courseid}' . PHP_EOL;
+ if ($this->hasminarchetype('editingteacher')) {
+ $menu .= '-{getstring}reports{/getstring}|/report/view.php?courseid={courseid}' . PHP_EOL;
+ $menu .= '-###' . PHP_EOL;
+ $menu .= '-{getstring:question}questionbank{/getstring}|/question/edit.php?courseid={courseid}' . PHP_EOL;
+ if ($CFG->branch >= 39) {
+ $menu .= '-{getstring}contentbank{/getstring}|/contentbank/index.php?contextid={coursecontextid}' . PHP_EOL;
+ }
+ $menu .= '-{getstring:completion}coursecompletion{/getstring}|/course/completion.php?id={courseid}' . PHP_EOL;
+ $menu .= '-{getstring:badges}badges{/getstring}|/badges/view.php?type=2&id={courseid}' . PHP_EOL;
+ }
+ $pluginame = '{getstring:competency}competencies{/getstring}';
+ $menu .= '-' . $pluginame . '|/admin/tool/lp/coursecompetencies.php?courseid={courseid}' . PHP_EOL;
+ if ($this->hasminarchetype('editingteacher')) {
+ $menu .= '-{getstring:admin}filters{/getstring}|/filter/manage.php?contextid={coursecontextid}' . PHP_EOL;
+ }
+ if ($CFG->branch >= 402) {
+ $menu .= '-{getstring:enrol}unenrolme{/getstring}|{courseunenrolurl}' . PHP_EOL;
+ } else {
+ $menu .= '-{getstring:filter_filtercodes}unenrolme{/getstring}|{courseunenrolurl}' . PHP_EOL;
+ }
+ if ($this->hasminarchetype('editingteacher')) {
+ $menu .= '-{getstring:mod_lti}courseexternaltools{/getstring}|/mod/lti/coursetools.php?id={courseid}' . PHP_EOL;
+ if ($CFG->branch >= 311) {
+ $pluginame = '{getstring:tool_brickfield}pluginname{/getstring}';
+ $menu .= '-' . $pluginame . '|/admin/tool/brickfield/index.php?courseid={courseid}' . PHP_EOL;
+ }
+ $menu .= '-{getstring}coursereuse{/getstring}|/backup/import.php?id={courseid}' . PHP_EOL;
+ }
+ $menu .= '{/ifincourse}' . PHP_EOL;
+ $replace['/\{menucoursemore\}/i'] = $menu;
+ }
+
+ // Tag: {menudev}.
+ // Description: Displays a menu of useful links for site administrators when added to the custom menu.
+ // Parameters: None.
+ if (stripos($text, '{menudev}') !== false) {
+ $menu = '';
+ if (is_siteadmin() && !is_role_switched($PAGE->course->id)) { // If a site administrator.
+ $menu .= '-{getstring:tool_installaddon}installaddons{/getstring}|/admin/tool/installaddon' . PHP_EOL;
+ $menu .= '-###' . PHP_EOL;
+ $menu .= '-{getstring:admin}debugging{/getstring}|/admin/settings.php?section=debugging' . PHP_EOL;
+ $menu .= '-{getstring:admin}purgecachespage{/getstring}|/admin/purgecaches.php' . PHP_EOL;
+ $menu .= '-###' . PHP_EOL;
+ if (file_exists(dirname(__FILE__) . '/../../local/adminer/index.php')) {
+ $menu .= '-{getstring:local_adminer}pluginname{/getstring}|/local/adminer' . PHP_EOL;
+ }
+ if (file_exists(dirname(__FILE__) . '/../../local/codechecker/index.php')) {
+ $menu .= '-{getstring:local_codechecker}pluginname{/getstring}|/local/codechecker' . PHP_EOL;
+ }
+ if (file_exists(dirname(__FILE__) . '/../../local/moodlecheck/index.php')) {
+ $menu .= '-{getstring:local_moodlecheck}pluginname{/getstring}|/local/moodlecheck' . PHP_EOL;
+ }
+ if (file_exists(dirname(__FILE__) . '/../../admin/tool/pluginskel/index.php')) {
+ $menu .= '-{getstring:tool_pluginskel}pluginname{/getstring}|/admin/tool/pluginskel' . PHP_EOL;
+ }
+ if (file_exists(dirname(__FILE__) . '/../../local/tinyfilemanager/index.php')) {
+ $menu .= '-{getstring:local_tinyfilemanager}pluginname{/getstring}|/local/tinyfilemanager' . PHP_EOL;
+ }
+ $menu .= '-{getstring}phpinfo{/getstring}|/admin/phpinfo.php' . PHP_EOL;
+ $menu .= '-###' . PHP_EOL;
+ $menu .= '-{getstring:filter_filtercodes}pagebuilder{/getstring}|'
+ . '{getstring:filter_filtercodes}pagebuilderlink{/getstring}"'
+ . ' target="popup" onclick="window.open(\'{getstring:filter_filtercodes}pagebuilderlink{/getstring}\''
+ . ',\'popup\',\'width=1340,height=700\'); return false;' . PHP_EOL;
+ $menu .= '-{getstring:filter_filtercodes}photoeditor{/getstring}|'
+ . '{getstring:filter_filtercodes}photoeditorlink{/getstring}"'
+ . ' target="popup" onclick="window.open(\'{getstring:filter_filtercodes}photoeditorlink{/getstring}\''
+ . ',\'popup\',\'width=1340,height=700\'); return false;' . PHP_EOL;
+ $menu .= '-{getstring:filter_filtercodes}screenrec{/getstring}|'
+ . '{getstring:filter_filtercodes}screenreclink{/getstring}"'
+ . ' target="popup" onclick="window.open(\'{getstring:filter_filtercodes}screenreclink{/getstring}\''
+ . ',\'popup\',\'width=1340,height=700\'); return false;' . PHP_EOL;
+ $menu .= '-###' . PHP_EOL;
+ $menu .= '-Dev docs|https://moodle.org/development|Moodle.org ({getstring}english{/getstring})' . PHP_EOL;
+ $menu .= '-Dev forum|https://moodle.org/mod/forum/view.php?id=55|Moodle.org ({getstring}english{/getstring})' .
+ PHP_EOL;
+ $menu .= '-Tracker|https://tracker.moodle.org/|Moodle.org ({getstring}english{/getstring})' . PHP_EOL;
+ $menu .= '-AMOS|https://lang.moodle.org/|Moodle.org ({getstring}english{/getstring})' . PHP_EOL;
+ $menu .= '-WCAG 2.1|https://www.w3.org/WAI/WCAG21/quickref/|W3C ({getstring}english{/getstring})' . PHP_EOL;
+ $menu .= '-###' . PHP_EOL;
+ $menu .= '-DevTuts|https://www.youtube.com/watch?v=UY_pcs4HdDM|{getstring}english{/getstring}' . PHP_EOL;
+ $menu .= '-Moodle Development School|https://moodledev.moodle.school/|{getstring}english{/getstring}' . PHP_EOL;
+ $menuurl = 'https://moodle.academy/course/index.php?categoryid=4';
+ $menu .= '-Moodle Dev Academy|' . $menuurl . '|{getstring}english{/getstring}' . PHP_EOL;
+ }
+ $replace['/\{menudev\}/i'] = $menu;
+ }
+
+ // Tag: {menuthemes}.
+ // Description: Theme switcher for custom menu. Only for administrators. Not available after POST.
+ // Parameters: None.
+ // Allow Theme Changes on URL must be enabled for this to have any effect.
+ if (stripos($text, '{menuthemes}') !== false) {
+ $menu = '';
+ if (is_siteadmin() && empty($_POST)) { // If a site administrator.
+ if (get_config('core', 'allowthemechangeonurl')) {
+ $url = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http")
+ . "://{$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}";
+ $url .= (strpos($url, '?') ? '&' : '?');
+ $themeslist = \core_component::get_plugin_list('theme');
+ $menu = '';
+ foreach ($themeslist as $theme => $themedir) {
+ $themename = ucfirst(get_string('pluginname', 'theme_' . $theme));
+ $menu .= '-' . $themename . '|' . $url . 'theme=' . $theme . PHP_EOL;
+ }
+
+ // If an administrator, add links to Advanced Theme Settings and to Current theme settings.
+ if (is_siteadmin() && !is_role_switched($PAGE->course->id)) {
+ $theme = $PAGE->theme->name;
+ $menu = 'Themes' . PHP_EOL . $menu;
+ if ($CFG->branch >= 404) {
+ $label = 'themesettingsadvanced';
+ $section = 'themesettingsadvanced';
+ } else {
+ $label = 'themesettings';
+ $section = 'themesettings';
+ }
+
+ $menu .= '-###' . PHP_EOL;
+ $menu .= '-{getstring:admin}' . $label . '{/getstring}|/admin/settings.php' .
+ '?section=' . $section . '|Including custom menus, designer mode, theme in URL' . PHP_EOL;
+ if (file_exists($CFG->dirroot . '/theme/' . $theme . '/settings.php')) {
+ require_once($CFG->libdir . '/adminlib.php');
+ if (admin_get_root()->locate('theme_' . $theme)) {
+ // Settings using categories interface URL.
+ $url = '/admin/category.php?category=theme_' . $theme . PHP_EOL;
+ } else {
+ // Settings using tabs interface URL.
+ $url = '/admin/settings.php?section=themesetting' . $theme . PHP_EOL;
+ }
+ $menu .= '-{getstring:admin}currenttheme{/getstring}|' . $url;
+ }
+ }
+ }
+ }
+ $replace['/\{menuthemes\}/i'] = $menu;
+ }
+
+ // Tag: {menuwishlist}.
+ // Description: Displays a list of wishlisted courses for the Primary (Custom) menu with an option to add or remove
+ // the current course from the wishlist. The list will be sorted alphabetically. If there are no courses in the
+ // wishlist or we are on a site page, a message will be displayed.
+ // Parameters: None.
+ if (stripos($text, '{menuwishlist}') !== false) {
+ // If not logged in, or guest user, do not display the Wishlist.
+ if (!isloggedin() || isguestuser()) {
+ $menu = '';
+ } else {
+ global $USER, $DB, $PAGE;
+
+ // Get the user's wishlist from the user_preference table.
+ $wishlist = $DB->get_record('user_preferences', [
+ 'userid' => $USER->id,
+ 'name' => 'filter_filtercodes_wishlist',
+ ]);
+ $wishlist = $wishlist ? explode(',', $wishlist->value) : [];
+
+ // Generate the list of wishlisted courses.
+ $menu = '';
+ foreach ($wishlist as $courseid) {
+ $course = $DB->get_record('course', ['id' => $courseid]);
+ if ($course) {
+ $courseurl = (new \moodle_url('/course/view.php', ['id' => $course->id]))->out();
+ $menu .= '-' . format_string($course->fullname) . '|' . $courseurl . "\n";
+ }
+ }
+ if (!empty($menu)) {
+ // Sort course names.
+ $menu = explode("\n", $menu);
+ $menu = array_filter($menu, 'strlen');
+ usort($menu, 'strnatcasecmp');
+ $menu = trim(implode("\n", $menu));
+ }
+
+ // Check if the current course is in the wishlist.
+ if ($PAGE->course->id === SITEID) {
+ if (empty($menu)) {
+ $menu = '-' . get_string('wishlist_nocourses', 'filter_filtercodes') . "\n";
+ }
+ } else {
+ if (!empty($menu)) {
+ $menu .= "\n-###\n";
+ }
+ $action = in_array($PAGE->course->id, $wishlist) ? 'remove' : 'add';
+ $url = (new \moodle_url('/filter/filtercodes/wishlist.php', [
+ 'courseid' => $PAGE->course->id,
+ 'action' => $action,
+ ]))->out();
+ $menu .= '-' . get_string('wishlist_' . $action, 'filter_filtercodes') . '|' . $url . "\n";
+ }
+ $menu = get_string('wishlist', 'filter_filtercodes') . "\n" . $menu;
+ }
+
+ // Replace the {menuwishlist} tag with the generated wishlist output, if any.
+ $replace['/\{menuwishlist\}/i'] = $menu;
+ }
+ }
+
+ // Check if any {course*} or %7Bcourse*%7D tags. Note: There is another course tags section further down.
+ $coursetagsexist = (stripos($text, '{course') !== false || stripos($text, '%7Bcourse') !== false);
+ if ($coursetagsexist) {
+ // Tag: {coursesummary} or {coursesummary courseid}.
+ // Description: Course summary as defined in the course settings.
+ // Optional parameters: Course id. Default is to use the current course, or site summary if not in a course.
+ if (stripos($text, '{coursesummary') !== false) {
+ if (stripos($text, '{coursesummary}') !== false) {
+ // No course ID specified.
+ $coursecontext = \context_course::instance($PAGE->course->id);
+ $PAGE->course->summary == null ? '' : $PAGE->course->summary;
+ $replace['/\{coursesummary\}/i'] = format_text(
+ $PAGE->course->summary,
+ FORMAT_HTML,
+ ['context' => $coursecontext]
+ );
+ }
+ if (stripos($text, '{coursesummary ') !== false) {
+ // Course ID was specified.
+ preg_match_all('/\{coursesummary ([0-9]+)\}/', $text, $matches);
+ // Eliminate course IDs.
+ $courseids = array_unique($matches[1]);
+ $coursecontext = \context_course::instance($PAGE->course->id);
+ foreach ($courseids as $id) {
+ $course = $DB->get_record('course', ['id' => $id]);
+ if (!empty($course)) {
+ $course->summary == null ? '' : $course->summary;
+ $replace['/\{coursesummary ' . $course->id . '\}/isuU'] = format_text(
+ $course->summary,
+ FORMAT_HTML,
+ ['context' => $coursecontext]
+ );
+ }
+ }
+ unset($matches, $course, $courseids, $id);
+ }
+ }
+ }
+
+ // Tag: {formquickquestion}
+ // Tag: {formcheckin}
+ // Tag: {formcontactus}
+ // Tag: {formcourserequest}
+ // Tag: {formsupport}
+ // Tag: {formsesskey}
+ //
+ // Description: Tags used to generate pre-define forms for use with ContactForm plugin.
+ // Parameters: None.
+ if (stripos($text, '{form') !== false) {
+ $pre = '