From ae2f69a8987a4f4e703248f8d9a0a5b096200d8a Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Wed, 10 Jul 2024 09:49:07 +0800 Subject: [PATCH 1/8] MDL-82427 core_filters: Move filter classes to autoloading --- filter/classes/filter_manager.php | 283 +++++++++ filter/classes/filter_object.php | 97 +++ filter/classes/null_filter_manager.php | 59 ++ .../performance_measuring_filter_manager.php | 76 +++ filter/classes/text_filter.php | 134 ++++ lib/db/legacyclasses.php | 22 + lib/filterlib.php | 574 ------------------ 7 files changed, 671 insertions(+), 574 deletions(-) create mode 100644 filter/classes/filter_manager.php create mode 100644 filter/classes/filter_object.php create mode 100644 filter/classes/null_filter_manager.php create mode 100644 filter/classes/performance_measuring_filter_manager.php create mode 100644 filter/classes/text_filter.php diff --git a/filter/classes/filter_manager.php b/filter/classes/filter_manager.php new file mode 100644 index 00000000000..2bfebe3ddbb --- /dev/null +++ b/filter/classes/filter_manager.php @@ -0,0 +1,283 @@ +. + +/** + * Class to manage the filtering of strings. It is intended that this class is + * only used by weblib.php. Client code should probably be using the + * format_text and format_string functions. + * + * This class is a singleton. + * + * @package core_filters + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class filter_manager { + /** + * @var moodle_text_filter[][] This list of active filters, by context, for filtering content. + * An array contextid => ordered array of filter name => filter objects. + */ + protected $textfilters = array(); + + /** + * @var moodle_text_filter[][] This list of active filters, by context, for filtering strings. + * An array contextid => ordered array of filter name => filter objects. + */ + protected $stringfilters = array(); + + /** @var array Exploded version of $CFG->stringfilters. */ + protected $stringfilternames = array(); + + /** @var filter_manager Holds the singleton instance. */ + protected static $singletoninstance; + + /** + * Constructor. Protected. Use {@link instance()} instead. + */ + protected function __construct() { + $this->stringfilternames = filter_get_string_filters(); + } + + /** + * Factory method. Use this to get the filter manager. + * + * @return filter_manager the singleton instance. + */ + public static function instance() { + global $CFG; + if (is_null(self::$singletoninstance)) { + if (!empty($CFG->perfdebug) and $CFG->perfdebug > 7) { + self::$singletoninstance = new performance_measuring_filter_manager(); + } else { + self::$singletoninstance = new self(); + } + } + return self::$singletoninstance; + } + + /** + * Resets the caches, usually to be called between unit tests + */ + public static function reset_caches() { + if (self::$singletoninstance) { + self::$singletoninstance->unload_all_filters(); + } + self::$singletoninstance = null; + } + + /** + * Unloads all filters and other cached information + */ + protected function unload_all_filters() { + $this->textfilters = array(); + $this->stringfilters = array(); + $this->stringfilternames = array(); + } + + /** + * Load all the filters required by this context. + * + * @param context $context the context. + */ + protected function load_filters($context) { + $filters = filter_get_active_in_context($context); + $this->textfilters[$context->id] = array(); + $this->stringfilters[$context->id] = array(); + foreach ($filters as $filtername => $localconfig) { + $filter = $this->make_filter_object($filtername, $context, $localconfig); + if (is_null($filter)) { + continue; + } + $this->textfilters[$context->id][$filtername] = $filter; + if (in_array($filtername, $this->stringfilternames)) { + $this->stringfilters[$context->id][$filtername] = $filter; + } + } + } + + /** + * Factory method for creating a filter. + * + * @param string $filtername The filter name, for example 'tex'. + * @param context $context context object. + * @param array $localconfig array of local configuration variables for this filter. + * @return ?moodle_text_filter The filter, or null, if this type of filter is + * not recognised or could not be created. + */ + protected function make_filter_object($filtername, $context, $localconfig) { + global $CFG; + + $path = $CFG->dirroot .'/filter/'. $filtername .'/filter.php'; + if (!is_readable($path)) { + return null; + } + include_once($path); + + $filterclassname = 'filter_' . $filtername; + if (class_exists($filterclassname)) { + return new $filterclassname($context, $localconfig); + } + + return null; + } + + /** + * Apply a list of filters to some content. + * @param string $text + * @param moodle_text_filter[] $filterchain array filter name => filter object. + * @param array $options options passed to the filters. + * @param array $skipfilters of filter names. Any filters that should not be applied to this text. + * @return string $text + */ + protected function apply_filter_chain($text, $filterchain, array $options = array(), + array $skipfilters = null) { + if (!isset($options['stage'])) { + $filtermethod = 'filter'; + } else if (in_array($options['stage'], ['pre_format', 'pre_clean', 'post_clean', 'string'], true)) { + $filtermethod = 'filter_stage_' . $options['stage']; + } else { + $filtermethod = 'filter'; + debugging('Invalid filter stage specified in options: ' . $options['stage'], DEBUG_DEVELOPER); + } + if ($text === null || $text === '') { + // Nothing to filter. + return ''; + } + foreach ($filterchain as $filtername => $filter) { + if ($skipfilters !== null && in_array($filtername, $skipfilters)) { + continue; + } + $text = $filter->$filtermethod($text, $options); + } + return $text; + } + + /** + * Get all the filters that apply to a given context for calls to format_text. + * + * @param context $context + * @return moodle_text_filter[] A text filter + */ + protected function get_text_filters($context) { + if (!isset($this->textfilters[$context->id])) { + $this->load_filters($context); + } + return $this->textfilters[$context->id]; + } + + /** + * Get all the filters that apply to a given context for calls to format_string. + * + * @param context $context the context. + * @return moodle_text_filter[] A text filter + */ + protected function get_string_filters($context) { + if (!isset($this->stringfilters[$context->id])) { + $this->load_filters($context); + } + return $this->stringfilters[$context->id]; + } + + /** + * Filter some text + * + * @param string $text The text to filter + * @param context $context the context. + * @param array $options options passed to the filters + * @param array $skipfilters of filter names. Any filters that should not be applied to this text. + * @return string resulting text + */ + public function filter_text($text, $context, array $options = array(), + array $skipfilters = null) { + $text = $this->apply_filter_chain($text, $this->get_text_filters($context), $options, $skipfilters); + if (!isset($options['stage']) || $options['stage'] === 'post_clean') { + // Remove tags for XHTML compatibility after the last filtering stage. + $text = str_replace(array('', ''), '', $text); + } + return $text; + } + + /** + * Filter a piece of string + * + * @param string $string The text to filter + * @param context $context the context. + * @return string resulting string + */ + public function filter_string($string, $context) { + return $this->apply_filter_chain($string, $this->get_string_filters($context), ['stage' => 'string']); + } + + /** + * @deprecated Since Moodle 3.0 MDL-50491. This was used by the old text filtering system, but no more. + */ + public function text_filtering_hash() { + throw new coding_exception('filter_manager::text_filtering_hash() can not be used any more'); + } + + /** + * Setup page with filters requirements and other prepare stuff. + * + * This method is used by {@see format_text()} and {@see format_string()} + * in order to allow filters to setup any page requirement (js, css...) + * or perform any action needed to get them prepared before filtering itself + * happens by calling to each every active setup() method. + * + * Note it's executed for each piece of text filtered, so filter implementations + * are responsible of controlling the cardinality of the executions that may + * be different depending of the stuff to prepare. + * + * @param moodle_page $page the page we are going to add requirements to. + * @param context $context the context which contents are going to be filtered. + * @since Moodle 2.3 + */ + public function setup_page_for_filters($page, $context) { + $filters = $this->get_text_filters($context); + foreach ($filters as $filter) { + $filter->setup($page, $context); + } + } + + /** + * Setup the page for globally available filters. + * + * This helps setting up the page for filters which may be applied to + * the page, even if they do not belong to the current context, or are + * not yet visible because the content is lazily added (ajax). This method + * always uses to the system context which determines the globally + * available filters. + * + * This should only ever be called once per request. + * + * @param moodle_page $page The page. + * @since Moodle 3.2 + */ + public function setup_page_for_globally_available_filters($page) { + $context = context_system::instance(); + $filterdata = filter_get_globally_enabled_filters_with_config(); + foreach ($filterdata as $name => $config) { + if (isset($this->textfilters[$context->id][$name])) { + $filter = $this->textfilters[$context->id][$name]; + } else { + $filter = $this->make_filter_object($name, $context, $config); + if (is_null($filter)) { + continue; + } + } + $filter->setup($page, $context); + } + } +} diff --git a/filter/classes/filter_object.php b/filter/classes/filter_object.php new file mode 100644 index 00000000000..08bbe85e1df --- /dev/null +++ b/filter/classes/filter_object.php @@ -0,0 +1,97 @@ +. + +/** + * This is just a little object to define a phrase and some instructions + * for how to process it. Filters can create an array of these to pass + * to the @{link filter_phrases()} function below. + * + * Note that although the fields here are public, you almost certainly should + * never use that. All that is supported is contructing new instances of this + * class, and then passing an array of them to filter_phrases. + * + * @package core_filters + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class filterobject { + /** @var string this is the phrase that should be matched. */ + public $phrase; + + /** @var bool whether to match complete words. If true, 'T' won't be matched in 'Tim'. */ + public $fullmatch; + + /** @var bool whether the match needs to be case sensitive. */ + public $casesensitive; + + /** @var string HTML to insert before any match. */ + public $hreftagbegin; + /** @var string HTML to insert after any match. */ + public $hreftagend; + + /** @var null|string replacement text to go inside begin and end. If not set, + * the body of the replacement will be the original phrase. + */ + public $replacementphrase; + + /** @var null|string once initialised, holds the regexp for matching this phrase. */ + public $workregexp = null; + + /** @var null|string once initialised, holds the mangled HTML to replace the regexp with. */ + public $workreplacementphrase = null; + + /** @var null|callable hold a replacement function to be called. */ + public $replacementcallback; + + /** @var null|array data to be passed to $replacementcallback. */ + public $replacementcallbackdata; + + /** + * Constructor. + * + * @param string $phrase this is the phrase that should be matched. + * @param string $hreftagbegin HTML to insert before any match. Default ''. + * @param string $hreftagend HTML to insert after any match. Default ''. + * @param bool $casesensitive whether the match needs to be case sensitive + * @param bool $fullmatch whether to match complete words. If true, 'T' won't be matched in 'Tim'. + * @param mixed $replacementphrase replacement text to go inside begin and end. If not set, + * the body of the replacement will be the original phrase. + * @param callback $replacementcallback if set, then this will be called just before + * $hreftagbegin, $hreftagend and $replacementphrase are needed, so they can be computed only if required. + * The call made is + * list($linkobject->hreftagbegin, $linkobject->hreftagend, $linkobject->replacementphrase) = + * call_user_func_array($linkobject->replacementcallback, $linkobject->replacementcallbackdata); + * so the return should be an array [$hreftagbegin, $hreftagend, $replacementphrase], the last of which may be null. + * @param array $replacementcallbackdata data to be passed to $replacementcallback (optional). + */ + public function __construct($phrase, $hreftagbegin = '', + $hreftagend = '', + $casesensitive = false, + $fullmatch = false, + $replacementphrase = null, + $replacementcallback = null, + array $replacementcallbackdata = null) { + + $this->phrase = $phrase; + $this->hreftagbegin = $hreftagbegin; + $this->hreftagend = $hreftagend; + $this->casesensitive = !empty($casesensitive); + $this->fullmatch = !empty($fullmatch); + $this->replacementphrase = $replacementphrase; + $this->replacementcallback = $replacementcallback; + $this->replacementcallbackdata = $replacementcallbackdata; + } +} diff --git a/filter/classes/null_filter_manager.php b/filter/classes/null_filter_manager.php new file mode 100644 index 00000000000..0842e5f60ca --- /dev/null +++ b/filter/classes/null_filter_manager.php @@ -0,0 +1,59 @@ +. + +/** + * Filter manager subclass that does nothing. Having this simplifies the logic + * of format_text, etc. + * + * @package core_filters + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class null_filter_manager { + /** + * As for the equivalent {@link filter_manager} method. + * + * @param string $text The text to filter + * @param context $context not used. + * @param array $options not used + * @param array $skipfilters not used + * @return string resulting text. + */ + public function filter_text($text, $context, array $options = array(), + array $skipfilters = null) { + return $text; + } + + /** + * As for the equivalent {@link filter_manager} method. + * + * @param string $string The text to filter + * @param context $context not used. + * @return string resulting string + */ + public function filter_string($string, $context) { + return $string; + } + + /** + * As for the equivalent {@link filter_manager} method. + * + * @deprecated Since Moodle 3.0 MDL-50491. + */ + public function text_filtering_hash() { + throw new coding_exception('filter_manager::text_filtering_hash() can not be used any more'); + } +} diff --git a/filter/classes/performance_measuring_filter_manager.php b/filter/classes/performance_measuring_filter_manager.php new file mode 100644 index 00000000000..c24107c8535 --- /dev/null +++ b/filter/classes/performance_measuring_filter_manager.php @@ -0,0 +1,76 @@ +. + +/** + * Filter manager subclass that tracks how much work it does. + * + * @package core_filters + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class performance_measuring_filter_manager extends filter_manager { + /** @var int number of filter objects created. */ + protected $filterscreated = 0; + + /** @var int number of calls to filter_text. */ + protected $textsfiltered = 0; + + /** @var int number of calls to filter_string. */ + protected $stringsfiltered = 0; + + protected function unload_all_filters() { + parent::unload_all_filters(); + $this->filterscreated = 0; + $this->textsfiltered = 0; + $this->stringsfiltered = 0; + } + + protected function make_filter_object($filtername, $context, $localconfig) { + $this->filterscreated++; + return parent::make_filter_object($filtername, $context, $localconfig); + } + + public function filter_text($text, $context, array $options = array(), + array $skipfilters = null) { + if (!isset($options['stage']) || $options['stage'] === 'post_clean') { + $this->textsfiltered++; + } + return parent::filter_text($text, $context, $options, $skipfilters); + } + + public function filter_string($string, $context) { + $this->stringsfiltered++; + return parent::filter_string($string, $context); + } + + /** + * Return performance information, in the form required by {@link get_performance_info()}. + * @return array the performance info. + */ + public function get_performance_summary() { + return array(array( + 'contextswithfilters' => count($this->textfilters), + 'filterscreated' => $this->filterscreated, + 'textsfiltered' => $this->textsfiltered, + 'stringsfiltered' => $this->stringsfiltered, + ), array( + 'contextswithfilters' => 'Contexts for which filters were loaded', + 'filterscreated' => 'Filters created', + 'textsfiltered' => 'Pieces of content filtered', + 'stringsfiltered' => 'Strings filtered', + )); + } +} diff --git a/filter/classes/text_filter.php b/filter/classes/text_filter.php new file mode 100644 index 00000000000..aa4594eb249 --- /dev/null +++ b/filter/classes/text_filter.php @@ -0,0 +1,134 @@ +. + +/** + * Base class for text filters. You just need to override this class and + * implement the filter method. + * + * @package core_filters + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class moodle_text_filter { + /** @var context The context we are in. */ + protected $context; + + /** @var array Any local configuration for this filter in this context. */ + protected $localconfig; + + /** + * Set any context-specific configuration for this filter. + * + * @param context $context The current context. + * @param array $localconfig Any context-specific configuration for this filter. + */ + public function __construct($context, array $localconfig) { + $this->context = $context; + $this->localconfig = $localconfig; + } + + /** + * @deprecated Since Moodle 3.0 MDL-50491. This was used by the old text filtering system, but no more. + */ + public function hash() { + throw new coding_exception('moodle_text_filter::hash() can not be used any more'); + } + + /** + * Setup page with filter requirements and other prepare stuff. + * + * Override this method if the filter needs to setup page + * requirements or needs other stuff to be executed. + * + * Note this method is invoked from {@see setup_page_for_filters()} + * for each piece of text being filtered, so it is responsible + * for controlling its own execution cardinality. + * + * @param moodle_page $page the page we are going to add requirements to. + * @param context $context the context which contents are going to be filtered. + * @since Moodle 2.3 + */ + public function setup($page, $context) { + // Override me, if needed. + } + + /** + * Override this function to actually implement the filtering. + * + * Filter developers must make sure that filtering done after text cleaning + * does not introduce security vulnerabilities. + * + * @param string $text some HTML content to process. + * @param array $options options passed to the filters + * @return string the HTML content after the filtering has been applied. + */ + abstract public function filter($text, array $options = array()); + + /** + * Filter text before changing format to HTML. + * + * @param string $text + * @param array $options + * @return string + */ + public function filter_stage_pre_format(string $text, array $options): string { + // NOTE: override if necessary. + return $text; + } + + /** + * Filter HTML text before sanitising text. + * + * NOTE: this is called even if $options['noclean'] is true and text is not cleaned. + * + * @param string $text + * @param array $options + * @return string + */ + public function filter_stage_pre_clean(string $text, array $options): string { + // NOTE: override if necessary. + return $text; + } + + /** + * Filter HTML text at the very end after text is sanitised. + * + * NOTE: this is called even if $options['noclean'] is true and text is not cleaned. + * + * @param string $text + * @param array $options + * @return string + */ + public function filter_stage_post_clean(string $text, array $options): string { + // NOTE: override if necessary. + return $this->filter($text, $options); + } + + /** + * Filter simple text coming from format_string(). + * + * Note that unless $CFG->formatstringstriptags is disabled + * HTML tags are not expected in returned value. + * + * @param string $text + * @param array $options + * @return string + */ + public function filter_stage_string(string $text, array $options): string { + // NOTE: override if necessary. + return $this->filter($text, $options); + } +} diff --git a/lib/db/legacyclasses.php b/lib/db/legacyclasses.php index 9a244e4b5a3..4a381b61ae6 100644 --- a/lib/db/legacyclasses.php +++ b/lib/db/legacyclasses.php @@ -158,4 +158,26 @@ $legacyclasses = [ \progress_trace::class => 'output/progress_trace.php', \progress_trace_buffer::class => 'output/progress_trace/progress_trace_buffer.php', \text_progress_trace::class => 'output/progress_trace/text_progress_trace.php', + + // Filters subsystem. + \filter_manager::class => [ + 'core_filters', + 'filter_manager.php', + ], + \filterobject::class => [ + 'core_filters', + 'filter_object.php', + ], + \moodle_text_filter::class => [ + 'core_filters', + 'text_filter.php', + ], + \null_filter_manager::class => [ + 'core_filters', + 'null_filter_manager.php', + ], + \performance_measuring_filter_manager::class => [ + 'core_filters', + 'performance_measuring_filter_manager.php', + ], ]; diff --git a/lib/filterlib.php b/lib/filterlib.php index 74770ddb34d..18686c4792f 100644 --- a/lib/filterlib.php +++ b/lib/filterlib.php @@ -22,8 +22,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -defined('MOODLE_INTERNAL') || die(); - /** The states a filter can be in, stored in the filter_active table. */ define('TEXTFILTER_ON', 1); /** The states a filter can be in, stored in the filter_active table. */ @@ -41,578 +39,6 @@ define('TEXTFILTER_DISABLED', -9999); define('TEXTFILTER_EXCL_SEPARATOR', chr(0x1F) . '%' . chr(0x1F)); -/** - * Class to manage the filtering of strings. It is intended that this class is - * only used by weblib.php. Client code should probably be using the - * format_text and format_string functions. - * - * This class is a singleton. - * - * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class filter_manager { - /** - * @var moodle_text_filter[][] This list of active filters, by context, for filtering content. - * An array contextid => ordered array of filter name => filter objects. - */ - protected $textfilters = array(); - - /** - * @var moodle_text_filter[][] This list of active filters, by context, for filtering strings. - * An array contextid => ordered array of filter name => filter objects. - */ - protected $stringfilters = array(); - - /** @var array Exploded version of $CFG->stringfilters. */ - protected $stringfilternames = array(); - - /** @var filter_manager Holds the singleton instance. */ - protected static $singletoninstance; - - /** - * Constructor. Protected. Use {@link instance()} instead. - */ - protected function __construct() { - $this->stringfilternames = filter_get_string_filters(); - } - - /** - * Factory method. Use this to get the filter manager. - * - * @return filter_manager the singleton instance. - */ - public static function instance() { - global $CFG; - if (is_null(self::$singletoninstance)) { - if (!empty($CFG->perfdebug) and $CFG->perfdebug > 7) { - self::$singletoninstance = new performance_measuring_filter_manager(); - } else { - self::$singletoninstance = new self(); - } - } - return self::$singletoninstance; - } - - /** - * Resets the caches, usually to be called between unit tests - */ - public static function reset_caches() { - if (self::$singletoninstance) { - self::$singletoninstance->unload_all_filters(); - } - self::$singletoninstance = null; - } - - /** - * Unloads all filters and other cached information - */ - protected function unload_all_filters() { - $this->textfilters = array(); - $this->stringfilters = array(); - $this->stringfilternames = array(); - } - - /** - * Load all the filters required by this context. - * - * @param context $context the context. - */ - protected function load_filters($context) { - $filters = filter_get_active_in_context($context); - $this->textfilters[$context->id] = array(); - $this->stringfilters[$context->id] = array(); - foreach ($filters as $filtername => $localconfig) { - $filter = $this->make_filter_object($filtername, $context, $localconfig); - if (is_null($filter)) { - continue; - } - $this->textfilters[$context->id][$filtername] = $filter; - if (in_array($filtername, $this->stringfilternames)) { - $this->stringfilters[$context->id][$filtername] = $filter; - } - } - } - - /** - * Factory method for creating a filter. - * - * @param string $filtername The filter name, for example 'tex'. - * @param context $context context object. - * @param array $localconfig array of local configuration variables for this filter. - * @return ?moodle_text_filter The filter, or null, if this type of filter is - * not recognised or could not be created. - */ - protected function make_filter_object($filtername, $context, $localconfig) { - global $CFG; - $path = $CFG->dirroot .'/filter/'. $filtername .'/filter.php'; - if (!is_readable($path)) { - return null; - } - include_once($path); - - $filterclassname = 'filter_' . $filtername; - if (class_exists($filterclassname)) { - return new $filterclassname($context, $localconfig); - } - - return null; - } - - /** - * Apply a list of filters to some content. - * @param string $text - * @param moodle_text_filter[] $filterchain array filter name => filter object. - * @param array $options options passed to the filters. - * @param array $skipfilters of filter names. Any filters that should not be applied to this text. - * @return string $text - */ - protected function apply_filter_chain($text, $filterchain, array $options = array(), - array $skipfilters = null) { - if (!isset($options['stage'])) { - $filtermethod = 'filter'; - } else if (in_array($options['stage'], ['pre_format', 'pre_clean', 'post_clean', 'string'], true)) { - $filtermethod = 'filter_stage_' . $options['stage']; - } else { - $filtermethod = 'filter'; - debugging('Invalid filter stage specified in options: ' . $options['stage'], DEBUG_DEVELOPER); - } - if ($text === null || $text === '') { - // Nothing to filter. - return ''; - } - foreach ($filterchain as $filtername => $filter) { - if ($skipfilters !== null && in_array($filtername, $skipfilters)) { - continue; - } - $text = $filter->$filtermethod($text, $options); - } - return $text; - } - - /** - * Get all the filters that apply to a given context for calls to format_text. - * - * @param context $context - * @return moodle_text_filter[] A text filter - */ - protected function get_text_filters($context) { - if (!isset($this->textfilters[$context->id])) { - $this->load_filters($context); - } - return $this->textfilters[$context->id]; - } - - /** - * Get all the filters that apply to a given context for calls to format_string. - * - * @param context $context the context. - * @return moodle_text_filter[] A text filter - */ - protected function get_string_filters($context) { - if (!isset($this->stringfilters[$context->id])) { - $this->load_filters($context); - } - return $this->stringfilters[$context->id]; - } - - /** - * Filter some text - * - * @param string $text The text to filter - * @param context $context the context. - * @param array $options options passed to the filters - * @param array $skipfilters of filter names. Any filters that should not be applied to this text. - * @return string resulting text - */ - public function filter_text($text, $context, array $options = array(), - array $skipfilters = null) { - $text = $this->apply_filter_chain($text, $this->get_text_filters($context), $options, $skipfilters); - if (!isset($options['stage']) || $options['stage'] === 'post_clean') { - // Remove tags for XHTML compatibility after the last filtering stage. - $text = str_replace(array('', ''), '', $text); - } - return $text; - } - - /** - * Filter a piece of string - * - * @param string $string The text to filter - * @param context $context the context. - * @return string resulting string - */ - public function filter_string($string, $context) { - return $this->apply_filter_chain($string, $this->get_string_filters($context), ['stage' => 'string']); - } - - /** - * @deprecated Since Moodle 3.0 MDL-50491. This was used by the old text filtering system, but no more. - */ - public function text_filtering_hash() { - throw new coding_exception('filter_manager::text_filtering_hash() can not be used any more'); - } - - /** - * Setup page with filters requirements and other prepare stuff. - * - * This method is used by {@see format_text()} and {@see format_string()} - * in order to allow filters to setup any page requirement (js, css...) - * or perform any action needed to get them prepared before filtering itself - * happens by calling to each every active setup() method. - * - * Note it's executed for each piece of text filtered, so filter implementations - * are responsible of controlling the cardinality of the executions that may - * be different depending of the stuff to prepare. - * - * @param moodle_page $page the page we are going to add requirements to. - * @param context $context the context which contents are going to be filtered. - * @since Moodle 2.3 - */ - public function setup_page_for_filters($page, $context) { - $filters = $this->get_text_filters($context); - foreach ($filters as $filter) { - $filter->setup($page, $context); - } - } - - /** - * Setup the page for globally available filters. - * - * This helps setting up the page for filters which may be applied to - * the page, even if they do not belong to the current context, or are - * not yet visible because the content is lazily added (ajax). This method - * always uses to the system context which determines the globally - * available filters. - * - * This should only ever be called once per request. - * - * @param moodle_page $page The page. - * @since Moodle 3.2 - */ - public function setup_page_for_globally_available_filters($page) { - $context = context_system::instance(); - $filterdata = filter_get_globally_enabled_filters_with_config(); - foreach ($filterdata as $name => $config) { - if (isset($this->textfilters[$context->id][$name])) { - $filter = $this->textfilters[$context->id][$name]; - } else { - $filter = $this->make_filter_object($name, $context, $config); - if (is_null($filter)) { - continue; - } - } - $filter->setup($page, $context); - } - } -} - - -/** - * Filter manager subclass that does nothing. Having this simplifies the logic - * of format_text, etc. - * - * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class null_filter_manager { - /** - * As for the equivalent {@link filter_manager} method. - * - * @param string $text The text to filter - * @param context $context not used. - * @param array $options not used - * @param array $skipfilters not used - * @return string resulting text. - */ - public function filter_text($text, $context, array $options = array(), - array $skipfilters = null) { - return $text; - } - - /** - * As for the equivalent {@link filter_manager} method. - * - * @param string $string The text to filter - * @param context $context not used. - * @return string resulting string - */ - public function filter_string($string, $context) { - return $string; - } - - /** - * As for the equivalent {@link filter_manager} method. - * - * @deprecated Since Moodle 3.0 MDL-50491. - */ - public function text_filtering_hash() { - throw new coding_exception('filter_manager::text_filtering_hash() can not be used any more'); - } -} - - -/** - * Filter manager subclass that tracks how much work it does. - * - * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class performance_measuring_filter_manager extends filter_manager { - /** @var int number of filter objects created. */ - protected $filterscreated = 0; - - /** @var int number of calls to filter_text. */ - protected $textsfiltered = 0; - - /** @var int number of calls to filter_string. */ - protected $stringsfiltered = 0; - - protected function unload_all_filters() { - parent::unload_all_filters(); - $this->filterscreated = 0; - $this->textsfiltered = 0; - $this->stringsfiltered = 0; - } - - protected function make_filter_object($filtername, $context, $localconfig) { - $this->filterscreated++; - return parent::make_filter_object($filtername, $context, $localconfig); - } - - public function filter_text($text, $context, array $options = array(), - array $skipfilters = null) { - if (!isset($options['stage']) || $options['stage'] === 'post_clean') { - $this->textsfiltered++; - } - return parent::filter_text($text, $context, $options, $skipfilters); - } - - public function filter_string($string, $context) { - $this->stringsfiltered++; - return parent::filter_string($string, $context); - } - - /** - * Return performance information, in the form required by {@link get_performance_info()}. - * @return array the performance info. - */ - public function get_performance_summary() { - return array(array( - 'contextswithfilters' => count($this->textfilters), - 'filterscreated' => $this->filterscreated, - 'textsfiltered' => $this->textsfiltered, - 'stringsfiltered' => $this->stringsfiltered, - ), array( - 'contextswithfilters' => 'Contexts for which filters were loaded', - 'filterscreated' => 'Filters created', - 'textsfiltered' => 'Pieces of content filtered', - 'stringsfiltered' => 'Strings filtered', - )); - } -} - - -/** - * Base class for text filters. You just need to override this class and - * implement the filter method. - * - * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -abstract class moodle_text_filter { - /** @var context The context we are in. */ - protected $context; - - /** @var array Any local configuration for this filter in this context. */ - protected $localconfig; - - /** - * Set any context-specific configuration for this filter. - * - * @param context $context The current context. - * @param array $localconfig Any context-specific configuration for this filter. - */ - public function __construct($context, array $localconfig) { - $this->context = $context; - $this->localconfig = $localconfig; - } - - /** - * @deprecated Since Moodle 3.0 MDL-50491. This was used by the old text filtering system, but no more. - */ - public function hash() { - throw new coding_exception('moodle_text_filter::hash() can not be used any more'); - } - - /** - * Setup page with filter requirements and other prepare stuff. - * - * Override this method if the filter needs to setup page - * requirements or needs other stuff to be executed. - * - * Note this method is invoked from {@see setup_page_for_filters()} - * for each piece of text being filtered, so it is responsible - * for controlling its own execution cardinality. - * - * @param moodle_page $page the page we are going to add requirements to. - * @param context $context the context which contents are going to be filtered. - * @since Moodle 2.3 - */ - public function setup($page, $context) { - // Override me, if needed. - } - - /** - * Override this function to actually implement the filtering. - * - * Filter developers must make sure that filtering done after text cleaning - * does not introduce security vulnerabilities. - * - * @param string $text some HTML content to process. - * @param array $options options passed to the filters - * @return string the HTML content after the filtering has been applied. - */ - abstract public function filter($text, array $options = array()); - - /** - * Filter text before changing format to HTML. - * - * @param string $text - * @param array $options - * @return string - */ - public function filter_stage_pre_format(string $text, array $options): string { - // NOTE: override if necessary. - return $text; - } - - /** - * Filter HTML text before sanitising text. - * - * NOTE: this is called even if $options['noclean'] is true and text is not cleaned. - * - * @param string $text - * @param array $options - * @return string - */ - public function filter_stage_pre_clean(string $text, array $options): string { - // NOTE: override if necessary. - return $text; - } - - /** - * Filter HTML text at the very end after text is sanitised. - * - * NOTE: this is called even if $options['noclean'] is true and text is not cleaned. - * - * @param string $text - * @param array $options - * @return string - */ - public function filter_stage_post_clean(string $text, array $options): string { - // NOTE: override if necessary. - return $this->filter($text, $options); - } - - /** - * Filter simple text coming from format_string(). - * - * Note that unless $CFG->formatstringstriptags is disabled - * HTML tags are not expected in returned value. - * - * @param string $text - * @param array $options - * @return string - */ - public function filter_stage_string(string $text, array $options): string { - // NOTE: override if necessary. - return $this->filter($text, $options); - } -} - - -/** - * This is just a little object to define a phrase and some instructions - * for how to process it. Filters can create an array of these to pass - * to the @{link filter_phrases()} function below. - * - * Note that although the fields here are public, you almost certainly should - * never use that. All that is supported is contructing new instances of this - * class, and then passing an array of them to filter_phrases. - * - * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class filterobject { - /** @var string this is the phrase that should be matched. */ - public $phrase; - - /** @var bool whether to match complete words. If true, 'T' won't be matched in 'Tim'. */ - public $fullmatch; - - /** @var bool whether the match needs to be case sensitive. */ - public $casesensitive; - - /** @var string HTML to insert before any match. */ - public $hreftagbegin; - /** @var string HTML to insert after any match. */ - public $hreftagend; - - /** @var null|string replacement text to go inside begin and end. If not set, - * the body of the replacement will be the original phrase. - */ - public $replacementphrase; - - /** @var null|string once initialised, holds the regexp for matching this phrase. */ - public $workregexp = null; - - /** @var null|string once initialised, holds the mangled HTML to replace the regexp with. */ - public $workreplacementphrase = null; - - /** @var null|callable hold a replacement function to be called. */ - public $replacementcallback; - - /** @var null|array data to be passed to $replacementcallback. */ - public $replacementcallbackdata; - - /** - * Constructor. - * - * @param string $phrase this is the phrase that should be matched. - * @param string $hreftagbegin HTML to insert before any match. Default ''. - * @param string $hreftagend HTML to insert after any match. Default ''. - * @param bool $casesensitive whether the match needs to be case sensitive - * @param bool $fullmatch whether to match complete words. If true, 'T' won't be matched in 'Tim'. - * @param mixed $replacementphrase replacement text to go inside begin and end. If not set, - * the body of the replacement will be the original phrase. - * @param callback $replacementcallback if set, then this will be called just before - * $hreftagbegin, $hreftagend and $replacementphrase are needed, so they can be computed only if required. - * The call made is - * list($linkobject->hreftagbegin, $linkobject->hreftagend, $linkobject->replacementphrase) = - * call_user_func_array($linkobject->replacementcallback, $linkobject->replacementcallbackdata); - * so the return should be an array [$hreftagbegin, $hreftagend, $replacementphrase], the last of which may be null. - * @param array $replacementcallbackdata data to be passed to $replacementcallback (optional). - */ - public function __construct($phrase, $hreftagbegin = '', - $hreftagend = '', - $casesensitive = false, - $fullmatch = false, - $replacementphrase = null, - $replacementcallback = null, - array $replacementcallbackdata = null) { - - $this->phrase = $phrase; - $this->hreftagbegin = $hreftagbegin; - $this->hreftagend = $hreftagend; - $this->casesensitive = !empty($casesensitive); - $this->fullmatch = !empty($fullmatch); - $this->replacementphrase = $replacementphrase; - $this->replacementcallback = $replacementcallback; - $this->replacementcallbackdata = $replacementcallbackdata; - } -} - /** * Look up the name of this filter * From 16456220cd7457d1c2db373559827f55b382a832 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Wed, 10 Jul 2024 10:04:16 +0800 Subject: [PATCH 2/8] MDL-82427 core: Update namespace of core_filters classes --- filter/classes/filter_manager.php | 18 +++- filter/classes/filter_object.php | 9 +- filter/classes/form/local_settings_form.php | 91 +++++++++++++++++++ filter/classes/null_filter_manager.php | 10 ++ .../performance_measuring_filter_manager.php | 7 ++ filter/classes/text_filter.php | 13 ++- filter/local_settings_form.php | 69 -------------- filter/manage.php | 1 - lib/db/legacyclasses.php | 4 + 9 files changed, 147 insertions(+), 75 deletions(-) create mode 100644 filter/classes/form/local_settings_form.php diff --git a/filter/classes/filter_manager.php b/filter/classes/filter_manager.php index 2bfebe3ddbb..152f2c1349d 100644 --- a/filter/classes/filter_manager.php +++ b/filter/classes/filter_manager.php @@ -14,6 +14,13 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace core_filters; + +use core\context; +use core\context\system as context_system; +use core\exception\coding_exception; +use moodle_page; + /** * Class to manage the filtering of strings. It is intended that this class is * only used by weblib.php. Client code should probably be using the @@ -27,13 +34,13 @@ */ class filter_manager { /** - * @var moodle_text_filter[][] This list of active filters, by context, for filtering content. + * @var text_filter[][] This list of active filters, by context, for filtering content. * An array contextid => ordered array of filter name => filter objects. */ protected $textfilters = array(); /** - * @var moodle_text_filter[][] This list of active filters, by context, for filtering strings. + * @var text_filter[][] This list of active filters, by context, for filtering strings. * An array contextid => ordered array of filter name => filter objects. */ protected $stringfilters = array(); @@ -114,7 +121,7 @@ class filter_manager { * @param string $filtername The filter name, for example 'tex'. * @param context $context context object. * @param array $localconfig array of local configuration variables for this filter. - * @return ?moodle_text_filter The filter, or null, if this type of filter is + * @return ?text_filter The filter, or null, if this type of filter is * not recognised or could not be created. */ protected function make_filter_object($filtername, $context, $localconfig) { @@ -281,3 +288,8 @@ class filter_manager { } } } + +// Alias this class to the old name. +// This file will be autoloaded by the legacyclasses autoload system. +// In future all uses of this class will be corrected and the legacy references will be removed. +class_alias(filter_manager::class, \filter_manager::class); diff --git a/filter/classes/filter_object.php b/filter/classes/filter_object.php index 08bbe85e1df..a1c2166ba3d 100644 --- a/filter/classes/filter_object.php +++ b/filter/classes/filter_object.php @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace core_filters; + /** * This is just a little object to define a phrase and some instructions * for how to process it. Filters can create an array of these to pass @@ -27,7 +29,7 @@ * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class filterobject { +class filter_object { /** @var string this is the phrase that should be matched. */ public $phrase; @@ -95,3 +97,8 @@ class filterobject { $this->replacementcallbackdata = $replacementcallbackdata; } } + +// Alias this class to the old name. +// This file will be autoloaded by the legacyclasses autoload system. +// In future all uses of this class will be corrected and the legacy references will be removed. +class_alias(filter_object::class, \filterobject::class); diff --git a/filter/classes/form/local_settings_form.php b/filter/classes/form/local_settings_form.php new file mode 100644 index 00000000000..c736430d71d --- /dev/null +++ b/filter/classes/form/local_settings_form.php @@ -0,0 +1,91 @@ +. + +namespace core_filters; + +use moodleform; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/formslib.php'); + +/** + * A Moodle form base class for editing local filter settings. + * + * @copyright Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @package core_filters + */ +abstract class local_settings_form extends moodleform { + /** @var string The filter to manage */ + protected $filter; + + /** @var \core\context The context */ + protected $context; + + public function __construct($submiturl, $filter, $context) { + $this->filter = $filter; + $this->context = $context; + parent::__construct($submiturl); + } + + #[\Override] + public function definition() { + $mform = $this->_form; + + $this->definition_inner($mform); + + $mform->addElement('hidden', 'contextid'); + $mform->setType('contextid', PARAM_INT); + $mform->setDefault('contextid', $this->context->id); + + $mform->addElement('hidden', 'filter'); + $mform->setType('filter', PARAM_SAFEPATH); + $mform->setDefault('filter', $this->filter); + + $this->add_action_buttons(); + } + + /** + * Override this method to add your form controls. + * + * @param $mform the form we are building. $this->_form, but passed in for convenience. + */ + abstract protected function definition_inner($mform); + + /** + * Override this method to save the settings to the database. + * + * The default implementation will probably be sufficient for most simple cases. + * + * @param object $data the form data that was submitted. + */ + public function save_changes($data) { + $data = (array) $data; + unset($data['filter']); + unset($data['contextid']); + foreach ($data as $name => $value) { + if ($value !== '') { + filter_set_local_config($this->filter, $this->context->id, $name, $value); + } else { + filter_unset_local_config($this->filter, $this->context->id, $name); + } + } + } +} + +class_alias(local_settings_form::class, \filter_local_settings_form::class); diff --git a/filter/classes/null_filter_manager.php b/filter/classes/null_filter_manager.php index 0842e5f60ca..6accad67def 100644 --- a/filter/classes/null_filter_manager.php +++ b/filter/classes/null_filter_manager.php @@ -14,6 +14,11 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace core_filters; + +use core\context; +use core\exception\coding_exception; + /** * Filter manager subclass that does nothing. Having this simplifies the logic * of format_text, etc. @@ -57,3 +62,8 @@ class null_filter_manager { throw new coding_exception('filter_manager::text_filtering_hash() can not be used any more'); } } + +// Alias this class to the old name. +// This file will be autoloaded by the legacyclasses autoload system. +// In future all uses of this class will be corrected and the legacy references will be removed. +class_alias(null_filter_manager::class, \null_filter_manager::class); diff --git a/filter/classes/performance_measuring_filter_manager.php b/filter/classes/performance_measuring_filter_manager.php index c24107c8535..12dc8969306 100644 --- a/filter/classes/performance_measuring_filter_manager.php +++ b/filter/classes/performance_measuring_filter_manager.php @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace core_filters; + /** * Filter manager subclass that tracks how much work it does. * @@ -74,3 +76,8 @@ class performance_measuring_filter_manager extends filter_manager { )); } } + +// Alias this class to the old name. +// This file will be autoloaded by the legacyclasses autoload system. +// In future all uses of this class will be corrected and the legacy references will be removed. +class_alias(performance_measuring_filter_manager::class, \performance_measuring_filter_manager::class); diff --git a/filter/classes/text_filter.php b/filter/classes/text_filter.php index aa4594eb249..ab08c9e660d 100644 --- a/filter/classes/text_filter.php +++ b/filter/classes/text_filter.php @@ -14,6 +14,12 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace core_filters; + +use core\context; +use core\exception\coding_exception; +use moodle_page; + /** * Base class for text filters. You just need to override this class and * implement the filter method. @@ -22,7 +28,7 @@ * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -abstract class moodle_text_filter { +abstract class text_filter { /** @var context The context we are in. */ protected $context; @@ -132,3 +138,8 @@ abstract class moodle_text_filter { return $this->filter($text, $options); } } + +// Alias this class to the old name. +// This file will be autoloaded by the legacyclasses autoload system. +// In future all uses of this class will be corrected and the legacy references will be removed. +class_alias(text_filter::class, \moodle_text_filter::class); diff --git a/filter/local_settings_form.php b/filter/local_settings_form.php index fce01a19bb2..9b70a943090 100644 --- a/filter/local_settings_form.php +++ b/filter/local_settings_form.php @@ -14,72 +14,3 @@ // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . - -/** - * A Moodle form base class for editing local filter settings. - * - * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package core - * @subpackage filter - */ -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->libdir . '/formslib.php'); - -abstract class filter_local_settings_form extends moodleform { - protected $filter; - protected $context; - - public function __construct($submiturl, $filter, $context) { - $this->filter = $filter; - $this->context = $context; - parent::__construct($submiturl); - } - - /** - * Build the form definition. Rather than overriding this method, you - * should probably override definition_inner instead. - * - * This method adds the necessary hidden fields and submit buttons, - * and calls definition_inner to insert the custom controls in the appropriate place. - */ - public function definition() { - $mform = $this->_form; - - $this->definition_inner($mform); - - $mform->addElement('hidden', 'contextid'); - $mform->setType('contextid', PARAM_INT); - $mform->setDefault('contextid', $this->context->id); - - $mform->addElement('hidden', 'filter'); - $mform->setType('filter', PARAM_SAFEPATH); - $mform->setDefault('filter', $this->filter); - - $this->add_action_buttons(); - } - - /** - * Override this method to add your form controls. - * @param $mform the form we are building. $this->_form, but passed in for convenience. - */ - abstract protected function definition_inner($mform); - - /** - * Override this method to save the settings to the database. The default - * implementation will probably be sufficient for most simple cases. - * @param object $data the form data that was submitted. - */ - public function save_changes($data) { - $data = (array) $data; - unset($data['filter']); - unset($data['contextid']); - foreach ($data as $name => $value) { - if ($value !== '') { - filter_set_local_config($this->filter, $this->context->id, $name, $value); - } else { - filter_unset_local_config($this->filter, $this->context->id, $name); - } - } - } -} diff --git a/filter/manage.php b/filter/manage.php index f10685fd3af..024b19ff07b 100644 --- a/filter/manage.php +++ b/filter/manage.php @@ -82,7 +82,6 @@ if ($forfilter) { if (!filter_has_local_settings($forfilter)) { throw new \moodle_exception('filterdoesnothavelocalconfig', 'error', $forfilter); } - require_once($CFG->dirroot . '/filter/local_settings_form.php'); require_once($CFG->dirroot . '/filter/' . $forfilter . '/filterlocalsettings.php'); $formname = $forfilter . '_filter_local_settings_form'; $settingsform = new $formname($CFG->wwwroot . '/filter/manage.php', $forfilter, $context); diff --git a/lib/db/legacyclasses.php b/lib/db/legacyclasses.php index 4a381b61ae6..d420223401e 100644 --- a/lib/db/legacyclasses.php +++ b/lib/db/legacyclasses.php @@ -180,4 +180,8 @@ $legacyclasses = [ 'core_filters', 'performance_measuring_filter_manager.php', ], + \filter_local_settings_form::class => [ + 'core_filters', + 'local_settings_form.php', + ], ]; From fe3b13d51abe275c817424ade6e5d5355ee0cb9b Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Wed, 10 Jul 2024 10:28:26 +0800 Subject: [PATCH 3/8] MDL-82427 core_filters: Update name of external service class --- .../get_available_in_context.php} | 15 +- filter/tests/external/external_test.php | 258 ------------------ lib/db/services.php | 3 +- 3 files changed, 8 insertions(+), 268 deletions(-) rename filter/classes/{external.php => external/get_available_in_context.php} (92%) delete mode 100644 filter/tests/external/external_test.php diff --git a/filter/classes/external.php b/filter/classes/external/get_available_in_context.php similarity index 92% rename from filter/classes/external.php rename to filter/classes/external/get_available_in_context.php index 65fadc5570f..84e78954b46 100644 --- a/filter/classes/external.php +++ b/filter/classes/external/get_available_in_context.php @@ -22,7 +22,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -namespace core_filters; +namespace core_filters\external; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/filterlib.php'); @@ -41,15 +41,14 @@ use Exception; * @copyright 2017 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class external extends external_api { - +class get_available_in_context extends external_api { /** - * Returns description of get_available_in_context() parameters. + * Returns description of get_available_in_context parameters. * * @return external_function_parameters * @since Moodle 3.4 */ - public static function get_available_in_context_parameters() { + public static function execute_parameters() { return new external_function_parameters ( array( 'contexts' => new external_multiple_structure( @@ -72,7 +71,7 @@ class external extends external_api { * @return array with the filters information and warnings * @since Moodle 3.4 */ - public static function get_available_in_context($contexts) { + public static function execute($contexts) { $params = self::validate_parameters(self::get_available_in_context_parameters(), array('contexts' => $contexts)); $filters = $warnings = array(); @@ -104,12 +103,12 @@ class external extends external_api { } /** - * Returns description of get_available_in_context() result value. + * Returns description of get_available_in_context result value. * * @return external_single_structure * @since Moodle 3.4 */ - public static function get_available_in_context_returns() { + public static function execute_returns() { return new external_single_structure( array( 'filters' => new external_multiple_structure( diff --git a/filter/tests/external/external_test.php b/filter/tests/external/external_test.php deleted file mode 100644 index f2be4520444..00000000000 --- a/filter/tests/external/external_test.php +++ /dev/null @@ -1,258 +0,0 @@ -. - -/** - * External filter functions unit tests. - * - * @package core_filters - * @category external - * @copyright 2017 Juan Leyva - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 3.4 - */ - -namespace core_filters\external; - -use core_external\external_api; -use core_filters\external; -use externallib_advanced_testcase; - -defined('MOODLE_INTERNAL') || die(); - -global $CFG; - -require_once($CFG->dirroot . '/webservice/tests/helpers.php'); - -/** - * External filter functions unit tests. - * - * @package core_filters - * @category external - * @copyright 2017 Juan Leyva - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 3.4 - */ -class external_test extends externallib_advanced_testcase { - - /** - * Test get_available_in_context_system - */ - public function test_get_available_in_context_system(): void { - global $DB; - - $this->resetAfterTest(true); - $this->setAdminUser(); - - $this->expectException('moodle_exception'); - external::get_available_in_context(array(array('contextlevel' => 'system', 'instanceid' => 0))); - } - - /** - * Test get_available_in_context_category - */ - public function test_get_available_in_context_category(): void { - global $DB; - - $this->resetAfterTest(true); - $this->setAdminUser(); - - $category = self::getDataGenerator()->create_category(); - - // Get all filters and disable them all globally. - $allfilters = filter_get_all_installed(); - foreach ($allfilters as $filter => $filtername) { - filter_set_global_state($filter, TEXTFILTER_DISABLED); - } - - $result = external::get_available_in_context(array(array('contextlevel' => 'coursecat', 'instanceid' => $category->id))); - $result = external_api::clean_returnvalue(external::get_available_in_context_returns(), $result); - $this->assertEmpty($result['filters']); // No filters, all disabled. - $this->assertEmpty($result['warnings']); - - // Enable one filter at global level. - reset($allfilters); - $firstfilter = key($allfilters); - filter_set_global_state($firstfilter, TEXTFILTER_ON); - - $result = external::get_available_in_context(array(array('contextlevel' => 'coursecat', 'instanceid' => $category->id))); - $result = external_api::clean_returnvalue(external::get_available_in_context_returns(), $result); - $this->assertEmpty($result['warnings']); - $this->assertEquals($firstfilter, $result['filters'][0]['filter']); // OK, the filter is enabled. - $this->assertEquals(TEXTFILTER_INHERIT, $result['filters'][0]['localstate']); // Inherits the parent context status. - $this->assertEquals(TEXTFILTER_ON, $result['filters'][0]['inheritedstate']); // In the parent context is available. - - // Set off the same filter at local context level. - filter_set_local_state($firstfilter, \context_coursecat::instance($category->id)->id, TEXTFILTER_OFF); - $result = external::get_available_in_context(array(array('contextlevel' => 'coursecat', 'instanceid' => $category->id))); - $result = external_api::clean_returnvalue(external::get_available_in_context_returns(), $result); - $this->assertEmpty($result['warnings']); - $this->assertEquals($firstfilter, $result['filters'][0]['filter']); // OK, the filter is enabled globally. - $this->assertEquals(TEXTFILTER_OFF, $result['filters'][0]['localstate']); // It is not available in this context. - $this->assertEquals(TEXTFILTER_ON, $result['filters'][0]['inheritedstate']); // In the parent context is available. - } - - /** - * Test get_available_in_context_course - */ - public function test_get_available_in_context_course(): void { - global $DB; - - $this->resetAfterTest(true); - $this->setAdminUser(); - - $course = self::getDataGenerator()->create_course(); - - // Get all filters and disable them all globally. - $allfilters = filter_get_all_installed(); - foreach ($allfilters as $filter => $filtername) { - filter_set_global_state($filter, TEXTFILTER_DISABLED); - } - - $result = external::get_available_in_context(array(array('contextlevel' => 'course', 'instanceid' => $course->id))); - $result = external_api::clean_returnvalue(external::get_available_in_context_returns(), $result); - $this->assertEmpty($result['filters']); // No filters, all disabled at global level. - $this->assertEmpty($result['warnings']); - - // Enable one filter at global level. - reset($allfilters); - $firstfilter = key($allfilters); - filter_set_global_state($firstfilter, TEXTFILTER_ON); - - $result = external::get_available_in_context(array(array('contextlevel' => 'course', 'instanceid' => $course->id))); - $result = external_api::clean_returnvalue(external::get_available_in_context_returns(), $result); - $this->assertEmpty($result['warnings']); - $this->assertEquals($firstfilter, $result['filters'][0]['filter']); // OK, the filter is enabled. - $this->assertEquals(TEXTFILTER_INHERIT, $result['filters'][0]['localstate']); // Inherits the parent context status. - $this->assertEquals(TEXTFILTER_ON, $result['filters'][0]['inheritedstate']); // In the parent context is available. - - // Set off the same filter at local context level. - filter_set_local_state($firstfilter, \context_course::instance($course->id)->id, TEXTFILTER_OFF); - $result = external::get_available_in_context(array(array('contextlevel' => 'course', 'instanceid' => $course->id))); - $result = external_api::clean_returnvalue(external::get_available_in_context_returns(), $result); - $this->assertEmpty($result['warnings']); - $this->assertEquals($firstfilter, $result['filters'][0]['filter']); // OK, the filter is enabled globally. - $this->assertEquals(TEXTFILTER_OFF, $result['filters'][0]['localstate']); // It is not available in this context. - $this->assertEquals(TEXTFILTER_ON, $result['filters'][0]['inheritedstate']); // In the parent context is available. - } - - /** - * Test get_available_in_context_module - */ - public function test_get_available_in_context_module(): void { - global $DB; - - $this->resetAfterTest(true); - $this->setAdminUser(); - - // Create one activity. - $course = self::getDataGenerator()->create_course(); - $forum = self::getDataGenerator()->create_module('forum', (object) array('course' => $course->id)); - - // Get all filters and disable them all globally. - $allfilters = filter_get_all_installed(); - foreach ($allfilters as $filter => $filtername) { - filter_set_global_state($filter, TEXTFILTER_DISABLED); - } - - $result = external::get_available_in_context(array(array('contextlevel' => 'module', 'instanceid' => $forum->cmid))); - $result = external_api::clean_returnvalue(external::get_available_in_context_returns(), $result); - $this->assertEmpty($result['filters']); // No filters, all disabled at global level. - $this->assertEmpty($result['warnings']); - - // Enable one filter at global level. - reset($allfilters); - $firstfilter = key($allfilters); - filter_set_global_state($firstfilter, TEXTFILTER_ON); - - $result = external::get_available_in_context(array(array('contextlevel' => 'module', 'instanceid' => $forum->cmid))); - $result = external_api::clean_returnvalue(external::get_available_in_context_returns(), $result); - $this->assertEmpty($result['warnings']); - $this->assertEquals($firstfilter, $result['filters'][0]['filter']); // OK, the filter is enabled. - $this->assertEquals(TEXTFILTER_INHERIT, $result['filters'][0]['localstate']); // Inherits the parent context status. - $this->assertEquals(TEXTFILTER_ON, $result['filters'][0]['inheritedstate']); // In the parent context is available. - - // Set off the same filter at local context level. - filter_set_local_state($firstfilter, \context_module::instance($forum->cmid)->id, TEXTFILTER_OFF); - $result = external::get_available_in_context(array(array('contextlevel' => 'module', 'instanceid' => $forum->cmid))); - $result = external_api::clean_returnvalue(external::get_available_in_context_returns(), $result); - $this->assertEmpty($result['warnings']); - $this->assertEquals($firstfilter, $result['filters'][0]['filter']); // OK, the filter is enabled globally. - $this->assertEquals(TEXTFILTER_OFF, $result['filters'][0]['localstate']); // It is not available in this context. - $this->assertEquals(TEXTFILTER_ON, $result['filters'][0]['inheritedstate']); // In the parent context is available. - - // Try user without permission, warning expected. - $user = $this->getDataGenerator()->create_user(); - $this->setUser($user); - $result = external::get_available_in_context(array(array('contextlevel' => 'module', 'instanceid' => $forum->cmid))); - $result = external_api::clean_returnvalue(external::get_available_in_context_returns(), $result); - $this->assertNotEmpty($result['warnings']); - $this->assertEquals('context', $result['warnings'][0]['item']); - $this->assertEquals($forum->cmid, $result['warnings'][0]['itemid']); - } - - /** - * Test get_all_states - * @covers \core_filters\external\get_all_states::execute - */ - public function test_get_all_states(): void { - $this->resetAfterTest(true); - $this->setAdminUser(); - - // Get all filters and disable them all globally except for the first. - $allfilters = filter_get_all_installed(); - reset($allfilters); - $firstfilter = key($allfilters); - foreach ($allfilters as $filter => $filterdata) { - if ($filter == $firstfilter) { - filter_set_global_state($filter, TEXTFILTER_ON); - continue; - } - filter_set_global_state($filter, TEXTFILTER_DISABLED); - } - - // Set some filters at particular levels. - $course = self::getDataGenerator()->create_course(); - filter_set_local_state($firstfilter, \context_course::instance($course->id)->id, TEXTFILTER_ON); - $forum = self::getDataGenerator()->create_module('forum', (object) ['course' => $course->id]); - filter_set_local_state($firstfilter, \context_module::instance($forum->cmid)->id, TEXTFILTER_OFF); - - $result = get_all_states::execute(); - $result = external_api::clean_returnvalue(get_all_states::execute_returns(), $result); - - $totalcount = count($allfilters) + 2; // All filters plus two local states. - $this->assertCount($totalcount, $result['filters']); - - $customfound = 0; - foreach ($result['filters'] as $filter) { - if ($filter['contextlevel'] == 'course' && $filter['instanceid'] == $course->id) { - $this->assertEquals($firstfilter, $filter['filter']); - $this->assertEquals(TEXTFILTER_ON, $filter['state']); - $customfound++; - } else if ($filter['contextlevel'] == 'module' && $filter['instanceid'] == $forum->cmid) { - $this->assertEquals($firstfilter, $filter['filter']); - $this->assertEquals(TEXTFILTER_OFF, $filter['state']); - $customfound++; - } else if ($filter['filter'] == $firstfilter) { - $this->assertEquals($firstfilter, $filter['filter']); - $this->assertEquals(TEXTFILTER_ON, $filter['state']); - $this->assertEquals(1, $filter['sortorder']); - } else { - $this->assertEquals(TEXTFILTER_DISABLED, $filter['state']); - } - } - $this->assertEquals(2, $customfound); // Both custom states found. - } -} diff --git a/lib/db/services.php b/lib/db/services.php index 39766f2707f..fa463f88558 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -2832,8 +2832,7 @@ $functions = array( // Filters functions. 'core_filters_get_available_in_context' => array( - 'classname' => 'core_filters\external', - 'methodname' => 'get_available_in_context', + 'classname' => 'core_filters\external\get_available_in_context', 'description' => 'Returns the filters available in the given contexts.', 'type' => 'read', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), From 534c3e182163065dc28d812e246142e5241096ad Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Tue, 9 Jul 2024 21:23:50 +0800 Subject: [PATCH 4/8] MDL-82427 core_filters: Support autoloading of filters --- .upgradenotes/MDL-82427-2024070901230429.yml | 6 ++++++ filter/classes/filter_manager.php | 5 +++++ lib/filterlib.php | 2 +- 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 .upgradenotes/MDL-82427-2024070901230429.yml diff --git a/.upgradenotes/MDL-82427-2024070901230429.yml b/.upgradenotes/MDL-82427-2024070901230429.yml new file mode 100644 index 00000000000..07ba3446eaf --- /dev/null +++ b/.upgradenotes/MDL-82427-2024070901230429.yml @@ -0,0 +1,6 @@ +issueNumber: MDL-82427 +notes: + core_filters: + - message: >- + Added support for autoloading of filters from `\filter_filtername\filter`. The existing class names are still supported. + type: improved diff --git a/filter/classes/filter_manager.php b/filter/classes/filter_manager.php index 152f2c1349d..e0febc60507 100644 --- a/filter/classes/filter_manager.php +++ b/filter/classes/filter_manager.php @@ -127,6 +127,11 @@ class filter_manager { protected function make_filter_object($filtername, $context, $localconfig) { global $CFG; + $filterclass = "\\filter_{$filtername}\\text_filter"; + if (class_exists($filterclass)) { + return new $filterclass($context, $localconfig); + } + $path = $CFG->dirroot .'/filter/'. $filtername .'/filter.php'; if (!is_readable($path)) { return null; diff --git a/lib/filterlib.php b/lib/filterlib.php index 18686c4792f..d6a84274120 100644 --- a/lib/filterlib.php +++ b/lib/filterlib.php @@ -70,7 +70,7 @@ function filter_get_name($filter) { function filter_get_all_installed() { $filternames = array(); foreach (core_component::get_plugin_list('filter') as $filter => $fulldir) { - if (is_readable("$fulldir/filter.php")) { + if (class_exists("\\filter_{$filter}\\text_filter") || is_readable("$fulldir/filter.php")) { $filternames[$filter] = filter_get_name($filter); } } From 29e7140e3029e3f94048619d4e3874b6c7fbe229 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Tue, 23 Jul 2024 11:32:32 +0800 Subject: [PATCH 5/8] MDL-82427 filter_*: Move text filters to autoloaded location Note: No backwards-compatability layer is included for manual inclusion of filter.php as this is not a public API. Inclusion should have always been through the filter manager, and the filter's own unit tests. --- .../{filter.php => classes/text_filter.php} | 51 ++--- .../{filter_test.php => text_filter_test.php} | 85 +++++---- .../{filter.php => classes/text_filter.php} | 161 +++++++++------- .../{filter_test.php => text_filter_test.php} | 51 ++--- .../{filter.php => classes/text_filter.php} | 12 +- .../{filter.php => classes/text_filter.php} | 23 +-- .../{filter_test.php => text_filter_test.php} | 7 +- .../{filter.php => classes/text_filter.php} | 21 +- filter/displayh5p/tests/filter_test.php | 103 ---------- filter/displayh5p/tests/text_filter_test.php | 127 +++++++++++++ .../{filter.php => classes/text_filter.php} | 50 +++-- .../emailprotect/tests/text_filter_test.php | 60 ++++++ .../{filter.php => classes/text_filter.php} | 14 +- .../{filter_test.php => text_filter_test.php} | 66 +++---- .../{filter.php => classes/text_filter.php} | 41 ++-- .../{filter_test.php => text_filter_test.php} | 124 ++++++------ .../{filter.php => classes/text_filter.php} | 17 +- .../mathjaxloader/tests/filtermath_test.php | 31 ++- .../{filter_test.php => text_filter_test.php} | 19 +- .../{filter.php => classes/text_filter.php} | 25 +-- filter/mediaplugin/dev/perftest.php | 3 +- filter/mediaplugin/tests/filter_test.php | 54 +++--- .../{filter.php => classes/text_filter.php} | 49 ++--- .../{filter_test.php => text_filter_test.php} | 42 ++-- .../{filter.php => classes/text_filter.php} | 179 +++++++++--------- filter/tex/tests/filter_test.php | 85 --------- filter/tex/tests/text_filter_test.php | 87 +++++++++ .../{filter.php => classes/text_filter.php} | 2 +- ...ter_tidy_test.php => text_filter_test.php} | 12 +- .../{filter.php => classes/text_filter.php} | 51 ++--- .../{filter_test.php => text_filter_test.php} | 161 +++++++--------- lib/classes/component.php | 4 +- lib/db/legacyclasses.php | 2 +- 33 files changed, 931 insertions(+), 888 deletions(-) rename filter/activitynames/{filter.php => classes/text_filter.php} (80%) rename filter/activitynames/tests/{filter_test.php => text_filter_test.php} (67%) rename filter/algebra/{filter.php => classes/text_filter.php} (72%) rename filter/algebra/tests/{filter_test.php => text_filter_test.php} (63%) rename filter/codehighlighter/{filter.php => classes/text_filter.php} (84%) rename filter/data/{filter.php => classes/text_filter.php} (92%) rename filter/data/tests/{filter_test.php => text_filter_test.php} (97%) rename filter/displayh5p/{filter.php => classes/text_filter.php} (95%) delete mode 100644 filter/displayh5p/tests/filter_test.php create mode 100644 filter/displayh5p/tests/text_filter_test.php rename filter/emailprotect/{filter.php => classes/text_filter.php} (66%) create mode 100644 filter/emailprotect/tests/text_filter_test.php rename filter/emoticon/{filter.php => classes/text_filter.php} (96%) rename filter/emoticon/tests/{filter_test.php => text_filter_test.php} (82%) rename filter/glossary/{filter.php => classes/text_filter.php} (90%) rename filter/glossary/tests/{filter_test.php => text_filter_test.php} (74%) rename filter/mathjaxloader/{filter.php => classes/text_filter.php} (97%) rename filter/mathjaxloader/tests/{filter_test.php => text_filter_test.php} (84%) rename filter/mediaplugin/{filter.php => classes/text_filter.php} (94%) rename filter/multilang/{filter.php => classes/text_filter.php} (82%) rename filter/multilang/tests/{filter_test.php => text_filter_test.php} (91%) rename filter/tex/{filter.php => classes/text_filter.php} (61%) delete mode 100644 filter/tex/tests/filter_test.php create mode 100644 filter/tex/tests/text_filter_test.php rename filter/tidy/{filter.php => classes/text_filter.php} (97%) rename filter/tidy/tests/{filter_tidy_test.php => text_filter_test.php} (89%) rename filter/urltolink/{filter.php => classes/text_filter.php} (87%) rename filter/urltolink/tests/{filter_test.php => text_filter_test.php} (79%) diff --git a/filter/activitynames/filter.php b/filter/activitynames/classes/text_filter.php similarity index 80% rename from filter/activitynames/filter.php rename to filter/activitynames/classes/text_filter.php index d6d30793c44..2939c828ad4 100644 --- a/filter/activitynames/filter.php +++ b/filter/activitynames/classes/text_filter.php @@ -1,5 +1,4 @@ . +namespace filter_activitynames; + +use cache; +use cache_store; +use core\output\html_writer; +use core_collator; +use filterobject; + /** * This filter provides automatic linking to * activities when its name (title) is found inside every Moodle text * - * @package filter + * @package filter_activitynames * @subpackage activitynames * @copyright 2004 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -defined('MOODLE_INTERNAL') || die(); - -/** - * Activity name filtering - */ -class filter_activitynames extends moodle_text_filter { - - function filter($text, array $options = array()) { +class text_filter extends \core_filters\text_filter { + #[\Override] + public function filter($text, array $options = []) { $coursectx = $this->context->get_course_context(false); if (!$coursectx) { return $text; @@ -41,12 +42,12 @@ class filter_activitynames extends moodle_text_filter { $activitylist = $this->get_cached_activity_list($courseid); - $filterslist = array(); + $filterslist = []; if (!empty($activitylist)) { $cmid = $this->context->instanceid; if ($this->context->contextlevel == CONTEXT_MODULE && isset($activitylist[$cmid])) { - // remove filterobjects for the current module - $filterslist = array_values(array_diff_key($activitylist, array($cmid => 1, $cmid.'-e' => 1))); + // Remove filterobjects for the current module. + $filterslist = array_values(array_diff_key($activitylist, [$cmid => 1, $cmid . '-e' => 1])); } else { $filterslist = array_values($activitylist); } @@ -89,23 +90,23 @@ class filter_activitynames extends moodle_text_filter { * @return filterobject[] the activities */ protected function get_activity_list($courseid) { - $activitylist = array(); + $activitylist = []; $modinfo = get_fast_modinfo($courseid); if (!empty($modinfo->cms)) { - $activitylist = array(); // We will store all the created filters here. + $activitylist = []; // We will store all the created filters here. // Create array of visible activities sorted by the name length (we are only interested in properties name and url). - $sortedactivities = array(); + $sortedactivities = []; foreach ($modinfo->cms as $cm) { // Use normal access control and visibility, but exclude labels and hidden activities. - if ($cm->visible and $cm->has_view() and $cm->uservisible) { - $sortedactivities[] = (object)array( + if ($cm->visible && $cm->has_view() && $cm->uservisible) { + $sortedactivities[] = (object)[ 'name' => $cm->name, 'url' => $cm->url, 'id' => $cm->id, 'namelen' => -strlen($cm->name), // Negative value for reverse sorting. - ); + ]; } } // Sort activities by the length of the activity name in reverse order. @@ -117,13 +118,15 @@ class filter_activitynames extends moodle_text_filter { $entitisedname = s($currentname); // Avoid empty or unlinkable activity names. if (!empty($title)) { - $hreftagbegin = html_writer::start_tag('a', - array('class' => 'autolink', 'title' => $title, - 'href' => $cm->url)); + $hreftagbegin = html_writer::start_tag( + 'a', + ['class' => 'autolink', 'title' => $title, + 'href' => $cm->url, ] + ); $activitylist[$cm->id] = new filterobject($currentname, $hreftagbegin, '', false, true); if ($currentname != $entitisedname) { // If name has some entity (& " < >) add that filter too. MDL-17545. - $activitylist[$cm->id.'-e'] = new filterobject($entitisedname, $hreftagbegin, '', false, true); + $activitylist[$cm->id . '-e'] = new filterobject($entitisedname, $hreftagbegin, '', false, true); } } } diff --git a/filter/activitynames/tests/filter_test.php b/filter/activitynames/tests/text_filter_test.php similarity index 67% rename from filter/activitynames/tests/filter_test.php rename to filter/activitynames/tests/text_filter_test.php index 08cf97134c8..b6713515ac1 100644 --- a/filter/activitynames/tests/filter_test.php +++ b/filter/activitynames/tests/text_filter_test.php @@ -14,25 +14,17 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Unit tests. - * - * @package filter_activitynames - * @category test - * @copyright 2018 The Open University - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace filter_activitynames; /** * Test case for the activity names auto-linking filter. * + * @package filter_activitynames * @copyright 2018 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \filter_activitynames\text_filter */ -class filter_test extends \advanced_testcase { - +final class text_filter_test extends \advanced_testcase { public function test_links(): void { $this->resetAfterTest(true); @@ -41,19 +33,26 @@ class filter_test extends \advanced_testcase { $context = \context_course::instance($course->id); // Create two pages that will be linked to. - $page1 = $this->getDataGenerator()->create_module('page', - ['course' => $course->id, 'name' => 'Test 1']); - $page2 = $this->getDataGenerator()->create_module('page', - ['course' => $course->id, 'name' => 'Test (2)']); + $page1 = $this->getDataGenerator()->create_module( + 'page', + ['course' => $course->id, 'name' => 'Test 1'] + ); + $page2 = $this->getDataGenerator()->create_module( + 'page', + ['course' => $course->id, 'name' => 'Test (2)'] + ); // Format text with all three entries in HTML. $html = '

Please read the two pages Test 1 and Test (2).

'; - $filtered = format_text($html, FORMAT_HTML, array('context' => $context)); + $filtered = format_text($html, FORMAT_HTML, ['context' => $context]); // Find all the glossary links in the result. $matches = []; - preg_match_all('~([^<]*)~', - $filtered, $matches); + preg_match_all( + '~([^<]*)~', + $filtered, + $matches + ); // There should be 2 links links. $this->assertCount(2, $matches[1]); @@ -86,11 +85,14 @@ class filter_test extends \advanced_testcase { $page = $this->getDataGenerator()->create_module('page', ['course' => $course->id, 'name' => '-']); $html = '

Please read the - page.

'; - $filtered = format_text($html, FORMAT_HTML, array('context' => $context)); + $filtered = format_text($html, FORMAT_HTML, ['context' => $context]); // Find the page link in the filtered html. - preg_match_all('~([^<]*)~', - $filtered, $matches); + preg_match_all( + '~([^<]*)~', + $filtered, + $matches + ); // We should have exactly one match. $this->assertCount(1, $matches[1]); @@ -110,42 +112,55 @@ class filter_test extends \advanced_testcase { $context2 = \context_course::instance($course2->id); // Create page 1. - $page1 = $this->getDataGenerator()->create_module('page', - ['course' => $course1->id, 'name' => 'Test 1']); + $page1 = $this->getDataGenerator()->create_module( + 'page', + ['course' => $course1->id, 'name' => 'Test 1'] + ); // Format text with page 1 in HTML. $html = '

Please read the two pages Test 1 and Test 2.

'; - $filtered1 = format_text($html, FORMAT_HTML, array('context' => $context1)); + $filtered1 = format_text($html, FORMAT_HTML, ['context' => $context1]); // Find all the activity links in the result. $matches = []; - preg_match_all('~([^<]*)~', - $filtered1, $matches); + preg_match_all( + '~([^<]*)~', + $filtered1, + $matches + ); // There should be 1 link. $this->assertCount(1, $matches[1]); $this->assertEquals($page1->name, $matches[1][0]); // Create page 2. - $page2 = $this->getDataGenerator()->create_module('page', - ['course' => $course1->id, 'name' => 'Test 2']); + $page2 = $this->getDataGenerator()->create_module( + 'page', + ['course' => $course1->id, 'name' => 'Test 2'] + ); // Filter the text again. - $filtered2 = format_text($html, FORMAT_HTML, array('context' => $context1)); + $filtered2 = format_text($html, FORMAT_HTML, ['context' => $context1]); // The filter result does not change due to caching. $this->assertEquals($filtered1, $filtered2); // Change context, so that cache for course 1 is cleared. - $filtered3 = format_text($html, FORMAT_HTML, array('context' => $context2)); + $filtered3 = format_text($html, FORMAT_HTML, ['context' => $context2]); $this->assertNotEquals($filtered1, $filtered3); $matches = []; - preg_match_all('~([^<]*)~', - $filtered3, $matches); + preg_match_all( + '~([^<]*)~', + $filtered3, + $matches + ); // There should be no links. $this->assertCount(0, $matches[1]); // Filter the text for course 1. - $filtered4 = format_text($html, FORMAT_HTML, array('context' => $context1)); + $filtered4 = format_text($html, FORMAT_HTML, ['context' => $context1]); // Find all the activity links in the result. $matches = []; - preg_match_all('~([^<]*)~', - $filtered4, $matches); + preg_match_all( + '~([^<]*)~', + $filtered4, + $matches + ); // There should be 2 links. $this->assertCount(2, $matches[1]); $this->assertEquals($page1->name, $matches[1][0]); diff --git a/filter/algebra/filter.php b/filter/algebra/classes/text_filter.php similarity index 72% rename from filter/algebra/filter.php rename to filter/algebra/classes/text_filter.php index e128f90b877..56fe83566eb 100644 --- a/filter/algebra/filter.php +++ b/filter/algebra/classes/text_filter.php @@ -1,5 +1,4 @@ . +namespace filter_algebra; + +use core\context\system as context_system; +use core\output\actions\popup_action; +use core\url; +use stdClass; + /** - * Moodle - Filter for converting simple calculator-type algebraic - * expressions to cached gif images + * Moodle - Filter for converting simple calculator-type algebraic expressions to cached gif images + * + * NOTE: This Moodle text filter converts algebraic expressions delimited + * by either @@...@@ or by ... tags + * first converts it to TeX using WeBWorK algebra parser Perl library + * AlgParser.pm, part of the WeBWorK distribution obtained from + * http://webhost.math.rochester.edu/downloadwebwork/ + * then converts the TeX to gif images using + * mimetex.cgi obtained from http://www.forkosh.com/mimetex.html authored by + * John Forkosh john@forkosh.com. The mimetex.cgi ELF binary compiled for Linux i386 + * as well as AlgParser.pm are included with this distribution. + * Note that there may be patent restrictions on the production of gif images + * in Canada and some parts of Western Europe and Japan until July 2004. + * ------------------------------------------------------------------------- + * You will then need to edit your moodle/config.php to invoke mathml_filter.php + * ------------------------------------------------------------------------- * * @package filter * @subpackage algebra @@ -25,71 +45,9 @@ * Originally based on code provided by Bruno Vernier bruno@vsbeducation.ca * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -defined('MOODLE_INTERNAL') || die(); - -//------------------------------------------------------------------------- -// NOTE: This Moodle text filter converts algebraic expressions delimited -// by either @@...@@ or by ... tags -// first converts it to TeX using WeBWorK algebra parser Perl library -// AlgParser.pm, part of the WeBWorK distribution obtained from -// http://webhost.math.rochester.edu/downloadwebwork/ -// then converts the TeX to gif images using -// mimetex.cgi obtained from http://www.forkosh.com/mimetex.html authored by -// John Forkosh john@forkosh.com. The mimetex.cgi ELF binary compiled for Linux i386 -// as well as AlgParser.pm are included with this distribution. -// Note that there may be patent restrictions on the production of gif images -// in Canada and some parts of Western Europe and Japan until July 2004. -//------------------------------------------------------------------------- -// You will then need to edit your moodle/config.php to invoke mathml_filter.php -//------------------------------------------------------------------------- - -function filter_algebra_image($imagefile, $tex= "", $height="", $width="", $align="middle") { - // Given the path to a picture file in a course, or a URL, - // this function includes the picture in the page. - global $CFG, $OUTPUT; - - $output = ""; - $style = 'style="border:0px; vertical-align:'.$align.';'; - $title = ''; - if ($tex) { - $tex = html_entity_decode($tex, ENT_QUOTES, 'UTF-8'); - $title = 'title="'.s($tex).'"'; - } - if ($height) { - $style .= " height:{$height}px;"; - } - if ($width) { - $style .= " width:{$width}px;"; - } - $style .= '"'; - $anchorcontents = ''; - if ($imagefile) { - $anchorcontents .= "\"".s($tex)."\"slasharguments) { // Use this method if possible for better caching - $anchorcontents .= "$CFG->wwwroot/filter/algebra/pix.php/$imagefile"; - } else { - $anchorcontents .= "$CFG->wwwroot/filter/algebra/pix.php?file=$imagefile"; - } - $anchorcontents .= "\" $style />"; - - if (!file_exists("$CFG->dataroot/filter/algebra/$imagefile") && has_capability('moodle/site:config', context_system::instance())) { - $link = '/filter/algebra/algebradebug.php'; - $action = null; - } else { - $link = new moodle_url('/filter/tex/displaytex.php', array('texexp'=>$tex)); - $action = new popup_action('click', $link, 'popup', array('width'=>320,'height'=>240)); //TODO: the popups do not work when text caching is enabled!! - } - $output .= $OUTPUT->action_link($link, $anchorcontents, $action, array('title'=>'TeX')); - - } else { - $output .= "Error: must pass URL or course"; - } - return $output; -} - -class filter_algebra extends moodle_text_filter { - public function filter($text, array $options = array()){ +class text_filter extends \core_filters\text_filter { + #[\Override] + public function filter($text, array $options = []) { global $CFG, $DB; /// Do a quick check using stripos to avoid unnecessary wor @@ -236,16 +194,75 @@ class filter_algebra extends moodle_text_filter { $texcache->rawtext = $texexp; $texcache->timemodified = time(); $DB->insert_record("cache_filters", $texcache, false); - $text = str_replace( $matches[0][$i], filter_algebra_image($filename, $texexp, '', '', $align), $text); + $text = str_replace( $matches[0][$i], self::get_image_markup($filename, $texexp, '', '', $align), $text); } else { $text = str_replace( $matches[0][$i],"Undetermined error: " . $matches[0][$i], $text); } } else { - $text = str_replace( $matches[0][$i], filter_algebra_image($filename, $texcache->rawtext), $text); + $text = str_replace($matches[0][$i], self::get_image_markup($filename, $texcache->rawtext), $text); } } return $text; } + + /** + * Create image link. + * + * @param string $imagefile name of file + * @param string $tex TeX notation (html entities already decoded) + * @param int $height O means automatic + * @param int $width O means automatic + * @param string $align + * @return string HTML markup + */ + protected function get_image_markup( + string $imagefile, + string $tex = "", + int $height = 0, + int $width = 0, + string $align = 'middle', + ): string { + // Given the path to a picture file in a course, or a URL, + // this function includes the picture in the page. + global $CFG, $OUTPUT; + + $output = ""; + $style = 'style="border:0px; vertical-align:' . $align . ';'; + $title = ''; + if ($tex) { + $tex = html_entity_decode($tex, ENT_QUOTES, 'UTF-8'); + $title = 'title="' . s($tex) . '"'; + } + if ($height) { + $style .= " height:{$height}px;"; + } + if ($width) { + $style .= " width:{$width}px;"; + } + $style .= '"'; + $anchorcontents = ''; + if ($imagefile) { + $anchorcontents .= "\""slasharguments) { + // Use this method if possible for better caching. + $anchorcontents .= "$CFG->wwwroot/filter/algebra/pix.php/$imagefile"; + } else { + $anchorcontents .= "$CFG->wwwroot/filter/algebra/pix.php?file=$imagefile"; + } + $anchorcontents .= "\" $style />"; + + if (!file_exists("$CFG->dataroot/filter/algebra/$imagefile") && has_capability('moodle/site:config', context_system::instance())) { + $link = '/filter/algebra/algebradebug.php'; + $action = null; + } else { + $link = new url('/filter/tex/displaytex.php', ['texexp' => $tex]); + // TODO: the popups do not work when text caching is enabled. + $action = new popup_action('click', $link, 'popup', ['width' => 320, 'height' => 240]); + } + $output .= $OUTPUT->action_link($link, $anchorcontents, $action, ['title' => 'TeX']); + } else { + $output .= "Error: must pass URL or course"; + } + return $output; + } } - - diff --git a/filter/algebra/tests/filter_test.php b/filter/algebra/tests/text_filter_test.php similarity index 63% rename from filter/algebra/tests/filter_test.php rename to filter/algebra/tests/text_filter_test.php index 3bc118050b2..def6bfbfe64 100644 --- a/filter/algebra/tests/filter_test.php +++ b/filter/algebra/tests/text_filter_test.php @@ -18,20 +18,13 @@ * Unit test for the filter_algebra * * @package filter_algebra - * @category phpunit * @copyright 2012 Tim Hunt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace filter_algebra; -use filter_algebra; - -defined('MOODLE_INTERNAL') || die(); - -global $CFG; -require_once($CFG->dirroot . '/filter/algebra/filter.php'); - +use core\context\system as context_system; /** * Unit tests for filter_algebra. @@ -41,35 +34,43 @@ require_once($CFG->dirroot . '/filter/algebra/filter.php'); * test server, and if it does not work here, it probably does not also work * for other people. A failing test will be irritating noise. * + * @package filter_algebra * @copyright 2012 Tim Hunt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \filter_algebra\text_filter */ -class filter_test extends \basic_testcase { - - protected $filter; +final class text_filter_test extends \basic_testcase { + /** @var text_filter The filter to test */ + protected text_filter $filter; protected function setUp(): void { parent::setUp(); - $this->filter = new filter_algebra(\context_system::instance(), array()); + $this->filter = new text_filter(context_system::instance(), []); } - function test_algebra_filter_no_algebra(): void { - $this->assertEquals('

Look no algebra!

', - $this->filter->filter('

Look no algebra!

')); + public function test_algebra_filter_no_algebra(): void { + $this->assertEquals( + '

Look no algebra!

', + $this->filter->filter('

Look no algebra!

') + ); } - function test_algebra_filter_pluginfile(): void { - $this->assertEquals('', - $this->filter->filter('')); + public function test_algebra_filter_pluginfile(): void { + $this->assertEquals( + '', + $this->filter->filter('') + ); } - function test_algebra_filter_draftfile(): void { - $this->assertEquals('', - $this->filter->filter('')); + public function test_algebra_filter_draftfile(): void { + $this->assertEquals( + '', + $this->filter->filter('') + ); } - function test_algebra_filter_unified_diff(): void { + public function test_algebra_filter_unified_diff(): void { $diff = ' diff -u -r1.1 Worksheet.php --- Worksheet.php 26 Sep 2003 04:18:02 -0000 1.1 @@ -88,7 +89,9 @@ diff -u -r1.1 Worksheet.php } else { '; - $this->assertEquals('
' . $diff . '
', - $this->filter->filter('
' . $diff . '
')); + $this->assertEquals( + '
' . $diff . '
', + $this->filter->filter('
' . $diff . '
') + ); } } diff --git a/filter/codehighlighter/filter.php b/filter/codehighlighter/classes/text_filter.php similarity index 84% rename from filter/codehighlighter/filter.php rename to filter/codehighlighter/classes/text_filter.php index 8812b7a2bd0..cc85e847075 100644 --- a/filter/codehighlighter/filter.php +++ b/filter/codehighlighter/classes/text_filter.php @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace filter_codehighlighter; + /** * Code highlighter filter. * @@ -23,14 +25,8 @@ * @copyright 2023 Meirza * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class filter_codehighlighter extends moodle_text_filter { - /** - * Apply the filter to the text - * - * @param string $text to be processed by the text - * @param array $options filter options - * @return string text after processing - */ +class text_filter extends \core_filters\text_filter { + #[\Override] public function filter($text, array $options = []): string { global $PAGE; diff --git a/filter/data/filter.php b/filter/data/classes/text_filter.php similarity index 92% rename from filter/data/filter.php rename to filter/data/classes/text_filter.php index 86bf031c5ff..d5ccfbe6ddf 100644 --- a/filter/data/filter.php +++ b/filter/data/classes/text_filter.php @@ -14,23 +14,18 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace filter_data; +use core_filters\filter_object; + /** - * This filter provides automatic linking to database activity entries - * when found inside every Moodle text. + * Filter providing automatic linking to database activity entries when found inside every Moodle text. * - * @package filter - * @subpackage data + * @package filter_data * @copyright 2006 Vy-Shane Sin Fat * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -defined('MOODLE_INTERNAL') || die(); - -/** - * Database activity filtering - */ -class filter_data extends moodle_text_filter { - +class text_filter extends \core_filters\text_filter { + #[\Override] public function filter($text, array $options = array()) { global $CFG, $DB, $USER; @@ -120,13 +115,13 @@ class filter_data extends moodle_text_filter { return $text; } - usort($contents, 'filter_data::sort_entries_by_length'); + usort($contents, [self::class, 'sort_entries_by_length']); foreach ($contents as $content) { $href_tag_begin = ''; - $contentlist[] = new filterobject($content->content, $href_tag_begin, '', false, true); + $contentlist[] = new filter_object($content->content, $href_tag_begin, '', false, true); } $contentlist = filter_remove_duplicates($contentlist); // Clean dupes diff --git a/filter/data/tests/filter_test.php b/filter/data/tests/text_filter_test.php similarity index 97% rename from filter/data/tests/filter_test.php rename to filter/data/tests/text_filter_test.php index 888b2761690..af70339568b 100644 --- a/filter/data/tests/filter_test.php +++ b/filter/data/tests/text_filter_test.php @@ -31,16 +31,13 @@ namespace filter_data; * @package filter_data * @copyright 2015 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \filter_data\text_filter */ -class filter_test extends \advanced_testcase { - +final class text_filter_test extends \advanced_testcase { /** * Tests that the filter applies the required changes. - * - * @return void */ public function test_filter(): void { - $this->resetAfterTest(true); $this->setAdminUser(); \filter_manager::reset_caches(); diff --git a/filter/displayh5p/filter.php b/filter/displayh5p/classes/text_filter.php similarity index 95% rename from filter/displayh5p/filter.php rename to filter/displayh5p/classes/text_filter.php index 9d38d2ea073..4996b11ed49 100644 --- a/filter/displayh5p/filter.php +++ b/filter/displayh5p/classes/text_filter.php @@ -13,16 +13,12 @@ // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Display H5P filter - * - * @package filter_displayh5p - * @copyright 2019 Victor Deniz - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -defined('MOODLE_INTERNAL') || die; +namespace filter_displayh5p; +use core\output\html_writer; +use core\url; +use core_filters\filter_object; use core_h5p\local\library\autoloader; /** @@ -34,8 +30,7 @@ use core_h5p\local\library\autoloader; * @copyright 2019 Victor Deniz * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class filter_displayh5p extends moodle_text_filter { - +class text_filter extends \core_filters\text_filter { /** * @var boolean $loadresizerjs This is whether to request the resize.js script. */ @@ -105,7 +100,7 @@ class filter_displayh5p extends moodle_text_filter { continue; } - $h5pcontenturl = new filterobject($source, null, null, false, + $h5pcontenturl = new filter_object($source, null, null, false, false, null, [$this, 'filterobject_prepare_replacement_callback'], $params + ['ish5plink' => false]); $h5pcontenturl->workregexp = '#'.$ultimatepattern.'#'; @@ -114,7 +109,7 @@ class filter_displayh5p extends moodle_text_filter { // Regex to find h5p extensions in an tag. $linkregexp = '~]*href=["\']('.$escapechars.'[^"\']*)["\'][^>]*>([^<]*)~is'; - $h5plinkurl = new filterobject($linkregexp, null, null, false, + $h5plinkurl = new filter_object($linkregexp, null, null, false, false, null, [$this, 'filterobject_prepare_replacement_callback'], $params + ['ish5plink' => true]); $h5plinkurl->workregexp = $linkregexp; $h5plinks[] = $h5plinkurl; @@ -152,7 +147,7 @@ class filter_displayh5p extends moodle_text_filter { // The Edit button placeholder has been added only if the file can be edited. if ($h5pcontent->replacementcallbackdata['canbeedited']) { // If the content was originally a link, ignore it (it won't have the placeholder). - $matchurl = new \moodle_url($matches[0]); + $matchurl = new url($matches[0]); if (strpos($matchurl->get_path(), 'h5p/embed.php') !== false) { return $matches[0]; } diff --git a/filter/displayh5p/tests/filter_test.php b/filter/displayh5p/tests/filter_test.php deleted file mode 100644 index bb75386b2f8..00000000000 --- a/filter/displayh5p/tests/filter_test.php +++ /dev/null @@ -1,103 +0,0 @@ -. - -/** - * Unit tests for the filter_displayh5p - * - * @package filter_displayh5p - * @category test - * @copyright 2019 Victor Deniz - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace filter_displayh5p; - -use filter_displayh5p; - -defined('MOODLE_INTERNAL') || die(); - -global $CFG; -require_once($CFG->dirroot.'/filter/displayh5p/filter.php'); - -/** - * Unit tests for the display H5P filter. - * - * @copyright 2019 Victor Deniz - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class filter_test extends \advanced_testcase { - - public function setUp(): void { - parent::setUp(); - - $this->resetAfterTest(true); - - set_config('allowedsources', - "https://moodle.h5p.com/content/[id]/embed\nhttps://moodle.h5p.com/content/[id] - \nhttps://generic.wordpress.soton.ac.uk/altc/wp-admin/admin-ajax.php?action=h5p_embed&id=[id]", - 'filter_displayh5p'); - } - - /** - * Check that h5p tags with urls from allowed domains are filtered. - * - * @param string $text Original text - * @param string $filteredtextpattern Text pattern after display H5P filter - * - * @dataProvider texts_provider - */ - public function test_filter_urls($text, $filteredtextpattern): void { - - $filterplugin = new filter_displayh5p(null, array()); - - $filteredtext = $filterplugin->filter($text); - $this->assertMatchesRegularExpression($filteredtextpattern, $filteredtext); - } - - /** - * Provides texts to filter for the {@link self::test_filter_urls} method. - * - * @return array - */ - public function texts_provider() { - global $CFG; - - return [ - ["http:://example.com", "#http:://example.com#"], - ["http://google.es/h5p/embed/3425234", "#http://google.es/h5p/embed/3425234#"], - ["https://moodle.h5p.com/content/1290729733828858779/embed", "##"], - [$CFG->wwwroot."/pluginfile.php/5/user/private/accordion-6-7138%20%281%29.h5p.h5p", - "##"] - ]; - } -} diff --git a/filter/displayh5p/tests/text_filter_test.php b/filter/displayh5p/tests/text_filter_test.php new file mode 100644 index 00000000000..ce843a89d1d --- /dev/null +++ b/filter/displayh5p/tests/text_filter_test.php @@ -0,0 +1,127 @@ +. + +/** + * Unit tests for the filter_displayh5p + * + * @package filter_displayh5p + * @category test + * @copyright 2019 Victor Deniz + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace filter_displayh5p; + +/** + * Unit tests for the display H5P filter. + * + * @package filter_displayh5p + * @copyright 2019 Victor Deniz + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \filter_displayh5p\text_filter + */ +final class text_filter_test extends \advanced_testcase { + /** + * Check that h5p tags with urls from allowed domains are filtered. + * + * @param string $text Original text + * @param string $filteredtextpattern Text pattern after display H5P filter + * @dataProvider texts_provider + */ + public function test_filter_urls($text, $filteredtextpattern): void { + $this->resetAfterTest(true); + + set_config( + 'allowedsources', + "https://moodle.h5p.com/content/[id]/embed\nhttps://moodle.h5p.com/content/[id] + \nhttps://generic.wordpress.soton.ac.uk/altc/wp-admin/admin-ajax.php?action=h5p_embed&id=[id]", + 'filter_displayh5p' + ); + + + $filterplugin = new text_filter(null, []); + + $filteredtext = $filterplugin->filter($text); + $this->assertMatchesRegularExpression($filteredtextpattern, $filteredtext); + } + + /** + * Provides texts to filter for the {@link self::test_filter_urls} method. + * + * @return array + */ + public static function texts_provider(): array { + global $CFG; + + $contenturla = "https://moodle.h5p.com/content/1290848995208939539/embed"; + $contenturlb = "https://moodle.h5p.com/content/1290729733828858779/embed"; + + return [ + [ + "http:://example.com", + "#http:://example.com#", + ], + [ + "http://google.es/h5p/embed/3425234", + "#http://google.es/h5p/embed/3425234#", + ], + [ + $contenturlb, + "##", + ], + [ + $CFG->wwwroot . "/pluginfile.php/5/user/private/accordion-6-7138%20%281%29.h5p.h5p", + "##", + ], + ]; + } +} diff --git a/filter/emailprotect/filter.php b/filter/emailprotect/classes/text_filter.php similarity index 66% rename from filter/emailprotect/filter.php rename to filter/emailprotect/classes/text_filter.php index 2d0dbc480e6..8c4b3be8ce1 100644 --- a/filter/emailprotect/filter.php +++ b/filter/emailprotect/classes/text_filter.php @@ -1,5 +1,4 @@ . +namespace filter_emailprotect; + /** * Basic email protection filter. * + * This class looks for email addresses in Moodle text and hides them using the Moodle obfuscate_text function. + * * @package filter * @subpackage emailprotect * @copyright 2004 Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -defined('MOODLE_INTERNAL') || die(); - -/** - * This class looks for email addresses in Moodle text and - * hides them using the Moodle obfuscate_text function. - */ -class filter_emailprotect extends moodle_text_filter { - function filter($text, array $options = array()) { +class text_filter extends \core_filters\text_filter { + #[\Override] + public function filter($text, array $options = array()) { /// Do a quick check using stripos to avoid unnecessary work if (strpos($text, '@') === false) { return $text; @@ -46,22 +43,33 @@ class filter_emailprotect extends moodle_text_filter { /// pattern to find a mailto link with the linked text. $pattern = '|()'.'(.*)'.'()|iU'; $text = preg_replace_callback($pattern, 'filter_emailprotect_alter_mailto', $text); + $text = preg_replace_callback($pattern, [self::class, 'alter_mailto'], $text); /// pattern to find any other email address in the text. $pattern = '/(^|\s+|>)'.$emailregex.'($|\s+|\.\s+|\.$|<)/i'; $text = preg_replace_callback($pattern, 'filter_emailprotect_alter_email', $text); + $text = preg_replace_callback($pattern, [self::class, 'alter_email'], $text); return $text; } + + /** + * Obfuscate the email address. + * + * @param mixed $matches + * @return string + */ + private function alter_email($matches) { + return $matches[1].obfuscate_text($matches[2]).$matches[3]; + } + + /** + * Obfuscate the mailto link. + * + * @param mixed $matches + * @return string + */ + private function alter_mailto($matches) { + return obfuscate_mailto($matches[2], $matches[4]); + } } - - -function filter_emailprotect_alter_email($matches) { - return $matches[1].obfuscate_text($matches[2]).$matches[3]; -} - -function filter_emailprotect_alter_mailto($matches) { - return obfuscate_mailto($matches[2], $matches[4]); -} - - diff --git a/filter/emailprotect/tests/text_filter_test.php b/filter/emailprotect/tests/text_filter_test.php new file mode 100644 index 00000000000..8bbd649b028 --- /dev/null +++ b/filter/emailprotect/tests/text_filter_test.php @@ -0,0 +1,60 @@ +. + +namespace filter_emailprotect; + +/** + * Tests for the filter_emailprotect text filter. + * + * @package filter_emailprotect + * @category test + * @copyright 2024 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \filter_emailprotect\text_filter + */ +final class text_filter_test extends \advanced_testcase { + /** + * Test the filter method. + * + * @dataProvider filter_provider + * @param string $expression The regexp to check. + * @param string $text The text to filter. + */ + public function test_filter( + string $expression, + string $text, + ): void { + $filter = new text_filter(\core\context\system::instance(), []); + $this->assertMatchesRegularExpression($expression, $text); + } + + /** + * Data provider for the filter test. + * + * @return array + */ + public static function filter_provider(): array { + $email = 'chaise@example.com'; + return [ + // No email address found. + ['/Hello, world!/', 'Hello, world!'], + // Email addresses present. + // Note: The obfuscation randomly choose which chars to obfuscate. + ['/.*@.*/', $email], + ["~.*@.*~", "$email"], + ]; + } +} diff --git a/filter/emoticon/filter.php b/filter/emoticon/classes/text_filter.php similarity index 96% rename from filter/emoticon/filter.php rename to filter/emoticon/classes/text_filter.php index 6b6651ddcac..02c9f1b3b7a 100644 --- a/filter/emoticon/filter.php +++ b/filter/emoticon/classes/text_filter.php @@ -15,23 +15,19 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace filter_emoticon; + /** * Filter converting emoticon texts into images * * This filter uses the emoticon settings in Site admin > Appearance > HTML settings * and replaces emoticon texts with images. * - * @package filter - * @subpackage emoticon - * @see emoticon_manager + * @package filter_emoticon * @copyright 2010 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -defined('MOODLE_INTERNAL') || die(); - -class filter_emoticon extends moodle_text_filter { - +class text_filter extends \core_filters\text_filter { /** * Internal cache used for replacing. Multidimensional array; * - dimension 1: language, @@ -51,7 +47,7 @@ class filter_emoticon extends moodle_text_filter { /** * Apply the filter to the text * - * @see filter_manager::apply_filter_chain() + * @see \core_filters\filter_manager::apply_filter_chain() * @param string $text to be processed by the text * @param array $options filter options * @return string text after processing diff --git a/filter/emoticon/tests/filter_test.php b/filter/emoticon/tests/text_filter_test.php similarity index 82% rename from filter/emoticon/tests/filter_test.php rename to filter/emoticon/tests/text_filter_test.php index 121adb3ba84..f7514ec07ae 100644 --- a/filter/emoticon/tests/filter_test.php +++ b/filter/emoticon/tests/text_filter_test.php @@ -14,6 +14,10 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace filter_emoticon; + +use core\context\system as context_system; + /** * Skype icons filter phpunit tests * @@ -21,22 +25,9 @@ * @category test * @copyright 2013 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \filter_emoticon\text_filter */ - -namespace filter_emoticon; - -use filter_emoticon; - -defined('MOODLE_INTERNAL') || die(); - -global $CFG; -require_once($CFG->dirroot . '/filter/emoticon/filter.php'); // Include the code to test. - -/** - * Skype icons filter testcase. - */ -class filter_test extends \advanced_testcase { - +final class text_filter_test extends \advanced_testcase { /** * Test that filter ignores nolink/pre element, and processes remaining text * @@ -48,7 +39,7 @@ class filter_test extends \advanced_testcase { public function test_filter_emoticon_filtered(string $input, string $expectedprefix): void { $this->resetAfterTest(); - $filteredtext = (new testable_filter_emoticon())->filter($input, [ + $filteredtext = $this->get_testable_filter_emoticon()->filter($input, [ 'originalformat' => FORMAT_HTML, ]); @@ -61,7 +52,7 @@ class filter_test extends \advanced_testcase { * * @return string[] */ - public function filter_emoticon_filtered_provider(): array { + public static function filter_emoticon_filtered_provider(): array { return [ 'FORMAT_HTML is filtered' => [ 'input' => 'Hello(n)', @@ -86,7 +77,7 @@ class filter_test extends \advanced_testcase { public function test_filter_emoticon($input, $format, $expected): void { $this->resetAfterTest(); - $filter = new testable_filter_emoticon(); + $filter = $this->get_testable_filter_emoticon(); $this->assertEquals($expected, $filter->filter($input, [ 'originalformat' => $format, ])); @@ -97,7 +88,7 @@ class filter_test extends \advanced_testcase { * * @return array */ - public function filter_emoticon_provider() { + public static function filter_emoticon_provider() { $grr = '(grr)'; return [ 'FORMAT_MOODLE is not filtered' => [ @@ -164,13 +155,12 @@ class filter_test extends \advanced_testcase { /** * Tests the filter doesn't break anything if activated but invalid format passed. - * */ public function test_filter_invalidformat(): void { global $PAGE; $this->resetAfterTest(); - $filter = new testable_filter_emoticon(); + $filter = $this->get_testable_filter_emoticon(); $input = '(grr)'; $expected = '(grr)'; @@ -181,7 +171,6 @@ class filter_test extends \advanced_testcase { /** * Tests the filter doesn't break anything if activated but no emoticons available. - * */ public function test_filter_emptyemoticons(): void { global $CFG; @@ -189,7 +178,7 @@ class filter_test extends \advanced_testcase { // Empty the emoticons array. $CFG->emoticons = null; - $filter = new filter_emoticon(\context_system::instance(), array('originalformat' => FORMAT_HTML)); + $filter = new text_filter(context_system::instance(), ['originalformat' => FORMAT_HTML]); $input = '(grr)'; $expected = '(grr)'; @@ -198,19 +187,24 @@ class filter_test extends \advanced_testcase { 'originalformat' => FORMAT_HTML, ])); } -} -/** - * Subclass for easier testing. - */ -class testable_filter_emoticon extends filter_emoticon { - public function __construct() { - // Reset static emoticon caches. - parent::$emoticontexts = array(); - parent::$emoticonimgs = array(); - // Use this context for filtering. - $this->context = \context_system::instance(); - // Define FORMAT_HTML as only one filtering in DB. - set_config('formats', implode(',', array(FORMAT_HTML)), 'filter_emoticon'); + /** + * Get a copy of the filter configured for testing. + * + * @param array $args + * @return \filter_emoticon\text_filter + */ + protected function get_testable_filter_emoticon(...$args): text_filter { + return new class extends text_filter { + public function __construct(...$args) { + // Reset static emoticon caches. + parent::$emoticontexts = []; + parent::$emoticonimgs = []; + // Use this context for filtering. + $this->context = context_system::instance(); + // Define FORMAT_HTML as only one filtering in DB. + set_config('formats', implode(',', [FORMAT_HTML]), 'filter_emoticon'); + } + }; } } diff --git a/filter/glossary/filter.php b/filter/glossary/classes/text_filter.php similarity index 90% rename from filter/glossary/filter.php rename to filter/glossary/classes/text_filter.php index 65f9186e8a4..d5c52820d02 100644 --- a/filter/glossary/filter.php +++ b/filter/glossary/classes/text_filter.php @@ -14,25 +14,25 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace filter_glossary; + +use cache; +use cache_store; +use core\output\html_writer; +use core\url; +use core_filters\filter_object; +use stdClass; + /** - * This filter provides automatic linking to - * glossary entries, aliases and categories when - * found inside every Moodle text. + * This filter provides automatic linking to glossary entries, aliases and categories when found inside every Moodle text. * - * @package filter - * @subpackage glossary + * NOTE: multilang glossary entries are not compatible with this filter. + * + * @package filter_glossary * @copyright 2004 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -defined('MOODLE_INTERNAL') || die(); - -/** - * Glossary linking filter class. - * - * NOTE: multilang glossary entries are not compatible with this filter. - */ -class filter_glossary extends moodle_text_filter { +class text_filter extends \core_filters\text_filter { /** @var null|cache_store cache used to store the terms for this course. */ protected $cache = null; @@ -44,7 +44,7 @@ class filter_glossary extends moodle_text_filter { /** * Get all the concepts for this context. - * @return filterobject[] the concepts, and filterobjects. + * @return filter_object[] the concepts, and filterobjects. */ protected function get_all_concepts() { global $USER; @@ -87,7 +87,7 @@ class filter_glossary extends moodle_text_filter { foreach ($allconcepts as $concepts) { foreach ($concepts as $concept) { - $conceptlist[] = new filterobject($concept->concept, null, null, + $conceptlist[] = new filter_object($concept->concept, null, null, $concept->casesensitive, $concept->fullmatch, null, [$this, 'filterobject_prepare_replacement_callback'], [$concept, $glossaries]); } @@ -122,7 +122,7 @@ class filter_glossary extends moodle_text_filter { if ($concept->category) { // Link to a category. $title = get_string('glossarycategory', 'filter_glossary', ['glossary' => $glossaries[$concept->glossaryid], 'category' => $concept->concept]); - $link = new moodle_url('/mod/glossary/view.php', + $link = new url('/mod/glossary/view.php', ['g' => $concept->glossaryid, 'mode' => 'cat', 'hook' => $concept->id]); $attributes = array( 'href' => $link, @@ -136,7 +136,7 @@ class filter_glossary extends moodle_text_filter { // to the current glossary format which may not work in a popup. // for example "entry list" means the popup would only contain // a link that opens another popup. - $link = new moodle_url('/mod/glossary/showentry.php', + $link = new url('/mod/glossary/showentry.php', ['eid' => $concept->id, 'displayformat' => 'dictionary']); $attributes = array( 'href' => $link, @@ -155,6 +155,7 @@ class filter_glossary extends moodle_text_filter { return [html_writer::start_tag('a', $attributes), '', null]; } + #[\Override] public function filter($text, array $options = array()) { global $GLOSSARY_EXCLUDEENTRY; @@ -184,8 +185,8 @@ class filter_glossary extends moodle_text_filter { /** * usort helper used in get_all_concepts above. - * @param filterobject $filterobject0 first item to compare. - * @param filterobject $filterobject1 second item to compare. + * @param filter_object $filterobject0 first item to compare. + * @param filter_object $filterobject1 second item to compare. * @return int -1, 0 or 1. */ private function sort_entries_by_length($filterobject0, $filterobject1) { diff --git a/filter/glossary/tests/filter_test.php b/filter/glossary/tests/text_filter_test.php similarity index 74% rename from filter/glossary/tests/filter_test.php rename to filter/glossary/tests/text_filter_test.php index ab02d5bcc05..6c5bd20bb9f 100644 --- a/filter/glossary/tests/filter_test.php +++ b/filter/glossary/tests/text_filter_test.php @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace filter_glossary; + /** * Unit tests. * @@ -21,15 +23,9 @@ * @category test * @copyright 2013 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \filter_glossary\text_filter */ - -namespace filter_glossary; - -/** - * Test case for glossary. - */ -class filter_test extends \advanced_testcase { - +final class text_filter_test extends \advanced_testcase { public function test_link_to_entry_with_alias(): void { global $CFG; $this->resetAfterTest(true); @@ -43,20 +39,25 @@ class filter_test extends \advanced_testcase { $context = \context_course::instance($course->id); // Create a glossary. - $glossary = $this->getDataGenerator()->create_module('glossary', - array('course' => $course->id, 'mainglossary' => 1)); + $glossary = $this->getDataGenerator()->create_module( + 'glossary', + ['course' => $course->id, 'mainglossary' => 1] + ); // Create two entries with ampersands and one normal entry. $generator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); - $normal = $generator->create_content($glossary, array('concept' => 'entry name'), - array('first alias', 'second alias')); + $normal = $generator->create_content( + $glossary, + ['concept' => 'entry name'], + ['first alias', 'second alias'] + ); // Format text with all three entries in HTML. $html = '

First we have entry name, then we have it twp aliases first alias and second alias.

'; - $filtered = format_text($html, FORMAT_HTML, array('context' => $context)); + $filtered = format_text($html, FORMAT_HTML, ['context' => $context]); // Find all the glossary links in the result. - $matches = array(); + $matches = []; preg_match_all('~eid=([0-9]+).*?title="(.*?)"~', $filtered, $matches); // There should be 3 glossary links. @@ -66,8 +67,8 @@ class filter_test extends \advanced_testcase { $this->assertEquals($normal->id, $matches[1][2]); // Check text of title attribute. - $this->assertEquals($glossary->name . ': entry name', $matches[2][0]); - $this->assertEquals($glossary->name . ': first alias', $matches[2][1]); + $this->assertEquals($glossary->name . ': entry name', $matches[2][0]); + $this->assertEquals($glossary->name . ': first alias', $matches[2][1]); $this->assertEquals($glossary->name . ': second alias', $matches[2][2]); } @@ -84,20 +85,22 @@ class filter_test extends \advanced_testcase { $context = \context_course::instance($course->id); // Create a glossary. - $glossary = $this->getDataGenerator()->create_module('glossary', - array('course' => $course->id, 'mainglossary' => 1)); + $glossary = $this->getDataGenerator()->create_module( + 'glossary', + ['course' => $course->id, 'mainglossary' => 1] + ); // Create two entries with ampersands and one normal entry. $generator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); - $shorter = $generator->create_content($glossary, array('concept' => 'Tim')); - $longer = $generator->create_content($glossary, array('concept' => 'Time')); + $shorter = $generator->create_content($glossary, ['concept' => 'Tim']); + $longer = $generator->create_content($glossary, ['concept' => 'Time']); // Format text with all three entries in HTML. $html = '

Time will tell

'; - $filtered = format_text($html, FORMAT_HTML, array('context' => $context)); + $filtered = format_text($html, FORMAT_HTML, ['context' => $context]); // Find all the glossary links in the result. - $matches = array(); + $matches = []; preg_match_all('~eid=([0-9]+).*?title="(.*?)"~', $filtered, $matches); // There should be 1 glossary link to Time, not Tim. @@ -105,7 +108,7 @@ class filter_test extends \advanced_testcase { $this->assertEquals($longer->id, $matches[1][0]); // Check text of title attribute. - $this->assertEquals($glossary->name . ': Time', $matches[2][0]); + $this->assertEquals($glossary->name . ': Time', $matches[2][0]); } public function test_link_to_category(): void { @@ -121,20 +124,22 @@ class filter_test extends \advanced_testcase { $context = \context_course::instance($course->id); // Create a glossary. - $glossary = $this->getDataGenerator()->create_module('glossary', - array('course' => $course->id, 'mainglossary' => 1)); + $glossary = $this->getDataGenerator()->create_module( + 'glossary', + ['course' => $course->id, 'mainglossary' => 1] + ); // Create two entries with ampersands and one normal entry. /** @var \mod_glossary_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); - $category = $generator->create_category($glossary, array('name' => 'My category', 'usedynalink' => 1)); + $category = $generator->create_category($glossary, ['name' => 'My category', 'usedynalink' => 1]); // Format text with all three entries in HTML. $html = '

This is My category you know.

'; - $filtered = format_text($html, FORMAT_HTML, array('context' => $context)); + $filtered = format_text($html, FORMAT_HTML, ['context' => $context]); // Find all the glossary links in the result. - $matches = array(); + $matches = []; preg_match_all('~hook=([0-9]+).*?title="(.*?)"~', $filtered, $matches); // There should be 1 glossary link. @@ -159,22 +164,24 @@ class filter_test extends \advanced_testcase { $context = \context_course::instance($course->id); // Create a glossary. - $glossary = $this->getDataGenerator()->create_module('glossary', - array('course' => $course->id, 'mainglossary' => 1)); + $glossary = $this->getDataGenerator()->create_module( + 'glossary', + ['course' => $course->id, 'mainglossary' => 1] + ); // Create two entries with ampersands and one normal entry. /** @var \mod_glossary_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); - $normal = $generator->create_content($glossary, array('concept' => 'normal')); - $amp1 = $generator->create_content($glossary, array('concept' => 'A&B')); - $amp2 = $generator->create_content($glossary, array('concept' => 'C&D')); + $normal = $generator->create_content($glossary, ['concept' => 'normal']); + $amp1 = $generator->create_content($glossary, ['concept' => 'A&B']); + $amp2 = $generator->create_content($glossary, ['concept' => 'C&D']); // Format text with all three entries in HTML. $html = '

A&B C&D normal

'; - $filtered = format_text($html, FORMAT_HTML, array('context' => $context)); + $filtered = format_text($html, FORMAT_HTML, ['context' => $context]); // Find all the glossary links in the result. - $matches = array(); + $matches = []; preg_match_all('~eid=([0-9]+).*?title="(.*?)"~', $filtered, $matches); // There should be 3 glossary links. @@ -205,22 +212,24 @@ class filter_test extends \advanced_testcase { $context = \context_course::instance($course->id); // Create a glossary. - $glossary = $this->getDataGenerator()->create_module('glossary', - array('course' => $course->id, 'mainglossary' => 1)); + $glossary = $this->getDataGenerator()->create_module( + 'glossary', + ['course' => $course->id, 'mainglossary' => 1] + ); // Create two entries with ampersands and one normal entry. /** @var \mod_glossary_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); - $simple = $generator->create_content($glossary, array('concept' => 'simple')); - $withbrackets = $generator->create_content($glossary, array('concept' => 'more complex (perhaps)')); - $test2 = $generator->create_content($glossary, array('concept' => 'Test (2)')); + $simple = $generator->create_content($glossary, ['concept' => 'simple']); + $withbrackets = $generator->create_content($glossary, ['concept' => 'more complex (perhaps)']); + $test2 = $generator->create_content($glossary, ['concept' => 'Test (2)']); // Format text with all three entries in HTML. $html = '

Some thigns are simple. Others are more complex (perhaps). Test (2).

'; - $filtered = format_text($html, FORMAT_HTML, array('context' => $context)); + $filtered = format_text($html, FORMAT_HTML, ['context' => $context]); // Find all the glossary links in the result. - $matches = array(); + $matches = []; preg_match_all('~eid=([0-9]+).*?title="(.*?)"~', $filtered, $matches); // There should be 3 glossary links. @@ -249,24 +258,29 @@ class filter_test extends \advanced_testcase { $context = \context_course::instance($course->id); // Create a glossary. - $glossary = $this->getDataGenerator()->create_module('glossary', - array('course' => $course->id, 'mainglossary' => 1)); + $glossary = $this->getDataGenerator()->create_module( + 'glossary', + ['course' => $course->id, 'mainglossary' => 1] + ); // Create two entries with ampersands and one normal entry. $generator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); - $tobeexcluded = $generator->create_content($glossary, array('concept' => 'entry name'), - array('first alias', 'second alias')); - $normal = $generator->create_content($glossary, array('concept' => 'other entry')); + $tobeexcluded = $generator->create_content( + $glossary, + ['concept' => 'entry name'], + ['first alias', 'second alias'] + ); + $normal = $generator->create_content($glossary, ['concept' => 'other entry']); // Format text with all three entries in HTML. $html = '

First we have entry name, then we have it twp aliases first alias and second alias. ' . 'In this case, those should not be linked, but this other entry should be.

'; $GLOSSARY_EXCLUDEENTRY = $tobeexcluded->id; - $filtered = format_text($html, FORMAT_HTML, array('context' => $context)); + $filtered = format_text($html, FORMAT_HTML, ['context' => $context]); $GLOSSARY_EXCLUDEENTRY = null; // Find all the glossary links in the result. - $matches = array(); + $matches = []; preg_match_all('~eid=([0-9]+).*?title="(.*?)"~', $filtered, $matches); // There should be 1 glossary links. @@ -288,22 +302,24 @@ class filter_test extends \advanced_testcase { $context = \context_course::instance($course->id); // Create a glossary. - $glossary = $this->getDataGenerator()->create_module('glossary', - array('course' => $course->id, 'mainglossary' => 1)); + $glossary = $this->getDataGenerator()->create_module( + 'glossary', + ['course' => $course->id, 'mainglossary' => 1] + ); // Create two entries with ampersands and one normal entry. /** @var \mod_glossary_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); - $category = $generator->create_category($glossary, array('name' => 'My category', 'usedynalink' => 1)); + $category = $generator->create_category($glossary, ['name' => 'My category', 'usedynalink' => 1]); // Format text with all three entries in HTML. $html = '

This is My category you know.

'; $GLOSSARY_EXCLUDEENTRY = $category->id; - $filtered = format_text($html, FORMAT_HTML, array('context' => $context)); + $filtered = format_text($html, FORMAT_HTML, ['context' => $context]); $GLOSSARY_EXCLUDEENTRY = null; // Find all the glossary links in the result. - $matches = array(); + $matches = []; preg_match_all('~hook=([0-9]+).*?title="(.*?)"~', $filtered, $matches); // There should be 1 glossary link. diff --git a/filter/mathjaxloader/filter.php b/filter/mathjaxloader/classes/text_filter.php similarity index 97% rename from filter/mathjaxloader/filter.php rename to filter/mathjaxloader/classes/text_filter.php index 10bba6a761b..07258e5f454 100644 --- a/filter/mathjaxloader/filter.php +++ b/filter/mathjaxloader/classes/text_filter.php @@ -14,6 +14,10 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace filter_mathjaxloader; + +use core\url; + /** * This filter provides automatic support for MathJax * @@ -21,14 +25,7 @@ * @copyright 2013 Damyon Wiese (damyon@moodle.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -defined('MOODLE_INTERNAL') || die(); - -/** - * Mathjax filtering - */ -class filter_mathjaxloader extends moodle_text_filter { - +class text_filter extends \core_filters\text_filter { /* * Perform a mapping of the moodle language code to the equivalent for MathJax. * @@ -83,12 +80,12 @@ class filter_mathjaxloader extends moodle_text_filter { if ($page->requires->should_create_one_time_item_now('filter_mathjaxloader-scripts')) { $url = get_config('filter_mathjaxloader', 'httpsurl'); $lang = $this->map_language_code(current_language()); - $url = new moodle_url($url, array('delayStartupUntil' => 'configured')); + $url = new url($url, array('delayStartupUntil' => 'configured')); $page->requires->js($url); $config = get_config('filter_mathjaxloader', 'mathjaxconfig'); - $wwwroot = new moodle_url('/'); + $wwwroot = new url('/'); $config = str_replace('{wwwroot}', $wwwroot->out(true), $config); diff --git a/filter/mathjaxloader/tests/filtermath_test.php b/filter/mathjaxloader/tests/filtermath_test.php index 5e33d25a37f..e66ef163291 100644 --- a/filter/mathjaxloader/tests/filtermath_test.php +++ b/filter/mathjaxloader/tests/filtermath_test.php @@ -16,11 +16,7 @@ namespace filter_mathjaxloader; -use filter_mathjaxloader; - -defined('MOODLE_INTERNAL') || die(); -global $CFG; -require_once($CFG->dirroot.'/filter/mathjaxloader/filter.php'); +use core\context\system as context_system; /** * Unit tests for the MathJax loader filter. @@ -30,10 +26,9 @@ require_once($CFG->dirroot.'/filter/mathjaxloader/filter.php'); * @copyright 2018 Markku Riekkinen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class filtermath_test extends \advanced_testcase { - +final class filtermath_test extends \advanced_testcase { /** - * Test the functionality of {@link filter_mathjaxloader::filter()}. + * Test the functionality of {@link text_filter::filter()}. * * @param string $inputtext The text given by the user. * @param string $expected The expected output after filtering. @@ -41,7 +36,7 @@ class filtermath_test extends \advanced_testcase { * @dataProvider math_filtering_inputs */ public function test_math_filtering($inputtext, $expected): void { - $filter = new filter_mathjaxloader(\context_system::instance(), []); + $filter = new text_filter(context_system::instance(), []); $this->assertEquals($expected, $filter->filter($inputtext)); } @@ -50,38 +45,38 @@ class filtermath_test extends \advanced_testcase { * * @return array of [inputtext, expectedoutput] tuples. */ - public function math_filtering_inputs() { + public static function math_filtering_inputs() { return [ // One inline formula. ['Some inline math \\( y = x^2 \\).', - 'Some inline math \\( y = x^2 \\).'], + 'Some inline math \\( y = x^2 \\).', ], // One inline and one display. ['Some inline math \\( y = x^2 \\) and display formula \\[ S = \\sum_{n=1}^{\\infty} 2^n \\]', 'Some inline math \\( y = x^2 \\) and ' - . 'display formula \\[ S = \\sum_{n=1}^{\\infty} 2^n \\]'], + . 'display formula \\[ S = \\sum_{n=1}^{\\infty} 2^n \\]', ], // One display and one inline. ['Display formula \\[ S = \\sum_{n=1}^{\\infty} 2^n \\] and some inline math \\( y = x^2 \\).', 'Display formula \\[ S = \\sum_{n=1}^{\\infty} 2^n \\] and ' - . 'some inline math \\( y = x^2 \\).'], + . 'some inline math \\( y = x^2 \\).', ], // One inline and one display (with dollars). ['Some inline math \\( y = x^2 \\) and display formula $$ S = \\sum_{n=1}^{\\infty} 2^n $$', 'Some inline math \\( y = x^2 \\) and ' - . 'display formula $$ S = \\sum_{n=1}^{\\infty} 2^n $$'], + . 'display formula $$ S = \\sum_{n=1}^{\\infty} 2^n $$', ], // One display (with dollars) and one inline. ['Display formula $$ S = \\sum_{n=1}^{\\infty} 2^n $$ and some inline math \\( y = x^2 \\).', 'Display formula $$ S = \\sum_{n=1}^{\\infty} 2^n $$ and ' - . 'some inline math \\( y = x^2 \\).'], + . 'some inline math \\( y = x^2 \\).', ], // Inline math environment nested inside display environment (using a custom LaTex macro). ['\\[ \\newcommand{\\False}{\\mathsf{F}} \\newcommand{\\NullF}{\\fbox{\\(\\False\\)}} \\] ' . 'Text with inline formula using the custom LaTex macro \\( a = \\NullF \\).', '' . '\\[ \\newcommand{\\False}{\\mathsf{F}} \\newcommand{\\NullF}{\\fbox{\\(\\False\\)}} \\] ' - . 'Text with inline formula using the custom LaTex macro \\( a = \\NullF \\).'], + . 'Text with inline formula using the custom LaTex macro \\( a = \\NullF \\).', ], // Nested environments and some more content. ['\\[ \\newcommand{\\False}{\\mathsf{F}} \\newcommand{\\NullF}{\\fbox{\\(\\False\\)}} \\] ' @@ -90,13 +85,13 @@ class filtermath_test extends \advanced_testcase { '' . '\\[ \\newcommand{\\False}{\\mathsf{F}} \\newcommand{\\NullF}{\\fbox{\\(\\False\\)}} \\] ' . 'Text with inline formula using the custom LaTex macro \\( a = \\NullF \\). ' - . 'Finally, a display formula $$ b = \\NullF $$'], + . 'Finally, a display formula $$ b = \\NullF $$', ], // Broken math: the delimiters ($$) are not closed. ['Writing text and starting display math. $$ k = i^3 \\newcommand{\\False}{\\mathsf{F}} \\newcommand{\\NullF}{\\fbox{\\(\\False\\)}} ' . 'More text and inline math \\( x = \\NullF \\).', 'Writing text and starting display math. $$ k = i^3 \\newcommand{\\False}{\\mathsf{F}} \\newcommand{\\NullF}{\\fbox{\\(\\False\\)}} ' - . 'More text and inline math \\( x = \\NullF \\).'], + . 'More text and inline math \\( x = \\NullF \\).', ], ]; } } diff --git a/filter/mathjaxloader/tests/filter_test.php b/filter/mathjaxloader/tests/text_filter_test.php similarity index 84% rename from filter/mathjaxloader/tests/filter_test.php rename to filter/mathjaxloader/tests/text_filter_test.php index 21107d6435c..e5474b1ac5f 100644 --- a/filter/mathjaxloader/tests/filter_test.php +++ b/filter/mathjaxloader/tests/text_filter_test.php @@ -16,13 +16,6 @@ namespace filter_mathjaxloader; -use filter_mathjaxloader; - -defined('MOODLE_INTERNAL') || die(); - -global $CFG; -require_once($CFG->dirroot.'/filter/mathjaxloader/filter.php'); - /** * Unit tests for the MathJax loader filter. * @@ -30,11 +23,11 @@ require_once($CFG->dirroot.'/filter/mathjaxloader/filter.php'); * @category test * @copyright 2017 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \filter_mathjaxloader\text_filter */ -class filter_test extends \advanced_testcase { - +final class text_filter_test extends \advanced_testcase { /** - * Test the functionality of {@link filter_mathjaxloader::map_language_code()}. + * Test the functionality of {@link text_filter::map_language_code()}. * * @param string $moodlelangcode the user's current language * @param string $mathjaxlangcode the mathjax language to be used for the moodle language @@ -42,8 +35,7 @@ class filter_test extends \advanced_testcase { * @dataProvider map_language_code_expected_mappings */ public function test_map_language_code($moodlelangcode, $mathjaxlangcode): void { - - $filter = new filter_mathjaxloader(\context_system::instance(), []); + $filter = new text_filter(\context_system::instance(), []); $this->assertEquals($mathjaxlangcode, $filter->map_language_code($moodlelangcode)); } @@ -52,8 +44,7 @@ class filter_test extends \advanced_testcase { * * @return array of [moodlelangcode, mathjaxcode] tuples */ - public function map_language_code_expected_mappings() { - + public static function map_language_code_expected_mappings() { return [ ['cz', 'cs'], // Explicit mapping. ['cs', 'cs'], // Implicit mapping (exact match). diff --git a/filter/mediaplugin/filter.php b/filter/mediaplugin/classes/text_filter.php similarity index 94% rename from filter/mediaplugin/filter.php rename to filter/mediaplugin/classes/text_filter.php index 7d70f8368c3..ed814c8d9e7 100644 --- a/filter/mediaplugin/filter.php +++ b/filter/mediaplugin/classes/text_filter.php @@ -14,19 +14,13 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Media plugin filtering - * - * This filter will replace any links to a media file with - * a media plugin that plays that media inline - * - * @package filter - * @subpackage mediaplugin - * @copyright 2004 onwards Martin Dougiamas {@link http://moodle.com} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ +namespace filter_mediaplugin; -defined('MOODLE_INTERNAL') || die(); +use core\context; +use core\url; +use core_media_manager; +use core_media_player_native; +use moodle_page; /** * Automatic media embedding filter class. @@ -39,7 +33,7 @@ defined('MOODLE_INTERNAL') || die(); * @copyright 2004 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class filter_mediaplugin extends moodle_text_filter { +class text_filter extends \core_filters\text_filter { /** @var bool True if currently filtering trusted text */ private $trusted; @@ -61,6 +55,7 @@ class filter_mediaplugin extends moodle_text_filter { $mediamanager = core_media_manager::instance($page); } + #[\Override] public function filter($text, array $options = array()) { global $CFG, $PAGE; @@ -215,11 +210,11 @@ class filter_mediaplugin extends moodle_text_filter { // Find all sources both as