MDL-82427 core_filters: Move filter classes to autoloading

This commit is contained in:
Andrew Nicols 2024-07-10 09:49:07 +08:00
parent 554a790bf0
commit ae2f69a898
No known key found for this signature in database
GPG Key ID: 6D1E3157C8CFBF14
7 changed files with 671 additions and 574 deletions

View File

@ -0,0 +1,283 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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 <nolink> tags for XHTML compatibility after the last filtering stage.
$text = str_replace(array('<nolink>', '</nolink>'), '', $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);
}
}
}

View File

@ -0,0 +1,97 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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 '<span class="highlight">'.
* @param string $hreftagend HTML to insert after any match. Default '</span>'.
* @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 = '<span class="highlight">',
$hreftagend = '</span>',
$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;
}
}

View File

@ -0,0 +1,59 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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');
}
}

View File

@ -0,0 +1,76 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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',
));
}
}

View File

@ -0,0 +1,134 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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);
}
}

View File

@ -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',
],
];

View File

@ -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 <nolink> tags for XHTML compatibility after the last filtering stage.
$text = str_replace(array('<nolink>', '</nolink>'), '', $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 '<span class="highlight">'.
* @param string $hreftagend HTML to insert after any match. Default '</span>'.
* @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 = '<span class="highlight">',
$hreftagend = '</span>',
$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
*