diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f3b20c..62221c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,20 +1,40 @@ # Change Log + All notable changes to this project will be documented in this file. -## [2.7.0] 2025-07-10 (dev) -### Updated -- Fixed PHP issue with {firstnamephonetic}, {lastnamephonetic}, {middlename} tags when blank. +## [2.7.2] 2025-10-27 (dev) + +## Added + +- Now detects encoded course tags (e.g. %7Bcoursecontextid%7D) +- Added option to link to Mobile Phone in {coursecontact} tag. +- Added support for nested {if...} tags. +- {coursecount students:active} Added active enrolments fallback if no role_assignments present. + +## Updated + +- Fix 342: {coursecontact} Phone link will now be displayed when configured in the settings. +- Fixed logic of {ifcourserequest} tags. +- Fix a Moodle coding guidelines compliance issue. +- Improved 3rd party plugin detection for {menudev} tag. +- Fixed PHP issue with blank {firstnamephonetic}, {lastnamephonetic}, {middlename} tags. - Fixed URL to Moodle reports. +- Compatible with Moodle 2.7 to 5.1 +- Compatible with PHP 5.4 to 8.4 ## [2.7.0] 2025-05-05 + ### Added + - New {ifnotingroup} tag (without parameters). - Primary/Custom menu text, such as course or category names, can now contain a pipe (|) character. - New {firstnamephonetic} tag. - New {lastnamephonetic} tag. - New {middlename} tag. - New Add New User and Upload Users to {menuadmin} tag. + ### Updated + - Fix-323: Escape arguments used to construct link from button code. - Fix-210: {if*rolename*} tags now work correctly. - Fix-319: Moodle 4.5 and 5.0 Plugin CI runs. @@ -25,7 +45,9 @@ All notable changes to this project will be documented in this file. - Fixed several issues relating to Multi-language text. ## [2.6.3] 2025-04-27 + ### Updated + - Fixed warning if the {ifprofile_field_shortname} field does not exist. - The {sitesummary} tag now displays the site summary instead of the site full name. - Managers can no longer access the Themes menu. @@ -42,12 +64,16 @@ All notable changes to this project will be documented in this file. - Copyright notice to include 2025. ## [2.6.1] 2024-11-20 + ### Update + - Fix-311: Global tags can now include numbers in their name. - Fix-308: Fixed compatibility issue with PHP 8.3. ## [2.6.0] 2024-10-07 + ### Added + - New {menulanguages} tag. - New {keyboard}...{/keyboard} tag. - New {menuwishlist} tag. @@ -63,7 +89,9 @@ All notable changes to this project will be documented in this file. - New {ifnotgrouping groupingid}...{/ifnotgrouping} tag. - New {mygroupings} tag. - New {ifnotincohort} tag. + ### Updated + - Fixed bug with ifactivitycompleted and ifnotactivitycompleted if activity does not exist. - Fixed issue with %7Bcoursemoduleid%7D leaving % symbol behind. - Fixed issue when {coursesummary} is used in a block. @@ -80,12 +108,16 @@ All notable changes to this project will be documented in this file. - Fixed compatibility issue with Moodle LMS 4.5. ## [2.5.1] 2024-05-01 + ### Updated + - {iftheme} tag now works even when at the beginning of a string. - Updated for Moodle coding guidelines. ## [2.5.0] 2024-04-24 + ### Added + - New {menucoursemore} tag. - New {iftheme}{/iftheme} tag. - New {ifnottheme}{/ifnottheme} tag. @@ -93,7 +125,9 @@ All notable changes to this project will be documented in this file. - New links to edit Advanced theme settings and current theme settings to {menuthemes} tag. - {ALPHA} New {dashboard_siteinfo} tag. Work in progress - doesn't display correctly in all themes. - GitHub actions workflow. + ### Updated + - Small performance optimization. - Corrected "Course: Badges" link in {menuadmin}. - Updated PHP and Moodle compatibility in CONTRIBUTING.md. @@ -111,12 +145,16 @@ All notable changes to this project will be documented in this file. - Fixed compatibility issue with Moodle LMS 4.4. ## [2.4.3] 2023-11-20 + ### Added + - New {menuthemes} tag. - New {sitename} tag. - New {sitesummary} tag. - New {ifminstudent} tag. + ### Updated + - {courseenddate} tag can now take an optional courseid parameter. - {courseenddate} tag will now display strftime date formats. - If support page is blank, the {supportpage} tag will be blank instead of displaying the tag. @@ -130,20 +168,28 @@ All notable changes to this project will be documented in this file. - Fixed issue where a tel: link was unexpectedly created in {teamcards}. ## [2.4.2] 2023-10-25 + ### Updated + - Fixed bug with rendering of coursecards in Moodle 3.10 and earlier. ## [2.4.1] 2023-10-23 + ### Added + - New %7Bwwwroot%7D - alias for the {wwwroot} tag. + ### Updated + - Fixed: {coursecard}, {coursecards}, {mycoursescards} and {coursecardsbyenrol} now include visible courses without an end date. - Fixed a failed PHPUnit test for {coursemoduleid}. - Fix spacing for some failed CSS code checks. - Tested compatible with PHP 8.2. ## [2.4.0] 2023-10-20 + ### Added + - Support for FontAwesome v6 syntax including fa-solid and fa-brands. E.g. {fa-solid fa-user}. - Fix-266: New {multilang}{/multilang} tag. Note: Depends on Moodle's `Multi-language content` filter. - Fix-198: Module level assigned roles detection to {ifcustomrole} and {ifnotcustomrole} tags. @@ -163,7 +209,9 @@ All notable changes to this project will be documented in this file. - Alternative (alt) text to {qrcode} tag. - Documented tags in the source code. - Compatibility with Moodle 4.3. + ### Updated + - The {button} tag will now attempt to automatically strip HTML tags created by some other filters. - Fixed profile pictures including user picture, gravatar and faceless avatar. - {scrape} tag now automatically removes any HTML in case Moodle turned the URL into a link. @@ -191,20 +239,26 @@ All notable changes to this project will be documented in this file. - Documentation (README.md) ## [2.3.6] 2023-05-07 + ### Updated + - Partial fix for sizing issue of radial and pie charts in Moodle 4.1 and 4.2. - Copyright notice to include 2023. - Compatibility with Moodle 4.2. - Compatibility with PHP 8.1. ## [2.3.5] 2023-01-31 + ### Added + - New {ifenrolpage}{/ifenrolpage} tags. - New {ifnotenrolpage}{/ifnotenrolpage} tags. - {courseid} tag now resolves to course id on enrolment pages. ## [2.3.4] 2022-12-11 + ### Added + - New {courseunenrolurl} tag. - New {coursecount students} tag. - Setting to show hidden profile fields using the {profile_field_...} tag. @@ -216,7 +270,9 @@ All notable changes to this project will be documented in this file. - Code of Conduct guidelines. - Compatibility with Moodle 4.1. - Compatibility with PHP 8.0. + ### Updated + - Fix-218: You can now use the {profile_field_...} tag inside the {chart} tag. - Fix-244: Blank avatars now appear in {coursecards} regardless of whether Gravatars are enabled. - Fix-217: You can now have up to 50 global tags. @@ -227,15 +283,21 @@ All notable changes to this project will be documented in this file. - Tested to be compatible with PHP 7.4 and 8.0. ## [2.3.1] 2022-06-07 + ### Added + - phpcs.xml.cont.dist file. + ### Updated + - .gitignore file. - Fix-221: Resolved conflict between {mygroups} and {ifingroup} tags when used at the same time. - Fix-222: Fixed PHPUnit v9.5 compatibility. ## [2.3.0] 2022-04-19 + ### Added + - New {ifhasarolename roleshortname}{/ifhasarolename} tags. - Sample ALPHA code in the documentation to patch Moodle 4.0 themes for support in the custom menu. - New {courseprogresspercent} tag. @@ -278,7 +340,9 @@ All notable changes to this project will be documented in this file. - Missing support for {supportname}, {supportemail} and {supportpage} tags. - Known limitation in README.md regarding Moodle's 'Download course content' feature. - Compatibility with Moodle 4.0 + ### Updated + - Improve parsing of {scrape} tag. Improper syntax will no longer make Moodle crash, the tag just won't work properly. - {lang}, {idnumber} and {coursegradepercent} tags can now be used within other tags for example. - {coursesummary} tag is now processed through Moodle filters for multi-language support. @@ -305,14 +369,18 @@ All notable changes to this project will be documented in this file. - Updated copyright notice to include 2022. ## [2.2.1] 2021-05-25 + ### Updated + - Corrections in some of the language strings. - Completed French translation. - Clarification for {coursecards} and {categorycards} documentation in this README.md file. - There is no new or changed functionality in this release. ## [2.2.0] 2021-05-22 + ### Added + - New {courseteachers} tag (ALPHA). - New %7Bcoursemoduleid%7D tag. - New define custom global {global_...} tags (up to 20). @@ -327,7 +395,9 @@ All notable changes to this project will be documented in this file. - New {supportemail} tag. - New {supportpage} tag. - New {webpage} gets automatically substituted to {profile_field_webpage} as of Moodle 3.11. + ### Updated + - {coursesummary} can now include other FilterCodes. - {categorycards} titles now always display white. - Request a Course link is no longer included in {mycourses}. See new {courserequest} tag. @@ -343,7 +413,9 @@ All notable changes to this project will be documented in this file. - Copyright notice for 2021. ## [2.1.0] 2020-11-23 + ### Added + - New {ifingroup id|idnumber}{/ifingroup} tags. - New {filtercodes} tag. Note: Only works for teachers and above. - New {alert style}{/alert} tags (ALPHA). @@ -366,6 +438,7 @@ All notable changes to this project will be documented in this file. - New option to format the date/time {now dateTimeFormat}. ### Updated + - {courseprogress} and {courseprogressbar} now show zero progress if progress is 0. - {alert} to allow for optional contextual class stying. - Reorganized and grouped list of tags and made some corrections in the documentation. @@ -379,7 +452,9 @@ All notable changes to this project will be documented in this file. - Tested to be compatible up to and including Moodle 3.10. ## [2.0.0] 2020-07-01 + ### Added + - New configurable setting to enable/disable escaped [{braces}] (e.g. for creating documentation). Default is enabled. - You can now escape tags so they are not processed by wrapping them in [{brackets}]. {{double-braces}} are no longer supported. - New {diskfreespacedata} tag. @@ -417,7 +492,9 @@ All notable changes to this project will be documented in this file. - composer.json - Separator in menu above Request a Course link (part of {mycoursesmenu} tag). - New question to FAQ regarding setting filter priorities so that all enabled filters works together. + ### Updated + - Tested to be compatible with PHP 7.3 and 7.4. - Tested to be compatible with Moodle 3.9. - Read-only name and email address fields are now also disabled in {form...} templates. @@ -429,7 +506,9 @@ All notable changes to this project will be documented in this file. - .travis.yml and fixed issues. - Fixed example of Create Course menu item. Now creates a course in the current category. - Fixed {note} tag which was not working. + ### Deprecated (no longer inluded) + - You can no longer escape tags using {{double}} braces. This was causing issues with MathJAX. Bracket your [{tag}] instead. ### Important notes @@ -448,7 +527,9 @@ They were found to be incompatible with the following Moodle themes: * Boost_Training ## [1.1.0] - 2019-11-17 + ### Added + - You can now escape tags so they are not processed by using a double set of braces {{ and }} around tags. - If Request a Course is enabled, it will now be appended in {mycourses} and {mycoursesmenu}. - New {wwwcontactform} tag. @@ -471,7 +552,9 @@ They were found to be incompatible with the following Moodle themes: - New {referrer} tag - alias of {referer} previously implemented. - Missing $string['pluginname'] to language file. - Added some unit tests. + ### Updated + - Fixed some unit tests. - Fix for {scrape} tag to better handle missing parameters. - Fixed {langx} tag so that it works correctly with language and culture codes. @@ -480,7 +563,9 @@ They were found to be incompatible with the following Moodle themes: - Documentation to reflect new functionality. ## [1.0.1] - 2019-05-20 + ### Added + - New {pagepath} tag. - New {editingtoggle} tag. - New {idnumber} tag (from user profile). @@ -490,11 +575,15 @@ They were found to be incompatible with the following Moodle themes: - New {details}, {summary}, {/summary}, {/details} tags (experimental). - New .travis.yml configuration file for Travis. - Expanded compatibility - now includes Moodle 2.7, 2.8, 2.9, 3.0, 3.1, 3,2, 3.3, 3.4, 3.5, 3.6 and now 3.7. + ### Updated + - Fixed {categories} filter code compatibility with Moodle 2.7 to 3.5. ## [1.0.0] - 2018-11-26 + ### Added + - New settings page. - New {getstring} tag. - New {siteyear} tag - current 4 digit year - useful for copyright notices. @@ -509,19 +598,27 @@ They were found to be incompatible with the following Moodle themes: - New {usersonline} tag. - New experimental support for Moodle Custom Menu filtering in Boost and Clean (bootstrapbase) themes. Must be enabled in FilterCodes settings and requires Moodle 3.2+. - Expanded compatibility - now includes Moodle 2.7, 2.8, 2.9, 3.0, 3.1, 3,2, 3.3, 3.4, 3.5 and now 3.6. + ### Updated + - No major issues in the last 12 months of BETA - Project status is now STABLE. ## [0.4.6] - 2018-05-22 + ### Added + - Added support for Privacy API. ## [0.4.5] - 2018-05-18 + ### Added + - New %7Bsesskey%7D tag as an alternative to {sesskey} for use with encoded URLs. ## [0.4.4] - 2018-05-08 + ### Added + - New %7Bcourseid%7D tag as an alternative to {courseid} for use with encoded URLs. - New %7Buserid%7D tag as an alternative to {userid} for use with encoded URLs. - New {coursestartdate} tag. @@ -529,25 +626,36 @@ They were found to be incompatible with the following Moodle themes: - New {coursecompletiondate} tag. ## [0.4.3] - 2018-03-30 + ### Added + - Support for reCAPTCHA v2 in Moodle 3.1.11+, 3.2.8+, 3.3.5+, 3.4.5+ and 3.5+. - FilterCodes upgrade notifications now works properly when a updates are available on Moodle.org. - Expanded compatibility - now includes Moodle 2.7, 2.8, 2.9, 3.0, 3.1, 3,2, 3.3, 3.4 and 3.5. + ### Updated + - Documentation - fixed errors and added FAQ for reCAPTCHA. - Copyright notice to include 2018. - Minor performance optimization. ## [0.4.2] - 2017-11-17 + ### Added + - Example of enabling filters in custom menu and custom user menu in boost based themes. + ### Updated + - ReCAPTCHA will now work on https. - Fixed example of enabling filters in custom menu and custom user menu in bootstrapbase based themes. ## [0.4.0] - 2017-11-11 + ### Added + Over a dozen new FilterCodes added including: + - New {alternatename} tag. - New {city} tag. - New {categories} tag. @@ -565,7 +673,9 @@ Over a dozen new FilterCodes added including: - Expanded compatibility now includes Moodle 2.7, 2.8, 2.9, 3.0, 3.1, 3,2, 3.3 and 3.4. - Added new useful examples of using FilterCodes in custom menus (see Usage section). - Added CONTRIBUTE.md. + ### Updated + - Project status is now BETA. - Reorganized README.md (New: logo, status badges, table of contents, contributing, etc). - Default Moodle role IDs are no longer hard coded. {ifrolename} and {ifminrolename} type tags now use role archetypes instead of role shortnames. (thanks @FMCorz !) @@ -577,13 +687,17 @@ Over a dozen new FilterCodes added including: - Updated documentation and FAQ. ## [0.3.0] - 2017-09-08 + ### Added + - Conditional role tags are now aware of switching roles. - New {ifminassistant}{/ifminassistant} set of tags. - New {ifminteacher}{/ifminteacher} set of tags. - New {ifmincreator}{/ifmincreator} set of tags. - New {ifminmanager}{/ifminmanager} set of tags. + ### Updated + - {ifrolename} type tags will now only display content if you have been assigned that particular role. - Identification of roles no longer depends on the verification of unique capabilities but by role assignment. - Bug fix: {ifstudent}{/ifstudent} set of tags now work. (thanks @gemguardian !) @@ -591,13 +705,19 @@ Over a dozen new FilterCodes added including: - Updated documentation and FAQ. ## [0.2.0] - 2017-07-18 + ### Added + - New tag: {ifnotenrolled} - Exact logical opposite of {ifenrolled} tag. + ### Updated + - Significant performance improvements. - Language strings are now correctly named. ## [0.1.0] - 2017-07-07 + ### Added + - Initial public release on Moodle.org and GitHub. - Plugin officially compatible and tested with Moodle 3.1, 3.2 and 3.3. diff --git a/README.md b/README.md index a832361..38fd3c4 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,9 @@ FilterCodes filter plugin for Moodle ==================================== -![PHP](https://img.shields.io/badge/PHP-v5.6%20to%20v8.3-blue.svg) -![Moodle](https://img.shields.io/badge/Moodle-v2.7%20to%20v5.0-orange.svg) + +![PHP](https://img.shields.io/badge/PHP-v5.4%20to%20v8.4-blue.svg) +![Moodle](https://img.shields.io/badge/Moodle-v2.7%20to%20v5.1-orange.svg) [![GitHub Issues](https://img.shields.io/github/issues/michael-milette/moodle-filter_filtercodes.svg)](https://github.com/michael-milette/moodle-filter_filtercodes/issues) [![Contributions welcome](https://img.shields.io/badge/contributions-welcome-green.svg)](#contributing) [![License](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](#license) @@ -46,7 +47,7 @@ FilterCodes filter plugin for Moodle - [Enabling FilterCodes in the Custom Menu / Primary Navigation](#enabling-filtercodes-in-the-custom-menu--primary-navigation) - [Technique A: Patching Moodle core](#technique-a-patching-moodle-core) - [Technique B: Patching your Moodle theme](#technique-b-patching-your-moodle-theme) - - [For themes based on **boost** (Moodle 5.0)](#for-themes-based-on-boost-moodle-50) + - [For themes based on **boost** (Moodle 5.0+)](#for-themes-based-on-boost-moodle-50) - [For themes based on **boost** (Moodle 4.0 to 4.5)](#for-themes-based-on-boost-moodle-40-to-45) - [For themes based on **boost** (Moodle 3.2 to 3.11 and some 4.0+ to 4.6 themes)](#for-themes-based-on-boost-moodle-32-to-311-and-some-40-to-46-themes) - [For themes based on the older **bootstrapbase** (Moodle 2.7 to 3.6)](#for-themes-based-on-the-older-bootstrapbase-moodle-27-to-36) @@ -62,38 +63,40 @@ FilterCodes filter plugin for Moodle - [Show contact's profile description](#show-contacts-profile-description) - [Show hidden profile fields](#show-hidden-profile-fields) - [Contact link type](#contact-link-type) - - [Show {categorycards} background](#show-categorycards-background) + - [Show background](#show-background) - [Global custom tags](#global-custom-tags) - - [Customizing or translating the forms generated by the {form...} tags](#customizing-or-translating-the-forms-generated-by-the-form-tags) + - [Customizing or translating the forms generated by the tags](#customizing-or-translating-the-forms-generated-by-the-tags) - [Updating](#updating) - [Uninstallation](#uninstallation) - [Limitations](#limitations) - [Language Support](#language-support) - [Troubleshooting](#troubleshooting) + - [Known Compatibility Issues](#known-compatibility-issues) + - [Third-Party Plugin Compatibility](#third-party-plugin-compatibility) - [FAQ](#faq) - [Answers to Frequently Asked Questions](#answers-to-frequently-asked-questions) - [Can I combine/nest conditional tags?](#can-i-combinenest-conditional-tags) - - [How can {ifactivitycompleted} work for the completion of a combination of multiple activities?](#how-can-ifactivitycompleted-work-for-the-completion-of-a-combination-of-multiple-activities) + - [How can work for the completion of a combination of multiple activities?](#how-can-work-for-the-completion-of-a-combination-of-multiple-activities) - [I am using FilterCodes on a multi-language site. Some of my non-FilterCode tags are not being processed. How can I fix this?](#i-am-using-filtercodes-on-a-multi-language-site-some-of-my-non-filtercode-tags-are-not-being-processed-how-can-i-fix-this) - [How can I use this to pre-populate one or more fields in a Contact Form for Moodle?](#how-can-i-use-this-to-pre-populate-one-or-more-fields-in-a-contact-form-for-moodle) - - [Why do administrators see the text of all other roles when using {ifminxxxx}Content{/ifminxxxx} tags?](#why-do-administrators-see-the-text-of-all-other-roles-when-using-ifminxxxxcontentifminxxxx-tags) + - [Why do administrators see the text of all other roles when usingContent tags?](#why-do-administrators-see-the-text-of-all-other-roles-when-usingcontent-tags) - [Is there a tag to display...?](#is-there-a-tag-to-display) - - [Why does the {button} tag not work?](#why-does-the-button-tag-not-work) - - [How can I style the {coursecontacts} tag?](#how-can-i-style-the-coursecontacts-tag) + - [Why does the tag not work?](#why-does-the-tag-not-work) + - [How can I style the tag?](#how-can-i-style-the-tag) - [Do you have examples/samples of how tags work in my version of FilterCodes?](#do-you-have-examplessamples-of-how-tags-work-in-my-version-of-filtercodes) - [When a user is logged out, the First name, Surname, Full Name, Email address and Username are empty. How can I set default values for these tags?](#when-a-user-is-logged-out-the-first-name-surname-full-name-email-address-and-username-are-empty-how-can-i-set-default-values-for-these-tags) - - [I added the "{mycoursesmenu}" to my custom menu. How can I hide it if the user is not logged in?](#i-added-the-mycoursesmenu-to-my-custom-menu-how-can-i-hide-it-if-the-user-is-not-logged-in) + - [I added the "" to my custom menu. How can I hide it if the user is not logged in?](#i-added-the--to-my-custom-menu-how-can-i-hide-it-if-the-user-is-not-logged-in) - [How can I add a "Logout" link in my custom menu?](#how-can-i-add-a-logout-link-in-my-custom-menu) - [How can I create a menu that is just for administrators or some other roles?](#how-can-i-create-a-menu-that-is-just-for-administrators-or-some-other-roles) - [Why is the IP Address listed as 0:0:0:0:0:0:0:1?](#why-is-the-ip-address-listed-as-00000001) - [Why does it show me as enrolled on the frontpage?](#why-does-it-show-me-as-enrolled-on-the-frontpage) - - [I added the {recaptcha} tag in my webform. Why doesn't the reCAPTCHA show up?](#i-added-the-recaptcha-tag-in-my-webform-why-doesnt-the-recaptcha-show-up) - - [How can I get the {scrape} tag to work?](#how-can-i-get-the-scrape-tag-to-work) + - [I added the tag in my webform. Why doesn't the reCAPTCHA show up?](#i-added-the-tag-in-my-webform-why-doesnt-the-recaptcha-show-up) + - [How can I get the tag to work?](#how-can-i-get-the-tag-to-work) - [How can I scrape content from more than one web page or more than one website?](#how-can-i-scrape-content-from-more-than-one-web-page-or-more-than-one-website) - [How can I scrape content based on a pattern of HTML tags instead of just one HTML tag with a class or id? Example, an h1 tag inside the div class="content" tag.](#how-can-i-scrape-content-based-on-a-pattern-of-html-tags-instead-of-just-one-html-tag-with-a-class-or-id-example-an-h1-tag-inside-the-div-classcontent-tag) - - [How can I get the {getstring} tag to work? It doesn't seem to be replaced with the correct text.](#how-can-i-get-the-getstring-tag-to-work-it-doesnt-seem-to-be-replaced-with-the-correct-text) - - [How can I customize or translate the forms generated by the {form...} tags?](#how-can-i-customize-or-translate-the-forms-generated-by-the-form-tags) - - [Is there more information about the {ifprofile shortname ...} tag?](#is-there-more-information-about-the-ifprofile-shortname--tag) + - [How can I get the tag to work? It doesn't seem to be replaced with the correct text.](#how-can-i-get-the-tag-to-work-it-doesnt-seem-to-be-replaced-with-the-correct-text) + - [How can I customize or translate the forms generated by the tags?](#how-can-i-customize-or-translate-the-forms-generated-by-the-tags) + - [Is there more information about the tag?](#is-there-more-information-about-the-tag) - [What are the supported dateTimeFormat formats?](#what-are-the-supported-datetimeformat-formats) - [Are there any security considerations?](#are-there-any-security-considerations) - [How can I get answers to other questions?](#how-can-i-get-answers-to-other-questions) @@ -104,7 +107,6 @@ FilterCodes filter plugin for Moodle - [Further Information](#further-information) - [License](#license) - # Basic Overview FilterCodes filter for Moodle enables content creators to easily customize and personalize Moodle sites and course content using over 135 plain text tags that can be used **almost** anywhere in Moodle. Support may also vary depending on the theme used. @@ -188,7 +190,7 @@ FilterCodes are meant to be entered as regular text in the Moodle WYSIWYG editor * {institution} : Display the name of the institution from the user's profile. * {department} : Display the name of the department from the user's profile. * {userpictureurl X} : Display the user's profile picture URL. X indicates the size and can be **sm** (small), **md** (medium) or **lg** (large). If the user does not have a profile picture or is logged out, the default faceless profile photo URL will be shown instead. -* {userpictureimg X} : Generates an html tag containing the user's profile picture. X indicates the size and can be **sm** (small), **md** (medium) or **lg** (large). If the user does not have a profile picture or is logged out, the default faceless profile photo will be used instead. +* {userpictureimg X} : Generates an `` html tag containing the user's profile picture. X indicates the size and can be **sm** (small), **md** (medium) or **lg** (large). If the user does not have a profile picture or is logged out, the default faceless profile photo will be used instead. * {profile_field_shortname} : Display's custom user profile field. Replace "shortname" with the shortname of a custom user profile field all in lowercase. NOTE: It will not display if the custom user profile field's settings are set to **Not Visible**. * {profilefullname}: Similar to {fullname} except that it displays a profile owner's name when placed on the Profile page. * {firstaccessdate dateTimeFormat} : Date that the user first accessed the site. For information on the optional dateTimeFormat format, see Supported dateTimeFormats Formats in the [FAQ](#faq) section of this documentation. @@ -426,8 +428,10 @@ Note: The **shortname** for {ifprofile **shortname**...} tags can be any visible If the condition is not met in the particular context, the specified tag and its content will be removed. #### Conditionally display content filters (For Moodle Mobile app and Web services) + * {ifmobile}{/ifmobile} : Will display content if accessed from a web service such as the Moodle mobile app. * {ifnotmobile}{/ifnotmobile} : Will display content if not accessed from a web service such as a web browser. + #### Conditionally display content filters (For Moodle Workplace) * (BETA) {iftenant idnumber|tenantid}{/iftenant} : Will display the content if a tenant idnumber or tenant id is specified. Only {iftenant 1} will work in Moodle classic. @@ -440,7 +444,7 @@ If the condition is not met in the particular context, the specified tag and its * {hr} : Horizontal rule. * {details}{summary}{/summary}{/details} : An easy way to create an HTML 5 Details/Summary expandable section in your page. IMPORTANT: {details}{summary}{/summary} must all be on one line (it is ok if the line wraps). The rest of the details can be on multiple lines followed by the {/details}. This is an experimental feature that may result in invalid HTML but it works. You can optionally add a CSS class name to the opening details tag. Example: {details faq-class} * {multilang xx}{/multilang} : Tags text so it displays only when the user interface is set to that particular language. For example, use {multilang en}English{/multilang}{multilang fr}Français{/multilang} to display ‘English’ when the UI is in English and ‘Français’ when it’s in French. Please be aware that this method does not actually perform language filtering. It merely simplifies the usage of Moodle’s **Multi-Language Content** filter. If this filter is activated, it will convert these plain text tags into HTML span tags, which are then processed by the **Multi-Language Content** filter. This only works if the Multi-Language Content filter is listed below the FilterCodes filter in ‘ Site Administration > Plugins > Filters > Manage filters’. When the content displayed, if you see the content for all languages, it is because you did not enable the Multi-Language Content filter. -* {langx xx}{/langx} : Tag specific text in a particular language by wrapping the text in a plain text pair of {langx xx} {/langx} or {langx xx-XX} {/langx} tags. This makes no visible changes to the content but wraps the content in an HTML inline tag. As a result, screen readers will make use of this localization information to apply a particular pronunciation if the text is in a different language than the language of the rest of the page. This is required for compliance with W3C Web Content Accessibility Guidelines (WCAG 2.0) +* {langx xx}{/langx} : Tag specific text in a particular language by wrapping the text in a plain text pair of {langx xx} {/langx} or {langx xx-XX} {/langx} tags. This makes no visible changes to the content but wraps the content in an HTML `` inline tag. As a result, screen readers will make use of this localization information to apply a particular pronunciation if the text is in a different language than the language of the rest of the page. This is required for compliance with W3C Web Content Accessibility Guidelines (WCAG 2.0) The opening {langx xx} tag should include two [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) language code abbreviation letters in lowercase associated with the language's name. French, for example, has the code **fr**: @@ -631,8 +635,8 @@ FilterCodes can work in custom menus but, unfortunately, only if the theme suppo There are currently four ways to enable FilterCodes in the Custom menu/Primary Navigation. You can use any one of the following: -* Upgrade to Moodle LMS 5.0. As a site administrator, you need to navigate to Site Administration > Appearance > Advanced theme settings. Enable `Filter custom menu`. -* Use the Trema theme for Moodle LMS. If you are not using Moodle 5.0 yet, you need to navigate to Site Administration > Appearance > Trema. In the General tab, Filter navigation. This provides the same functionality as in Moodle 5.0 but is compatible with Moodle 4.0 and later. +* Upgrade to Moodle LMS 5.0+. As a site administrator, you need to navigate to Site Administration > Appearance > Advanced theme settings. Enable `Filter custom menu`. +* Use the Trema theme for Moodle LMS. If you are not using Moodle 5.0+ yet, you need to navigate to Site Administration > Appearance > Trema. In the General tab, Filter navigation. This provides the same functionality as in Moodle 5.0+ but is compatible with Moodle 4.0 and later. * Patch Moodle core. See [Technique A: Patching Moodle core](#technique-a-patching-moodle-core) below. * Patch your theme. See [Technique B: Patching your Moodle theme](#technique-b-patching-your-moodle-theme) below. @@ -640,7 +644,7 @@ If you are still using a version of Moodle older than 4.1, we highly recommend t ### Technique A: Patching Moodle core -Moodle 5.0 does not require any patching of Moodle core. For Moodle 3.7 to 4.5, preferred method is to patch your instance of Moodle using Git. If you did not install Moodle using Git, you can still apply the changes but you will need to do so manually. +Moodle 5.0+ does not require any patching of Moodle core. For Moodle 3.7 to 4.5, preferred method is to patch your instance of Moodle using Git. If you did not install Moodle using Git, you can still apply the changes but you will need to do so manually. To patch Moodle to handle this properly for most Moodle themes, cherry-pick the following patch to your Moodle site: @@ -669,9 +673,9 @@ This is usually enough to make the filters work in the custom menu. However, we If technique A does not work for you, you will need to integrate a few lines of code into your Moodle theme, or ask your theme's developer/maintainer to apply this change for you. Be sure to follow the correct instructions for your version of Moodle. -#### For themes based on **boost** (Moodle 5.0) +#### For themes based on **boost** (Moodle 5.0+) -If you are using Moodle 5.0, you should not need patch your theme. However, some 3rd party themes bypass Moodle's API when generate their navigation menu and may not yet be compatible. Please reach out to the developer and let them know that they need to make their theme compatible with Moodle 5.0. +If you are using Moodle 5.0+, you should not need patch your theme. However, some 3rd party themes bypass Moodle's API when generate their navigation menu and may not yet be compatible. Please reach out to the developer and let them know that they need to make their theme compatible with Moodle 5.0+. #### For themes based on **boost** (Moodle 4.0 to 4.5) @@ -906,7 +910,7 @@ If the matching tag, class and/or id cannot be found, will return all of the pag Help students navigate your Moodle site by implementing this handy-dandy BACK button. Works at both the section and activity level. ```html -

