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 *