Go Back

+

Go Back

``` If you are in a section and want to go directly back to the main course outline but scroll down to the current section, try this: @@ -957,7 +961,7 @@ If enabled, custom profile fields that are hidden from the user will be displaye Choose the type of link for the teacher\s link in the {coursecontacts} tags. Profile, Messaging, Email address or None. Choose None if you don't want just the name without a link. -### Show {categorycards} background +### Show background Enable or disable the background pattern/image for {categorycards}. You can also optionally configure the look of {categorycards} using CSS on the .fc-categorycards class. @@ -965,7 +969,7 @@ Enable or disable the background pattern/image for {categorycards}. You can also Define your own custom global tags, sometimes also called global blocks. This feature enables you to create FilterCodes tags that are prefixed by **global_** . You can currently have up to 100 custom {global_...} tags. -### Customizing or translating the forms generated by the {form...} tags +### Customizing or translating the forms generated by the tags You can translate or customize the form tags in Moodle's language editor. Here is how to do it: @@ -1065,9 +1069,24 @@ Note: There have also been reported cases where some tags in URLs, like {wwwroot * If **nginx** is being used but not configured correctly. * If you are using Moodle's **Log In As** feature, there are several reports of Moodle re-writing HTML when logged in as another user. It does not just affect FilterCodes. See https://tracker.moodle.org/browse/MDL-65372 +## Known Compatibility Issues + +### Third-Party Plugin Compatibility + +**Edwiser Reports Plugin**: Some versions of the Edwiser Reports plugin (local_edwiserreports) contain a bug that can cause FilterCodes unit tests to fail. The issue occurs when the plugin attempts to access `$_SERVER['REQUEST_URI']` without checking if it exists, which can happen in PHPUnit test environments. + +**Symptoms**: Unit test failures with errors like "Undefined array key 'REQUEST_URI'" originating from the Edwiser Reports plugin. + +**Impact**: This only affects unit testing, not production functionality. The FilterCodes plugin works normally in production environments. + +**Workaround**: If you encounter this issue during testing, the problem is with the Edwiser Reports plugin, not FilterCodes. Please report the issue to the Edwiser Reports developers. + +**Note**: FilterCodes does not compensate for bugs in third-party plugins. This documentation is provided for awareness and troubleshooting purposes only. + More helpful information can be found in the [FAQ](#faq) below. # FAQ + ## Answers to Frequently Asked Questions IMPORTANT: Although we expect everything to work, this release has not been fully tested in every situation. If you find a problem, please help by reporting it in the [Bug Tracker](https://github.com/michael-milette/moodle-filter_filtercodes/issues). @@ -1080,7 +1099,7 @@ Yes. You can only combine (AND) them. The two, or more, tags must be all be true This plugin does not support {IF this OR that} type conditions at this time. Depending on your requirement, the {ifmin...} tags might help you achieve this. These tags enable you to display content to users with a minimum role level. This could be useful if you wanted to only display a message to faculty such as (teacher or above). -### How can {ifactivitycompleted} work for the completion of a combination of multiple activities? +### How can work for the completion of a combination of multiple activities? You will need to use the Pulse plugin for Moodle LMS by Stefan Scholz. It allows you to have one activity completion reflect the completed status of multiple other activities. Dave Foord has an excellent tutorial on how to use this on his YouTube channel at: https://www.youtube.com/watch?v=VlmLjIUFC6I . Once you have that setup, you will then be able to use FilterCodes {ifactivitycompleted} tag to do whatever you want based on the completion of the one Pulse activity. @@ -1104,7 +1123,7 @@ Pro Tip: You can pre-populate a field and make it non-editable for logged in use ``` -### Why do administrators see the text of all other roles when using {ifminxxxx}Content{/ifminxxxx} tags? +### Why do administrators see the text of all other roles when usingContent tags? This is normal as the administrator has the permission of all other roles. The {ifmin...} tags will display content if the user has a minimum of the specified role or above. For example, {ifminteacher}Content here!{/ifminteacher} will display "Content here!" whether the user is a teacher, course creator, manager or administrator even if they are not a teacher. @@ -1122,7 +1141,7 @@ Be sure to check back on your issue as we may have further questions for you. If you have the skills, feel free to contribute code for new tags. These are more likely to get integrated quicker (subject to our review and approval). -### Why does the {button} tag not work? +### Why does the tag not work? It works just fine. Here are 3 examples: @@ -1140,7 +1159,7 @@ The last one will create a button called "Dashboard", using the Moodle language The trick is to make sure that Moodle doesn't convert the URL to a link in the editor. If it does (probably blue, underlined), you will need to use the Unlink tool to turn it back into plain text. Once saved, it will appear as a button. Alternatively, disable the **Convert URLs into links and images** filter in Site Administration > Plugins > Filters > Manage Filters and then re-enter the {button} FilterCode. -### How can I style the {coursecontacts} tag? +### How can I style the tag? Here is an example that reduces the image and places the information next to it. Just add this CSS to your site: @@ -1224,11 +1243,11 @@ Create a Page on your Moodle site, preferably in a course, so that those tags wo * (ALPHA) Course cards [{coursecards}]: {coursecards} * (ALPHA) Individual course cards [{coursecard 2}]: {coursecard 2} * (ALPHA) Course cards by enrolment [{coursecardsbyenrol}]: {coursecardsbyenrol} -* Team cards Our faculty team [{teamcards}]: Our faculty team
{teamcards} +* Team cards Our faculty team [{teamcards}]: Our faculty team`
`{teamcards} * (ALPHA) Category cards [{categorycards}]: {categorycards} * (ALPHA) Category 1 cards [{categorycards 1}]: Sub-categories of Miscellaneous category include {categorycards 1} * (ALPHA) Dashboard site information [{dashboard_siteinfo}]: -{dashboard_siteinfo} + {dashboard_siteinfo} * Total courses [{coursecount}]: {coursecount} * Institution [{institution}]: {institution} * Department [{department}]: {department} @@ -1248,33 +1267,33 @@ Create a Page on your Moodle site, preferably in a course, so that those tags wo * Available free application disk space [{diskfreespace}]: {diskfreespace} * Available free moodledata disk space [{diskfreespacedata}]: {diskfreespacedata} * My Enrolled Courses [{mycourses}]: {mycourses} -* My Enrolled Courses menu [{mycoursesmenu}]:
{mycoursesmenu}
-* My Enrolled Courses as cards [{mycoursescards}]:
{mycoursescards} +* My Enrolled Courses menu [{mycoursesmenu}]: `
`{mycoursesmenu}`
` +* My Enrolled Courses as cards [{mycoursescards}]: `
`{mycoursescards} * My Completed Courses [{myccourses}]: {myccourses} * Link to the request a course page (blank if not enabled) [{courserequest}]: {courserequest} -* Request a course / Course request in top level menu [{courserequestmenu0}]:
{courserequestmenu0}
-* Request a course / Course request in submenu [{courserequestmenu}]:
{courserequestmenu}
+* Request a course / Course request in top level menu [{courserequestmenu0}]: `
`{courserequestmenu0}`
` +* Request a course / Course request in submenu [{courserequestmenu}]: `
`{courserequestmenu}`
` * Label [{label info}]Criteria for completion[{/label}]: {label info}Criteria for completion{/label} * Button [{button https://moodle.org}]Go to Moodle.org{/button}]: {button https://moodle.org.org}Go to Moodle.org{/button} * 80% radial chart [{chart radial 80 Are you over 70%?}]: {chart radial 80 Are you over 70%?} * 60% pie chart [{chart pie 60 Are you over 70%?}]: {chart radial 60 Are you over 70%?} * 75% progressbar chart [{chart progressbar 75 Are you over 70%?}]: {chart progressbar 75 Are you over 70%?} * 80% progresspie chart [{chart progresspie 80 --size:100px --border:15px --color:darkblue --bgcolor:lightblue --title:Are you over 70%?}]: {chart progresspie 80 --size:100px --border:15px --color:darkblue --bgcolor:lightblue --title:Are you over 70%?} -* Moodle Admin custom menu items [{menuadmin}]:
{menuadmin}
-* Moodle Dev custom menu items [{menudev}]:
{menudev}
-* Moodle Admin theme switcher [{menuthemes}]:
{menuthemes}
-* Secondary menu for pre-4.x themes [{menucoursemore}]:
{menucoursemore}
-* Wishlist menu [{menuwishlist}]:
{menuwishlist}
+* Moodle Admin custom menu items [{menuadmin}]: `
`{menuadmin}`
` +* Moodle Dev custom menu items [{menudev}]: `
`{menudev}`
` +* Moodle Admin theme switcher [{menuthemes}]: `
`{menuthemes}`
` +* Secondary menu for pre-4.x themes [{menucoursemore}]: `
`{menucoursemore}`
` +* Wishlist menu [{menuwishlist}]: `
`{menuwishlist}`
` * Course's category ID (0 if not in a course or category list of course) [{categoryid}]: {categoryid} * Course's category name (blank if not in a course) [{categoryname}]: {categoryname} * Course's category number (blank if not in a course) [{categorynumber}]: {categorynumber} * Course's category description (blank if not in a course) [{categorydescription}]: {categorydescription} * Course categories [{categories}]: {categories} -* Course categories menu [{categoriesmenu}]:
{categoriesmenu}
+* Course categories menu [{categoriesmenu}]: `
`{categoriesmenu}`
` * Top level course categories [{categories0}]: {categories0} -* Top level course categories menu [{categories0menu}]:
{categories0menu}
+* Top level course categories menu [{categories0menu}]: `
`{categories0menu}`
` * Other course categories in this category [{categoriesx}]: {categoriesx} -* Other course categories in this categories menu [{categoriesxmenu}]:
{categoriesxmenu}
+* Other course categories in this categories menu [{categoriesxmenu}]: `
`{categoriesxmenu}`
` * List of custom course fields [{course_fields}]: {course_fields} * Course custom fields [{course_field_location}] (assumes you have created a custom course field called "location"): {course_field_location} * Number of participants in the course [{courseparticipantcount}]: {courseparticipantcount} @@ -1307,7 +1326,7 @@ Create a Page on your Moodle site, preferably in a course, so that those tags wo * String with component [{getstring:filter_filtercodes}]filtername[{/getstring}]: {getstring:filter_filtercodes}filtername{/getstring} * String [{getstring}]Help[{/getstring}]: {getstring}help{/getstring} * Toggle editing menu [{toggleeditingmenu}]: {toggleeditingmenu} -* Editing Toggle [{editingtoggle}]: Toggle editing +* Editing Toggle [{editingtoggle}]: ``Toggle editing`` * FontAwesome "fa-globe": v4.x [{fa fa-globe}] {fa fa-globe}, v5.x [{fas fa-globe}] {fas fa-globe}, v6.x [{fa-solid fa-globe}] {fa-solid fa-globe}. Must be supported by your theme. * Press the [{keyboard}]Ctrl[{/keyboard}]+[{keyboard}]r[{/keyboard}] to refresh the page: Press the {keyboard}CTRL{/keyboard} + {keyboard}r{/keyboard} to refresh the page. * Glyphicons "glyphicon-envelope": Glyphicons [{glyphicon glyphicon-envelope}] {glyphicon glyphicon-envelope}. Must be supported by your theme. @@ -1334,7 +1353,7 @@ Create a Page on your Moodle site, preferably in a course, so that those tags wo * [{ifprofile email contains "@example.com"}]We have additional courses available for example.com customers today.[{/ifprofile}] : {ifprofile email contains "@example.com"}We have additional courses available for example.com customers today.{/ifprofile} * [{ifprofile country in "CA,US,UK,AU,NZ"}]We have many courses available in English[{/ifprofile}] : {ifprofile country in "CA,US,UK,AU,NZ"}We have many courses available in English{/ifprofile} * If defined custom user profile field with a shortname called "iswoman" is not blank or zero [{ifprofile_field_iswoman}Female{/ifprofile_field_iswoman}]: {ifprofile_field_iswoman}Female{/ifprofile_field_iswoman} -* If Editing mode is deactivated (off) [{ifnoteditmode}]<a href="{wwwroot}/course/view.php?id={courseid}&sesskey={sesskey}&edit=on">Turn edit mode on<a/>[{/ifnoteditmode}]: {ifnoteditmode}Turn edit mode on{/ifnoteditmode} +* If Editing mode is deactivated (off) [{ifnoteditmode}]<a href="{wwwroot}/course/view.php?id={courseid}&sesskey={sesskey}&edit=on">Turn edit mode on<a/>[{/ifnoteditmode}]: {ifnoteditmode}``Turn edit mode on``{/ifnoteditmode} * If on the course enrolment page? [{ifenrolpage}]Yes[{/ifenrolpage}]: {ifenrolpage}Yes{/ifenrolpage} * If Enrolled [{ifenrolled}]You are enrolled in this course.[{/ifenrolled}]: {ifenrolled}You are enrolled in this course.{/ifenrolled} * If Not Enrolled [{ifnotenrolled}]You are not enrolled in this course.[{/ifnotenrolled}]: {ifnotenrolled}You are not enrolled in this course.{/ifnotenrolled} @@ -1405,7 +1424,7 @@ You can switch to different roles to see how each will affect the content being You can do this using the language editor built into Moodle. There is currently support for the following defaults: defaultfirstname, defaultsurname, defaultusername, defaultemail. By default, these are blank. As for the Full Name, it is made up of the first name and surname separated by a space and is therefore not settable. -### I added the "{mycoursesmenu}" to my custom menu. How can I hide it if the user is not logged in? +### I added the "" to my custom menu. How can I hide it if the user is not logged in? You can use the {ifloggedin}{/ifloggedin} tags to conditionally hide it when users are not logged in. Example: @@ -1432,7 +1451,7 @@ Building on the previous two questions, see the [usage](#usage) section for some The Frontpage is a course in Moodle. All users are enrolled by default in this course. -### I added the {recaptcha} tag in my webform. Why doesn't the reCAPTCHA show up? +### I added the tag in my webform. Why doesn't the reCAPTCHA show up? First, the reCAPTCHA is only made to work with forms processed by the Contact Form for Moodle plugin. That said, it is 100% generated by Moodle API so, if you have some other purpose, it will probably work as well as long as the receiving form is made to process it. @@ -1440,7 +1459,7 @@ For reCAPTCHA to work, you need to configure the site and secret keys in Moodle. If you are using older versions of Moodle before 3.1.11+, 3.2.8+, 3.3.5+, 3.4.5+ and 3.5+, that implementation of ReCAPTCHA is no longer supported by Google. -### How can I get the {scrape} tag to work? +### How can I get the tag to work? You need to enable this feature in the FilterCodes settings in Moodle. @@ -1452,15 +1471,15 @@ Use multiple {scrape} tags. That is not possible at this time. This is a very simple scraper. With some funding or contributions, this feature can be enhanced. -### How can I get the {getstring} tag to work? It doesn't seem to be replaced with the correct text. +### How can I get the tag to work? It doesn't seem to be replaced with the correct text. Verify that the component (plugin) name and/or the string key are correct. If a component name is not specified, it will default to "moodle". If you recently modified a language file manually in Moodle, you may need to refresh the Moodle cache. -### How can I customize or translate the forms generated by the {form...} tags? +### How can I customize or translate the forms generated by the tags? See **Customizing or translating the forms generated by the {form...} tags** in the [Usage](#usage) section. -### Is there more information about the {ifprofile shortname ...} tag? +### Is there more information about the tag? {ifprofile shortname ...} is a conditional tag that enables you to display information depending on the value of fields in a user's profile. @@ -1530,7 +1549,6 @@ As of version 2.2.8+ of FilterCodes, you can also use (strftime)[https://www.php Note: The date and/or time format can vary depending on the language pack in use. While you can customize these using the Moodle language customization tool included with Moodle, making such a change will could affect the format of dates displayed throughout your Moodle site. Pro tip: Standardize the date format used throughout your site as much as possible to minimize the chance of potentially confusing your learners. - ### Are there any security considerations? There are no known security considerations at this time. @@ -1630,6 +1648,6 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License -along with FilterCodes. If not, see . +along with FilterCodes. If not, see [https://www.gnu.org/licenses/](https://www.gnu.org/licenses/). [(Back to top)](#table-of-contents) diff --git a/classes/text_filter.php b/classes/text_filter.php index b7af37c..b5c2cc4 100644 --- a/classes/text_filter.php +++ b/classes/text_filter.php @@ -41,6 +41,18 @@ class_alias('\core_filters\text_filter', 'filtercodes_base_text_filter'); class_alias('\moodle_text_filter', 'filtercodes_base_text_filter'); } +/** + * str_contains() Polyfill for PHP < 8.0. + */ +if (!function_exists('str_contains')) { + function str_contains($haystack, $needle) { + if ($needle === '') { + return true; // Match PHP 8.0 behavior - empty string is always found. + } + return mb_strpos($haystack, $needle) !== false; + } +} + /** * Extends the moodle_text_filter class to provide plain text support for new tags. * @@ -95,6 +107,12 @@ private function hasarchetype($archetype) { // Handle caching of results. static $archetypes = []; + + // Clear cache in unit tests to ensure test isolation. + if (defined('PHPUNIT_TEST') && PHPUNIT_TEST) { + $archetypes = []; + } + if (isset($archetypes[$archetype])) { return $archetypes[$archetype]; } @@ -286,7 +304,13 @@ private function getuserprofilefields($user, $fields = []) { static $profilefields; static $lastfields; - // If we have already cached the profile fields and data, return them. + // Clear cache in unit tests to ensure test isolation. + if (defined('PHPUNIT_TEST') && PHPUNIT_TEST) { + $profilefields = null; + $lastfields = null; + } + + // If we have already cached the profile fields and data for this user and fields, return them. if (isset($profilefields) && $lastfields == $fields) { return $profilefields; } @@ -365,7 +389,7 @@ private function ishttps() { 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))); + $isws = (WS_SERVER || (strstr($ME ?? '', "webservice/") !== false && optional_param('token', '', PARAM_ALPHANUM))); return $isws; } @@ -567,9 +591,13 @@ private function rendercategorycard($category, $categoryshowpic) { * @return bool True if the user is logged in and not a guest user, false otherwise. */ private function isauthenticateduser() { + global $USER; static $isauthenticateduser; + static $cacheduserId; - if (!isset($isauthenticateduser)) { + // Recalculate if user has changed or not yet cached. + if (!isset($cacheduserId) || $cacheduserId !== $USER->id) { + $cacheduserId = $USER->id; $isauthenticateduser = isloggedin() && !isguestuser(); } @@ -754,7 +782,7 @@ private function getcoursecardinfo($format = null) { /** * 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 string $clinktype Type of link to generate. Options include: email, message, profile, phone, mobile. * @param object $user A user object. * @param string $name The name to be displayed. * @@ -764,22 +792,22 @@ private function userlink($clinktype, $user, $name) { if (!isloggedin() || isguestuser()) { $clinktype = ''; // No link, only name. } - switch ($clinktype) { - case 'email': + + switch (true) { + case $clinktype == 'email': $link = '' . $name . ''; break; - case 'message': + case $clinktype == 'message': $link = '' . $name . ''; break; - case 'profile': + case $clinktype == 'profile': $link = '' . $name . ''; break; - case 'phone1': - if (!empty($user->phone1)) { - $link = '' . $name . ''; - } else { - $link = $name; - } + case $clinktype == 'phone' && !empty($user->phone1): + $link = '' . $name . ''; + break; + case $clinktype == 'mobile' && !empty($user->phone2): + $link = '' . $name . ''; break; default: $link = $name; @@ -1328,7 +1356,7 @@ private function generatortags(&$text) { global $OUTPUT, $DB; $sql = 'SELECT DISTINCT u.id, u.username, u.firstname, u.lastname, u.email, u.picture, u.imagealt, u.firstnamephonetic, - u.lastnamephonetic, u.middlename, u.alternatename, u.description, u.phone1 + u.lastnamephonetic, u.middlename, u.alternatename, u.description, u.phone1, u.phone2 FROM {course} c, {role_assignments} ra, {user} u, {context} ct WHERE c.id = ct.instanceid AND ra.roleid in (?) AND ra.userid = u.id AND ct.id = ra.contextid AND u.suspended = 0 AND u.deleted = 0 @@ -1367,6 +1395,7 @@ private function generatortags(&$text) { 'message' => get_string('message', 'message'), 'profile' => get_string('profile'), 'phone' => get_string('phone'), + 'mobile' => get_string('phone2'), ]; if ($cardformat == 'verbose') { if (empty($CFG->enablegravatar)) { @@ -1610,6 +1639,104 @@ private function replacetags(&$text, &$replace) { return $moretags; } + /** + * Helper function for creating if tags. + * + * @param string $text The text to check fo tags in + * @param array $replace The array of replacement rules to add to + * @param string $tagname The tagname to search for + * @param callable $callableistrue Callable taking the arguments, + * returning 1 if the content is to be shown, 0 for not show, -1 to ignore. + * @return void Nothing + */ + private function if_tag($text, &$replace, $tagname, callable $callableistrue) { + $emit = function (array $stack) use (&$replace, $tagname) { + $key = ''; + $value = ''; + for ($i = 0; $i < count($stack); $i++) { + $isopening = $stack[$i][0]; + $args = $stack[$i][1]; + $istrue = $stack[$i][2]; + if ($isopening) { + $key .= '{' . $tagname . '\s+' . preg_quote($args, '/') . '}(.*)'; + if ($istrue) { + $value .= '$' . ($i + 1); + } + } else { + $key .= '(.*){\/' . $tagname . '}'; + if ($istrue) { + $value .= '$' . ($i + 1); + } + } + } + $replace['/' . $key . '/isuU'] = $value; + }; + + if (stripos($text, '{' . $tagname) !== false && stripos($text, '{/' . $tagname . '}') !== false) { + // Find opening and closing tags. + $re = '/{' . $tagname . '\s+(.*)}|{(\/)' . $tagname . '}/isuU'; + $found = preg_match_all($re, $text, $matches, PREG_SET_ORDER); + if ($found > 0) { + $balance = 0; + $stack = []; + $istrue = []; + foreach ($matches as $match) { + $isopening = (!isset($match[2])); + if (empty($stack) && !$isopening) { + continue; // No opening tag found. + } + $balance += $isopening ? 1 : -1; + if ($isopening) { + $lastistrue = empty($istrue) ? true : end($istrue); + $thisistrue = $callableistrue($match[1]); + if ($thisistrue === -1) { + continue; + } + $istrue[] = $lastistrue && $thisistrue; + } + + $stack[] = [ + $isopening, + $match[1], + end($istrue), + ]; + + if (!$isopening) { + // Pop the last element. + array_pop($istrue); + } + + if ($balance == 0) { + // We are balanced, generate replacement. + $emit($stack); + $stack = []; + } + } + if (!empty($stack)) { + // Drop opening tags till we are balanced. + $newstack = []; + foreach (array_reverse($stack) as $item) { + if ($item[0] && $balance > 0) { + // Need to remove opening tag. + $balance--; + continue; + } + $newstack[] = $item; + } + // Fix istrue values for the new stack by assigning the value of the opening tags + // to the closing tags in the final stack. + for ($i = 0; $i < count($newstack) / 2; $i++) { + $newstack[$i][2] = $newstack[count($newstack) - 1 - $i][2]; + } + + if (!empty($newstack)) { + $emit(array_reverse($newstack)); + } + } + } + } + } + /** * Main filter function called by Moodle. * @@ -1631,6 +1758,20 @@ public function filter($text, array $options = []) { static $mygroupingslist; static $mycohorts; + // Clear cache in unit tests to ensure test isolation. + if (defined('PHPUNIT_TEST') && PHPUNIT_TEST) { + $options['no-cache'] = true; + } + + // Clear user-specific cached data if necessary. + if (!empty($options['no-cache'])) { + $profilefields = null; + $profiledata = null; + $mygroupslist = null; + $mygroupingslist = null; + $mycohorts = null; + } + $replace = []; // Array of key/value filterobjects. // Handle escaped tags to be ignored. Remove them so they don't get processed if the option to [{escape braces}] is enabled. @@ -1705,7 +1846,7 @@ public function filter($text, array $options = []) { } // Tags: {courseid... - if (stripos($text, '{course') !== false || stripos($text, '%7Bcourseid') !== false) { + if (stripos($text, '{course') !== false || stripos($text, '%7Bcourse') !== false) { $courseid = 1; // Default to site. if ($PAGE->pagetype == 'enrol-index') { // Make it work, even when we are on the enrolment page. @@ -1863,8 +2004,10 @@ public function filter($text, array $options = []) { } if (stripos($text, '{thisurl') !== false) { - $url = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . - "://{$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}"; + $protocol = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http"); + $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost'; + $uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/'; + $url = $protocol . "://" . $host . $uri; // Tag: {thisurl}. // Description: Complete URL of the current page. @@ -2550,7 +2693,7 @@ function ($matches) use ($now) { if ($CFG->branch >= 311) { $text = str_replace('{webpage}', '{profile_field_webpage}', $text); } else { - $replace['/\{webpage\}/i'] = $this->isauthenticateduser() ? $USER->url : ''; + $replace['/\{webpage\}/i'] = '';//$this->isauthenticateduser() ? $USER->url : ''; } } @@ -2587,7 +2730,7 @@ function ($matches) use ($now) { // Description: Support email address for the site from Moodle settings. // None. if (stripos($text, '{supportemail}') !== false) { - if (empty($CFG->supportname)) { + if (empty($CFG->supportemail)) { $replace['/\{supportemail\}/i'] = get_string('notavailable', 'filter_filtercodes'); } else { $replace['/\{supportemail\}/i'] = $CFG->supportemail; @@ -2897,12 +3040,14 @@ function ($matches) use ($USER) { 'message' => get_string('message', 'message'), 'profile' => get_string('profile'), 'phone' => get_string('phone'), + 'mobile' => get_string('phone2'), ]; $iconclass = ['' => '', 'email' => 'fa fa-envelope-o', 'message' => 'fa fa-comment-o', 'profile' => 'fa fa-user-o', - 'phone' => 'fa fa-mobile', + 'phone' => 'fa fa-phone', + 'mobile' => 'fa fa-mobile', ]; $cnt = 0; @@ -2937,29 +3082,33 @@ function ($matches) use ($USER) { $contacts .= '' . implode(", ", $rolenames) . ': '; - switch ($clinktype) { - case 'email': + switch (true) { + case $clinktype == 'email': $contacts .= $icon . ''; $contacts .= $contactsclose; break; - case 'message': + case $clinktype == 'message': $contacts .= $icon . ''; $contacts .= $contactsclose; break; - case 'profile': + case $clinktype == 'profile': $contacts .= $icon . ''; $contacts .= $contactsclose; break; - case 'phone1' && !empty($user->phone1): + case $clinktype == 'phone' && !empty($user->phone1): $contacts .= $icon . ''; $contacts .= $contactsclose; break; + case $clinktype == 'mobile' && !empty($user->phone2): + $contacts .= $icon . ''; + $contacts .= $contactsclose; + break; default: // Default is no-link. $contacts .= $fullname; break; @@ -3018,6 +3167,7 @@ function ($matches) use ($USER) { $replace['/\{coursecount students\}/i'] = $cnt; } if (stripos($text, '{coursecount students:active}') !== false) { + // First attempt: count students via role assignments (preferred, accurate when roles assigned). $sql = "SELECT COUNT(DISTINCT ue.userid) FROM {user_enrolments} ue JOIN {enrol} e ON e.id = ue.enrolid @@ -3026,7 +3176,18 @@ function ($matches) use ($USER) { JOIN {role_assignments} ra ON ra.contextid = ctx.id AND ra.userid = ue.userid JOIN {role} r ON r.id = ra.roleid AND r.shortname = 'student' WHERE ue.status = 0 AND e.courseid = :courseid"; - $cnt = $DB->count_records_sql($sql, ['courseid' => $PAGE->course->id]); + $cnt = (int)$DB->count_records_sql($sql, ['courseid' => $PAGE->course->id]); + + // Fallback: some test setups or unusual enrolment flows may not have role_assignments + // present. In that case, count distinct active enrolments as a safe fallback. + if ($cnt === 0) { + $fallbacksql = "SELECT COUNT(DISTINCT ue.userid) + FROM {user_enrolments} ue + JOIN {enrol} e ON e.id = ue.enrolid + WHERE ue.status = 0 AND e.courseid = :courseid"; + $cnt = (int)$DB->count_records_sql($fallbacksql, ['courseid' => $PAGE->course->id]); + } + $replace['/\{coursecount students:active\}/i'] = $cnt; } @@ -3696,6 +3857,13 @@ function ($matches) use ($USER) { // Description: An unordered list of links to categories in the same level as the current course. // Parameters: None. if (stripos($text, '{categoriesx}') !== false) { + if (empty($PAGE->course->category)) { + // If we are not in a course, check if categoryid is part of URL (ex: course lists). + $catid = optional_param('categoryid', 0, PARAM_INT); + } else { + // Retrieve the category id of the course we are in. + $catid = $PAGE->course->category; + } $sql = "SELECT cc.id, cc.sortorder, cc.name, cc.visible, cc.parent FROM {course_categories} cc WHERE cc.parent = $catid AND cc.visible = 1 @@ -3716,6 +3884,13 @@ function ($matches) use ($USER) { // Description: A list of links to categories in the same level as the current course - for use in the custom menu. // Parameters: None. if (stripos($text, '{categoriesxmenu}') !== false) { + if (empty($PAGE->course->category)) { + // If we are not in a course, check if categoryid is part of URL (ex: course lists). + $catid = optional_param('categoryid', 0, PARAM_INT); + } else { + // Retrieve the category id of the course we are in. + $catid = $PAGE->course->category; + } $sql = "SELECT cc.id, cc.sortorder, cc.name, cc.visible, cc.parent FROM {course_categories} cc WHERE cc.parent = $catid AND cc.visible = 1 @@ -4024,97 +4199,69 @@ function ($matches) use ($USER) { } // Tag: {ifactivitycompleted coursemoduleid}...{/ifactivitycompleted}. - // Description: Will display content if the specified activity has been completed. - // Required Parameter: coursemoduleid is the id of the instance of the content module. - // Requires content between tags. - if (stripos($text, '{/ifactivitycompleted}') !== false) { + // Tag: {ifnotactivitycompleted coursemoduleid}...{/ifnotactivitycompleted}. + if (( + stripos($text, '{ifactivitycompleted') !== false + && stripos($text, '{/ifactivitycompleted}') !== false + ) || ( + stripos($text, '{ifnotactivitycompleted') !== false + && stripos($text, '{/ifnotactivitycompleted}') !== false + )) { $completion = new \completion_info($PAGE->course); - if ($completion->is_enabled_for_site() && $completion->is_enabled() == COMPLETION_ENABLED) { - // Get a list of the the instances of this tag. - $re = '/{ifactivitycompleted\s+([0-9]+)\}(.*)\{\/ifactivitycompleted\}/isuU'; - $found = preg_match_all($re, $text, $matches); - - if ($found > 0) { - // Check if the activity is in the list. - foreach ($matches[1] as $cmid) { - $iscompleted = false; - - // Only process valid IDs. - if (($cm = \get_coursemodule_from_id('', $cmid, 0)) !== false) { - // Get the completion data for this activity if it exists. - try { - $data = $completion->get_data($cm, false, $USER->id); - $iscompleted = ($data->completionstate > COMPLETION_INCOMPLETE); // A completed state. - } catch (\moodle_exception $e) { - // Handle Moodle-specific exceptions. - unset($e); - continue; - } catch (\Exception $e) { - unset($e); - continue; - } - } - - // If the activity has been completed, remove just the tags. Otherwise remove tags and content. - $key = '/{ifactivitycompleted\s+' . $cmid . '\}(.*)\{\/ifactivitycompleted\}/isuU'; - if ($iscompleted) { - // Completed. Keep the text and remove the tags. - $replace[$key] = "$1"; - } else { - // Activity not completed. Remove tags and content. - $replace[$key] = ''; + // Tag: {ifactivitycompleted coursemoduleid}...{/ifactivitycompleted}. + // Description: Will display content if the specified activity has been completed. + // Required Parameter: coursemoduleid is the id of the instance of the content module. + // Requires content between tags. + $this->if_tag( + $text, + $replace, + 'ifactivitycompleted', + function ($cmid) use ($USER, $completion) { + // Only process valid IDs. + if (($cm = \get_coursemodule_from_id('', $cmid, 0)) !== false) { + // Get the completion data for this activity if it exists. + try { + $data = $completion->get_data($cm, false, $USER->id); + return $data->completionstate > COMPLETION_INCOMPLETE; // A completed state. + } catch (\moodle_exception $e) { + // Handle Moodle-specific exceptions. + unset($e); + } catch (\Exception $e) { + unset($e); } } - } - } - } - - // Tag: {ifnotactivitycompleted coursemoduleid}...{/ifnotactivitycompleted}. - // Description: Will display content if the specified activity has been completed. - // Required Parameter: coursemoduleid is the id of the instance of the content module. - // Requires content between tags. - if (stripos($text, '{/ifnotactivitycompleted}') !== false) { - $completion = new \completion_info($PAGE->course); - if ($completion->is_enabled_for_site() && $completion->is_enabled() == COMPLETION_ENABLED) { - // Get a list of the the instances of this tag. - $re = '/{ifnotactivitycompleted\s+([0-9]+)\}(.*)\{\/ifnotactivitycompleted\}/isuU'; - $found = preg_match_all($re, $text, $matches); - - if ($found > 0) { - // Check if the activity is in the list. - foreach ($matches[1] as $cmid) { - $iscompleted = false; - - // Only process valid IDs. - if (($cm = \get_coursemodule_from_id('', $cmid, 0)) !== false) { - // Get the completion data for this activity if it exists. - try { - $data = $completion->get_data($cm, false, $USER->id); - $iscompleted = ($data->completionstate > COMPLETION_INCOMPLETE); // A completed state. - } catch (\moodle_exception $e) { - // Handle Moodle-specific exceptions. - unset($e); - continue; - } catch (\Exception $e) { - unset($e); - continue; - } - } + return -1; + }, + ); - // If the activity has been completed, remove just the tags. Otherwise remove tags and content. - $key = '/{ifnotactivitycompleted\s+' . $cmid . '\}(.*)\{\/ifnotactivitycompleted\}/isuU'; - if (!$iscompleted) { - // Completed. Keep the text and remove the tags. - $replace[$key] = "$1"; - } else { - // Activity not completed. Remove tags and content. - $replace[$key] = ''; + // Tag: {ifnotactivitycompleted coursemoduleid}...{/ifnotactivitycompleted}. + // Description: Will display content if the specified activity has been completed. + // Required Parameter: coursemoduleid is the id of the instance of the content module. + // Requires content between tags. + $this->if_tag( + $text, + $replace, + 'ifnotactivitycompleted', + function ($cmid) use ($USER, $completion) { + // Only process valid IDs. + if (($cm = \get_coursemodule_from_id('', $cmid, 0)) !== false) { + // Get the completion data for this activity if it exists. + try { + $data = $completion->get_data($cm, false, $USER->id); + return !($data->completionstate > COMPLETION_INCOMPLETE); // A completed state. + } catch (\moodle_exception $e) { + // Handle Moodle-specific exceptions. + unset($e); + } catch (\Exception $e) { + unset($e); } } - } - } + + return -1; + }, + ); } // Tag: {ifprofile_field_shortname}...{ifprofile_field_shortname}. @@ -4172,72 +4319,77 @@ function ($matches) use ($USER) { // 'in' to check if the value is in the fields content. // Parameters: "value": The text to compare the field against. // Requires content between tags. - if (stripos($text, '{/ifprofile}') !== false) { + if (stripos($text, '{/ifprofile}') !== false && stripos($text, '{ifprofile') !== false) { // Retrieve all custom profile fields and specified core fields. $corefields = ['id', 'username', 'auth', 'idnumber', 'email', 'institution', 'department', 'city', 'country', 'timezone', 'lang']; $profilefields = $this->getuserprofilefields($USER, $corefields); - // Find all ifprofile tags. - $re = '/{ifprofile\s+(\w+)\s+(is|not|contains|in)\s+"([^}]*)"}(.*){\/ifprofile}/isuU'; - $found = preg_match_all($re, $text, $matches); - if ($found > 0) { - foreach ($matches[1] as $key => $match) { - $fieldname = $matches[1][$key]; - $string = $matches[0][$key]; // String found in $text. - $operator = $matches[2][$key]; - $value = $matches[3][$key]; + // Process ifprofile tags recursively to handle nesting. + // Keep matching and replacing until no more tags are found. + $maxiterations = 10; // Prevent infinite loops. + $iteration = 0; + while (stripos($text, '{ifprofile') !== false && $iteration < $maxiterations) { + // Find innermost (non-nested) ifprofile tags. + // Use a regex that matches tags with content that doesn't contain other ifprofile tags. + $re = '/{ifprofile\s+(\w+)\s+(is|not|contains|in)\s+"([^}]*)"}((?:[^{]|{(?!(?:\/)?ifprofile))*?){\/ifprofile}/isuU'; + $found = preg_match_all($re, $text, $matches, PREG_SET_ORDER); + if ($found === 0) { + break; + } + + foreach ($matches as $match) { + $fieldname = $match[1]; + $operator = $match[2]; + $value = $match[3]; + $content = $match[4]; + $fullmatch = $match[0]; + $replacement = ''; // Do not process tag if the specified profile field name does not exist or user is not logged in. - if (!array_key_exists($fieldname, $profilefields) || !isloggedin() || isguestuser()) { - if ($operator == 'not') { - // It will always meet criteria of a "not" if the user doesn't have a profile. - $replace['/' . preg_quote($string, '/') . '/isuU'] = $matches[4][$key]; - } else { - // It will never match the criteria "is", "contains" or "in" if the user doesn't have a profile. - $replace['/' . preg_quote($string, '/') . '/isuU'] = ''; + if (array_key_exists($fieldname, $profilefields) && isloggedin() && !isguestuser()) { + $fieldvalue = $profilefields[$fieldname]->value; + if (!empty($value)) { + $value = trim($value, '"'); // Trim quotation marks. } - continue; - } - if (!empty($value)) { - $value = trim($value, '"'); // Trim quotation marks. + $matches_condition = false; + switch ($operator) { + case 'is': + // If the specified field is exactly the specified value. + // Example: {ifprofile country is "CA"}...{/ifprofile}. + // Example: {ifprofile city is ""}...{/ifprofile}. + $matches_condition = $fieldvalue === $value; + break; + case 'not': + // Example: {ifprofile country not "CA"}...{/ifprofile}. + // Example: {ifprofile institution not ""}...{/ifprofile}. + $matches_condition = $fieldvalue !== $value; + break; + case 'contains': + // If the specified field contains the specified value. + // Example:{ifprofile email contains "@yoursite.com"}...{/ifprofile}. + $matches_condition = strpos($fieldvalue, $value) !== false; + break; + case 'in': + // If the specified value contains the value specified in the field. + // Example: {ifprofile country in "CA,US,UK,AU,NZ"}...{/ifprofile}. + $matches_condition = strpos($value, $fieldvalue) !== false; + break; + } + if ($matches_condition) { + $replacement = $content; + } + } else { + // User not logged in or field doesn't exist + if ($operator === 'not') { + $replacement = $content; + } } - $content = ''; - switch ($operator) { - case 'is': - // If the specified field is exactly the specified value. - // Example: {ifprofile country is "CA"}...{/ifprofile}. - // Example: {ifprofile city is ""}...{/ifprofile}. - if ($profilefields[$fieldname]->value === $value) { - $content = $matches[4][$key]; - } - break; - case 'not': - // Example: {ifprofile country not "CA"}...{/ifprofile}. - // Example: {ifprofile institution not ""}...{/ifprofile}. - if ($profilefields[$fieldname]->value !== $value) { - $content = $matches[4][$key]; - } - break; - case 'contains': - // If the specified field contains the specified value. - // Example:{ifprofile email contains "@yoursite.com"}...{/ifprofile}. - if (strpos($profilefields[$fieldname]->value, $value) !== false) { - $content = $matches[4][$key]; - } - break; - case 'in': - // If the specified value contains the value specified in the field. - // Example: {ifprofile country in "CA,US,UK,AU,NZ"}...{/ifprofile}. - if (strpos($value, $profilefields[$fieldname]->value) !== false) { - $content = $matches[4][$key]; - } - break; - } - $replace['/' . preg_quote($string, '/') . '/isuU'] = $content; + $text = str_replace($fullmatch, $replacement, $text); } + $iteration++; } } @@ -4431,12 +4583,12 @@ function ($matches) use ($mycohorts) { // If Request a course is enabled... $context = \context_system::instance(); if (empty($CFG->enablecourserequests) || !has_capability('moodle/course:request', $context)) { + // If Request a Course is not enabled, remove the ifcourserequests tags and contained content. + $replace['/\{ifcourserequests\}(.*)\{\/ifcourserequests\}/isuU'] = ''; + } else { // Just remove the tags. $replace['/\{ifcourserequests\}/i'] = ''; $replace['/\{\/ifcourserequests\}/i'] = ''; - } else { - // If Request a Course is not enabled, remove the ifcourserequests tags and contained content. - $replace['/\{ifcourserequests\}(.*)\{\/ifcourserequests\}/isuU'] = ''; } } @@ -4522,7 +4674,7 @@ function ($matches) use ($mycohorts) { $replace['/\{ifinsection\}(.*)\{\/ifinsection\}/isuU'] = ''; } } else { - if ($this->hasarchetype('student')) { // If user is enrolled in the course. + if ($this->hasarchetype('student')) { // If user is enrolled in the course as a student archetype. // Remove the {ifnotincourse} strings if in a course. if (stripos($text, '{ifnotincourse}') !== false) { $replace['/\{ifnotincourse\}(.*)\{\/ifnotincourse\}/isuU'] = ''; @@ -4791,7 +4943,7 @@ function ($matches) use ($mycohorts) { // Requires content between tags. if (stripos($text, '{ifdev}') !== false) { // If an administrator with debugging is set to DEVELOPER mode... - if ($CFG->debugdisplay == 1 && is_siteadmin() && !is_role_switched($PAGE->course->id)) { + if ($CFG->debug == DEBUG_DEVELOPER && is_siteadmin() && !is_role_switched($PAGE->course->id)) { // Just remove the tags. $replace['/\{ifdev\}/i'] = ''; $replace['/\{\/ifdev\}/i'] = ''; @@ -4805,144 +4957,135 @@ function ($matches) use ($mycohorts) { // Description: Display content if the user is a member of the specified group. // Required Parameters: group id or idnumber. // Requires content between tags. - if (stripos($text, '{ifingroup') !== false) { - if (!isset($mygroupslist)) { // Fetch my groups. - $mygroupslist = groups_get_all_groups($PAGE->course->id, $USER->id); - } - $re = '/{ifingroup\s+(.*)\}(.*)\{\/ifingroup\}/isuU'; - $found = preg_match_all($re, $text, $matches); - if ($found > 0) { - foreach ($matches[1] as $groupid) { - $key = '/{ifingroup\s+' . $groupid . '\}(.*)\{\/ifingroup\}/isuU'; - $ismember = false; - foreach ($mygroupslist as $group) { - if ($groupid == $group->id || $groupid == $group->idnumber) { - $ismember = true; - break; - } - } - if ($ismember) { // Just remove the tags. - $replace[$key] = '$1'; - } else { // Remove the ifingroup tags and content. - $replace[$key] = ''; + $this->if_tag( + $text, + $replace, + 'ifingroup', + function ($groupid) use (&$mygroupslist, $PAGE, $USER) { + if (!isset($mygroupslist)) { // Fetch my groups. + $mygroupslist = groups_get_all_groups($PAGE->course->id, $USER->id); + } + + $ismember = false; + foreach ($mygroupslist as $group) { + if ($groupid == $group->id || $groupid == $group->idnumber) { + $ismember = true; + break; } } - } - } - // Tag: {ifnotingroup...}...{/ifnotingroup} with and without parameters. - if (stripos($text, '{ifnotingroup') !== false) { - // Tag: {ifnotingroup}...{/ifnotingroup}. - // Description: Display content if the user is NOT a member of any group. - // Required Parameters: None. - // Requires content between tags. - if (stripos($text, '{ifnotingroup}') !== false) { + return $ismember; + }, + ); + + // Tag: {ifnotingroup}...{/ifnotingroup}. + // Description: Display content if the user is NOT a member of any group. + // Required Parameters: None. + // Requires content between tags. + + // Tag: {ifnotingroup id|idnumber}...{/ifnotingroup}. + // Description: Display content if the user is NOT a member of the specified group. + // Required Parameters: group id or idnumber. + // Requires content between tags. + $this->if_tag( + $text, + $replace, + 'ifnotingroup', + function ($groupid) use (&$mygroupslist, $PAGE, $USER) { if (!isset($mygroupslist)) { // Fetch my groups. $mygroupslist = groups_get_all_groups($PAGE->course->id, $USER->id); } + if (empty($mygroupslist)) { - // User is not in any group, just remove the tags. - $replace['/\{ifnotingroup\}/i'] = ''; - $replace['/\{\/ifnotingroup\}/i'] = ''; - } else { - // User is in at least one group, remove tags and content. - $replace['/\{ifnotingroup\}(.*)\{\/ifnotingroup\}/isuU'] = ''; + return true; } - } - // Tag: {ifnotingroup id|idnumber}...{/ifnotingroup}. - // Description: Display content if the user is NOT a member of the specified group. - // Required Parameters: group id or idnumber. - // Requires content between tags. - if (stripos($text, '{ifnotingroup') !== false) { - if (!isset($mygroupslist)) { // Fetch my groups. - $mygroupslist = groups_get_all_groups($PAGE->course->id, $USER->id); + if (empty($groupid)) { + // My groups list is not empty, but no group id was specified. + return false; } - $re = '/{ifnotingroup\s+(.*)\}(.*)\{\/ifnotingroup\}/isuU'; - $found = preg_match_all($re, $text, $matches); - if ($found > 0) { - foreach ($matches[1] as $groupid) { - $key = '/{ifnotingroup\s+' . $groupid . '\}(.*)\{\/ifnotingroup\}/isuU'; - $ismember = false; - foreach ($mygroupslist as $group) { - if ($groupid == $group->id || $groupid == $group->idnumber) { - $ismember = true; - break; - } - } - if ($ismember) { // Remove the ifnotingroup tags and content. - $replace[$key] = ''; - } else { // Just remove the tags and keep the content. - $replace[$key] = '$1'; - } + + $ismember = false; + foreach ($mygroupslist as $group) { + if ($groupid == $group->id || $groupid == $group->idnumber) { + $ismember = true; + break; } } - } - } + + return !$ismember; + }, + ); // Tag: {ifingrouping id|idnumber}...{/ifingrouping}. // Description: Display content if the user is a member of the specified grouping. // Required Parameters: group id or idnumber. // Requires content between tags. - if (stripos($text, '{ifingrouping') !== false) { - if (!isset($mygroupingslist)) { - $mygroupingslist = $this->getusergroupings($PAGE->course->id, $USER->id); - } - $re = '/{ifingrouping\s+(.*)\}(.*)\{\/ifingrouping\}/isuU'; - $found = preg_match_all($re, $text, $matches); - if ($found > 0) { - foreach ($matches[1] as $groupingid) { - $key = '/{ifingrouping\s+' . $groupingid . '\}(.*)\{\/ifingrouping\}/isuU'; - $ismember = false; - foreach ($mygroupingslist as $grouping) { - if ($groupingid == $grouping->id || $groupingid == $grouping->idnumber) { - $ismember = true; - break; - } - } - if ($ismember) { // Just remove the tags. - $replace[$key] = '$1'; - } else { // Remove the ifingroup tags and content. - $replace[$key] = ''; + $this->if_tag( + $text, + $replace, + 'ifingrouping', + function ($groupingid) use (&$mygroupingslist, $PAGE, $USER) { + if (!isset($mygroupingslist)) { + $mygroupingslist = $this->getusergroupings($PAGE->course->id, $USER->id); + } + + $ismember = false; + foreach ($mygroupingslist as $grouping) { + if ($groupingid == $grouping->id || $groupingid == $grouping->idnumber) { + $ismember = true; + break; } } - } - } + + return $ismember; + }, + ); + + // Tag: {ifnotingrouping}...{/ifnotingrouping}. + // Description: Display content if the user is NOT a member of any grouping. + // Required Parameters: None. + // Requires content between tags. // Tag: {ifnotingrouping id|idnumber}...{/ifnotingrouping}. // Description: Display content if the user is NOT a member of the specified grouping. // Required Parameters: group id or idnumber. // Requires content between tags. - if (stripos($text, '{ifnotingrouping') !== false) { - if (!isset($mygroupingslist)) { - $mygroupingslist = $this->getusergroupings($PAGE->course->id, $USER->id); - } - $re = '/{ifnotingrouping\s+(.*)\}(.*)\{\/ifnotingrouping\}/isuU'; - $found = preg_match_all($re, $text, $matches); - if ($found > 0) { - foreach ($matches[1] as $groupingid) { - $key = '/{ifnotingrouping\s+' . $groupingid . '\}(.*)\{\/ifnotingrouping\}/isuU'; - $ismember = false; - foreach ($mygroupingslist as $grouping) { - if ($groupingid == $grouping->id || $groupingid == $grouping->idnumber) { - $ismember = true; - break; - } - } - if ($ismember) { // Remove the ifnotingroup tags and content. - $replace[$key] = ''; - } else { // Just remove the tags and keep the content. - $replace[$key] = '$1'; + $this->if_tag( + $text, + $replace, + 'ifnotingrouping', + function ($groupingid) use (&$mygroupingslist, $PAGE, $USER) { + if (!isset($mygroupingslist)) { // Fetch my groups. + $mygroupingslist = $this->getusergroupings($PAGE->course->id, $USER->id); + } + + if (empty($mygroupingslist)) { + return true; + } + + if (empty($groupingid)) { + // My groupings list is not empty, but no grouping id was specified. + return false; + } + + $ismember = false; + foreach ($mygroupingslist as $grouping) { + if ($groupingid == $grouping->id || $groupingid == $grouping->idnumber) { + $ismember = true; + break; } } - } - } + + return !$ismember; + }, + ); // Tag: {iftenant idnumber|tenantid}...{/iftenant}. // Description: Display content only if the user is part of the specified tenant on Moodle Workplace. // Required Parameter: tenant idnumber or tenantid. // Requires content between tags. - if (stripos($text, '{iftenant') !== false) { + if (stripos($text, '{iftenant') !== false && stripos($text, '{/iftenant}') !== false) { if (class_exists('tool_tenant\tenancy')) { // Moodle Workplace. $tenants = \tool_tenant\tenancy::get_tenants(); @@ -4963,20 +5106,15 @@ function ($matches) use ($mycohorts) { $currenttenantidnumber = $tenant->idnumber ? $tenant->idnumber : $tenant->id; } } - $re = '/{iftenant\s+(.*)\}(.*)\{\/iftenant\}/isuU'; - $found = preg_match_all($re, $text, $matches); - if ($found > 0) { - foreach ($matches[1] as $tenantid) { - $key = '/{iftenant\s+' . $tenantid . '\}(.*)\{\/iftenant\}/isuU'; - if ($tenantid == $currenttenantidnumber) { - // Just remove the tags. - $replace[$key] = '$1'; - } else { - // Remove the iftenant strings. - $replace[$key] = ''; - } + + $this->if_tag( + $text, + $replace, + "iftenant", + function ($tenantid) use ($currenttenantidnumber) { + return $tenantid == $currenttenantidnumber; } - } + ); } // Tag: {ifworkplace}...{/ifworkplace}. @@ -4998,105 +5136,78 @@ function ($matches) use ($mycohorts) { // Description: Display content only if user has the role specified by shortrolename in the current context. // Parameters: Short role name. // Requires content between tags. - if (stripos($text, '{ifcustomrole') !== false) { - $re = '/{ifcustomrole\s+(.*)\}(.*)\{\/ifcustomrole\}/isuU'; - $found = preg_match_all($re, $text, $matches); - if ($found > 0) { - $context = $PAGE->context; - if ($context->contextlevel == CONTEXT_COURSE) { - // We are in a course. - $context = \context_course::instance($context->instanceid); - } else if ($context->contextlevel == CONTEXT_MODULE) { - // We are in an activity. - $cm = get_coursemodule_from_id('', $context->instanceid, 0, false, MUST_EXIST); - $context = \context_module::instance($cm->id); - unset($cm); + if (stripos($text, '{ifcustomrole') !== false && stripos($text, '{/ifcustomrole}') !== false) { + $context = $PAGE->context; + if ($context->contextlevel == CONTEXT_COURSE) { + // We are in a course. + $context = \context_course::instance($context->instanceid); + } else if ($context->contextlevel == CONTEXT_MODULE) { + // We are in an activity. + $cm = get_coursemodule_from_id('', $context->instanceid, 0, false, MUST_EXIST); + $context = \context_module::instance($cm->id); + unset($cm); + } + + // Get roles within this context. + $roles = get_user_roles($context, $USER->id, true); + $roles = array_column($roles, 'shortname'); + unset($context); + + $this->if_tag( + $text, + $replace, + 'ifcustomrole', + function ($roleshortname) use ($roles) { + return in_array($roleshortname, $roles); } - - // Get roles within this context. - $roles = get_user_roles($context, $USER->id, true); - $roles = array_column($roles, 'shortname'); - unset($context); - - // Replace all instances of a given ifcustomrole tag. - foreach ($matches[1] as $roleshortname) { - $key = '/{ifcustomrole\s+' . $roleshortname . '\}(.*)\{\/ifcustomrole\}/isuU'; - // We have a role that matches this tag. - if (in_array($roleshortname, $roles)) { - // Just remove the tags. - $replace[$key] = '$1'; - } else { - // Otherwise, remove the ifcustomrole tags and the string inside it. - $replace[$key] = ''; - } - unset($key); - } - } - unset($re); - unset($found); + ); } // Tag: {ifnotcustomrole shortrolename}...{/ifnotcustomrole}. // Description: Display content only if user does NOT have the role specified by shortrolename in the current context. // Required Parameters: Short role name. // Requires content between tags. - if (stripos($text, '{ifnotcustomrole') !== false) { - $re = '/{ifnotcustomrole\s+(.*)\}(.*)\{\/ifnotcustomrole\}/isuU'; - $found = preg_match_all($re, $text, $matches); - if ($found > 0) { - $context = $PAGE->context; - if ($context->contextlevel == CONTEXT_COURSE) { - // We are in a course. - $context = \context_course::instance($context->instanceid); - } else if ($context->contextlevel == CONTEXT_MODULE) { - // We are in an activity. - $cm = get_coursemodule_from_id('', $context->instanceid, 0, false, MUST_EXIST); - $context = \context_module::instance($cm->id); - unset($cm); - } - - // Get roles within this context. - $roles = get_user_roles($context, $USER->id, true); - $roles = array_column($roles, 'shortname'); - unset($context); - - // Replace all instances of a given ifnotcustomrole tag. - foreach ($matches[1] as $roleshortname) { - $key = '/{ifnotcustomrole\s+' . $roleshortname . '\}(.*)\{\/ifnotcustomrole\}/isuU'; - // We do not have a role that matches this tag. - if (!in_array($roleshortname, $roles)) { - // Just remove the tags. - $replace[$key] = '$1'; - } else { - // Otherwise, remove the ifnotcustomrole strings. - $replace[$key] = ''; - } - unset($key); + if (stripos($text, '{ifnotcustomrole') !== false + && stripos($text, '{/ifnotcustomrole}') !== false) { + $context = $PAGE->context; + if ($context->contextlevel == CONTEXT_COURSE) { + // We are in a course. + $context = \context_course::instance($context->instanceid); + } else if ($context->contextlevel == CONTEXT_MODULE) { + // We are in an activity. + $cm = get_coursemodule_from_id('', $context->instanceid, 0, false, MUST_EXIST); + $context = \context_module::instance($cm->id); + unset($cm); + } + + // Get roles within this context. + $roles = get_user_roles($context, $USER->id, true); + $roles = array_column($roles, 'shortname'); + unset($context); + + $this->if_tag( + $text, + $replace, + 'ifcustomrole', + function ($roleshortname) use ($roles) { + return !in_array($roleshortname, $roles); } - } - unset($re); - unset($found); + ); } // Tag: {ifhasarolename roleshortname}...{/ifhasarolename}. // Description: Display content only if user has the role specified by shortrolename ANYWHERE on the site. // Parameters: Short role name. // Requires content between tags. - if (stripos($text, '{ifhasarolename') !== false) { - $re = '/{ifhasarolename\s+(.*)\}(.*)\{\/ifhasarolename\}/isuU'; - $found = preg_match_all($re, $text, $matches); - if ($found > 0) { - foreach ($matches[1] as $roleshortname) { - $key = '/{ifhasarolename\s+' . $roleshortname . '\}(.*)\{\/ifhasarolename\}/isuU'; - if ($this->hasarole($roleshortname, $USER->id)) { - // Just remove the tags. - $replace[$key] = '$1'; - } else { - // Remove the ifhasarolename strings. - $replace[$key] = ''; - } + if (stripos($text, '{ifhasarolename') !== false && stripos($text, '{/ifhasarolename}') !== false) { + $this->if_tag( + $text, + $replace, + 'ifhasarolename', + function ($roleshortname) use ($USER) { + return $this->hasarole($roleshortname, $USER->id); } - } + ); } // Tag: {iftheme themename}...{/iftheme}. @@ -5475,7 +5586,7 @@ function ($matches) { // Parameters: None. // Requires content between tags. if (stripos($text, '{rawurlencode}') !== false) { - // Replace {urlencode} tags and content with encoded content. + // Replace {rawurlencode} tags and content with encoded content. $newtext = preg_replace_callback( '/\{rawurlencode\}(.*)\{\/rawurlencode\}/isuU', function ($matches) { diff --git a/settings/general.php b/settings/general.php index 49ee7bb..a8aac59 100644 --- a/settings/general.php +++ b/settings/general.php @@ -124,6 +124,7 @@ 'message' => get_string('message', 'message'), 'profile' => get_string('profile'), 'phone' => get_string('phone'), + 'mobile' => get_string('phone2'), ]; $setting = new admin_setting_configselect($name, $title, $description, $default, $choices); $settings->add($setting); diff --git a/version.php b/version.php index fea18aa..97ba1c6 100644 --- a/version.php +++ b/version.php @@ -25,8 +25,8 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2025050500; // The current plugin version (Date: YYYYMMDDXX). +$plugin->version = 2025102700; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2014051200; // Requires Moodle version 2.7 or later. $plugin->component = 'filter_filtercodes'; // Full name of the plugin (used for diagnostics). -$plugin->release = '2.7.0'; +$plugin->release = '2.7.2'; $plugin->maturity = MATURITY_STABLE;