PageRenderTime 52ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/filterlib.php

https://bitbucket.org/moodle/moodle
PHP | 1622 lines | 997 code | 143 blank | 482 comment | 85 complexity | 3270726963f0a4978febef0c0aebed6a MD5 | raw file
Possible License(s): Apache-2.0, LGPL-2.1, BSD-3-Clause, MIT, GPL-3.0

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * Library functions for managing text filter plugins.
  18. *
  19. * @package core
  20. * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
  21. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  22. */
  23. defined('MOODLE_INTERNAL') || die();
  24. /** The states a filter can be in, stored in the filter_active table. */
  25. define('TEXTFILTER_ON', 1);
  26. /** The states a filter can be in, stored in the filter_active table. */
  27. define('TEXTFILTER_INHERIT', 0);
  28. /** The states a filter can be in, stored in the filter_active table. */
  29. define('TEXTFILTER_OFF', -1);
  30. /** The states a filter can be in, stored in the filter_active table. */
  31. define('TEXTFILTER_DISABLED', -9999);
  32. /**
  33. * Define one exclusive separator that we'll use in the temp saved tags
  34. * keys. It must be something rare enough to avoid having matches with
  35. * filterobjects. MDL-18165
  36. */
  37. define('TEXTFILTER_EXCL_SEPARATOR', chr(0x1F) . '%' . chr(0x1F));
  38. /**
  39. * Class to manage the filtering of strings. It is intended that this class is
  40. * only used by weblib.php. Client code should probably be using the
  41. * format_text and format_string functions.
  42. *
  43. * This class is a singleton.
  44. *
  45. * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
  46. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  47. */
  48. class filter_manager {
  49. /**
  50. * @var moodle_text_filter[][] This list of active filters, by context, for filtering content.
  51. * An array contextid => ordered array of filter name => filter objects.
  52. */
  53. protected $textfilters = array();
  54. /**
  55. * @var moodle_text_filter[][] This list of active filters, by context, for filtering strings.
  56. * An array contextid => ordered array of filter name => filter objects.
  57. */
  58. protected $stringfilters = array();
  59. /** @var array Exploded version of $CFG->stringfilters. */
  60. protected $stringfilternames = array();
  61. /** @var filter_manager Holds the singleton instance. */
  62. protected static $singletoninstance;
  63. /**
  64. * Constructor. Protected. Use {@link instance()} instead.
  65. */
  66. protected function __construct() {
  67. $this->stringfilternames = filter_get_string_filters();
  68. }
  69. /**
  70. * Factory method. Use this to get the filter manager.
  71. *
  72. * @return filter_manager the singleton instance.
  73. */
  74. public static function instance() {
  75. global $CFG;
  76. if (is_null(self::$singletoninstance)) {
  77. if (!empty($CFG->perfdebug) and $CFG->perfdebug > 7) {
  78. self::$singletoninstance = new performance_measuring_filter_manager();
  79. } else {
  80. self::$singletoninstance = new self();
  81. }
  82. }
  83. return self::$singletoninstance;
  84. }
  85. /**
  86. * Resets the caches, usually to be called between unit tests
  87. */
  88. public static function reset_caches() {
  89. if (self::$singletoninstance) {
  90. self::$singletoninstance->unload_all_filters();
  91. }
  92. self::$singletoninstance = null;
  93. }
  94. /**
  95. * Unloads all filters and other cached information
  96. */
  97. protected function unload_all_filters() {
  98. $this->textfilters = array();
  99. $this->stringfilters = array();
  100. $this->stringfilternames = array();
  101. }
  102. /**
  103. * Load all the filters required by this context.
  104. *
  105. * @param context $context the context.
  106. */
  107. protected function load_filters($context) {
  108. $filters = filter_get_active_in_context($context);
  109. $this->textfilters[$context->id] = array();
  110. $this->stringfilters[$context->id] = array();
  111. foreach ($filters as $filtername => $localconfig) {
  112. $filter = $this->make_filter_object($filtername, $context, $localconfig);
  113. if (is_null($filter)) {
  114. continue;
  115. }
  116. $this->textfilters[$context->id][$filtername] = $filter;
  117. if (in_array($filtername, $this->stringfilternames)) {
  118. $this->stringfilters[$context->id][$filtername] = $filter;
  119. }
  120. }
  121. }
  122. /**
  123. * Factory method for creating a filter.
  124. *
  125. * @param string $filtername The filter name, for example 'tex'.
  126. * @param context $context context object.
  127. * @param array $localconfig array of local configuration variables for this filter.
  128. * @return moodle_text_filter The filter, or null, if this type of filter is
  129. * not recognised or could not be created.
  130. */
  131. protected function make_filter_object($filtername, $context, $localconfig) {
  132. global $CFG;
  133. $path = $CFG->dirroot .'/filter/'. $filtername .'/filter.php';
  134. if (!is_readable($path)) {
  135. return null;
  136. }
  137. include_once($path);
  138. $filterclassname = 'filter_' . $filtername;
  139. if (class_exists($filterclassname)) {
  140. return new $filterclassname($context, $localconfig);
  141. }
  142. return null;
  143. }
  144. /**
  145. * Apply a list of filters to some content.
  146. * @param string $text
  147. * @param moodle_text_filter[] $filterchain array filter name => filter object.
  148. * @param array $options options passed to the filters.
  149. * @param array $skipfilters of filter names. Any filters that should not be applied to this text.
  150. * @return string $text
  151. */
  152. protected function apply_filter_chain($text, $filterchain, array $options = array(),
  153. array $skipfilters = null) {
  154. foreach ($filterchain as $filtername => $filter) {
  155. if ($skipfilters !== null && in_array($filtername, $skipfilters)) {
  156. continue;
  157. }
  158. $text = $filter->filter($text, $options);
  159. }
  160. return $text;
  161. }
  162. /**
  163. * Get all the filters that apply to a given context for calls to format_text.
  164. *
  165. * @param context $context
  166. * @return moodle_text_filter[] A text filter
  167. */
  168. protected function get_text_filters($context) {
  169. if (!isset($this->textfilters[$context->id])) {
  170. $this->load_filters($context);
  171. }
  172. return $this->textfilters[$context->id];
  173. }
  174. /**
  175. * Get all the filters that apply to a given context for calls to format_string.
  176. *
  177. * @param context $context the context.
  178. * @return moodle_text_filter[] A text filter
  179. */
  180. protected function get_string_filters($context) {
  181. if (!isset($this->stringfilters[$context->id])) {
  182. $this->load_filters($context);
  183. }
  184. return $this->stringfilters[$context->id];
  185. }
  186. /**
  187. * Filter some text
  188. *
  189. * @param string $text The text to filter
  190. * @param context $context the context.
  191. * @param array $options options passed to the filters
  192. * @param array $skipfilters of filter names. Any filters that should not be applied to this text.
  193. * @return string resulting text
  194. */
  195. public function filter_text($text, $context, array $options = array(),
  196. array $skipfilters = null) {
  197. $text = $this->apply_filter_chain($text, $this->get_text_filters($context), $options, $skipfilters);
  198. // Remove <nolink> tags for XHTML compatibility.
  199. $text = str_replace(array('<nolink>', '</nolink>'), '', $text);
  200. return $text;
  201. }
  202. /**
  203. * Filter a piece of string
  204. *
  205. * @param string $string The text to filter
  206. * @param context $context the context.
  207. * @return string resulting string
  208. */
  209. public function filter_string($string, $context) {
  210. return $this->apply_filter_chain($string, $this->get_string_filters($context));
  211. }
  212. /**
  213. * @deprecated Since Moodle 3.0 MDL-50491. This was used by the old text filtering system, but no more.
  214. */
  215. public function text_filtering_hash() {
  216. throw new coding_exception('filter_manager::text_filtering_hash() can not be used any more');
  217. }
  218. /**
  219. * Setup page with filters requirements and other prepare stuff.
  220. *
  221. * This method is used by {@see format_text()} and {@see format_string()}
  222. * in order to allow filters to setup any page requirement (js, css...)
  223. * or perform any action needed to get them prepared before filtering itself
  224. * happens by calling to each every active setup() method.
  225. *
  226. * Note it's executed for each piece of text filtered, so filter implementations
  227. * are responsible of controlling the cardinality of the executions that may
  228. * be different depending of the stuff to prepare.
  229. *
  230. * @param moodle_page $page the page we are going to add requirements to.
  231. * @param context $context the context which contents are going to be filtered.
  232. * @since Moodle 2.3
  233. */
  234. public function setup_page_for_filters($page, $context) {
  235. $filters = $this->get_text_filters($context);
  236. foreach ($filters as $filter) {
  237. $filter->setup($page, $context);
  238. }
  239. }
  240. /**
  241. * Setup the page for globally available filters.
  242. *
  243. * This helps setting up the page for filters which may be applied to
  244. * the page, even if they do not belong to the current context, or are
  245. * not yet visible because the content is lazily added (ajax). This method
  246. * always uses to the system context which determines the globally
  247. * available filters.
  248. *
  249. * This should only ever be called once per request.
  250. *
  251. * @param moodle_page $page The page.
  252. * @since Moodle 3.2
  253. */
  254. public function setup_page_for_globally_available_filters($page) {
  255. $context = context_system::instance();
  256. $filterdata = filter_get_globally_enabled_filters_with_config();
  257. foreach ($filterdata as $name => $config) {
  258. if (isset($this->textfilters[$context->id][$name])) {
  259. $filter = $this->textfilters[$context->id][$name];
  260. } else {
  261. $filter = $this->make_filter_object($name, $context, $config);
  262. if (is_null($filter)) {
  263. continue;
  264. }
  265. }
  266. $filter->setup($page, $context);
  267. }
  268. }
  269. }
  270. /**
  271. * Filter manager subclass that does nothing. Having this simplifies the logic
  272. * of format_text, etc.
  273. *
  274. * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
  275. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  276. */
  277. class null_filter_manager {
  278. /**
  279. * As for the equivalent {@link filter_manager} method.
  280. *
  281. * @param string $text The text to filter
  282. * @param context $context not used.
  283. * @param array $options not used
  284. * @param array $skipfilters not used
  285. * @return string resulting text.
  286. */
  287. public function filter_text($text, $context, array $options = array(),
  288. array $skipfilters = null) {
  289. return $text;
  290. }
  291. /**
  292. * As for the equivalent {@link filter_manager} method.
  293. *
  294. * @param string $string The text to filter
  295. * @param context $context not used.
  296. * @return string resulting string
  297. */
  298. public function filter_string($string, $context) {
  299. return $string;
  300. }
  301. /**
  302. * As for the equivalent {@link filter_manager} method.
  303. *
  304. * @deprecated Since Moodle 3.0 MDL-50491.
  305. */
  306. public function text_filtering_hash() {
  307. throw new coding_exception('filter_manager::text_filtering_hash() can not be used any more');
  308. }
  309. }
  310. /**
  311. * Filter manager subclass that tracks how much work it does.
  312. *
  313. * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
  314. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  315. */
  316. class performance_measuring_filter_manager extends filter_manager {
  317. /** @var int number of filter objects created. */
  318. protected $filterscreated = 0;
  319. /** @var int number of calls to filter_text. */
  320. protected $textsfiltered = 0;
  321. /** @var int number of calls to filter_string. */
  322. protected $stringsfiltered = 0;
  323. protected function unload_all_filters() {
  324. parent::unload_all_filters();
  325. $this->filterscreated = 0;
  326. $this->textsfiltered = 0;
  327. $this->stringsfiltered = 0;
  328. }
  329. protected function make_filter_object($filtername, $context, $localconfig) {
  330. $this->filterscreated++;
  331. return parent::make_filter_object($filtername, $context, $localconfig);
  332. }
  333. public function filter_text($text, $context, array $options = array(),
  334. array $skipfilters = null) {
  335. $this->textsfiltered++;
  336. return parent::filter_text($text, $context, $options, $skipfilters);
  337. }
  338. public function filter_string($string, $context) {
  339. $this->stringsfiltered++;
  340. return parent::filter_string($string, $context);
  341. }
  342. /**
  343. * Return performance information, in the form required by {@link get_performance_info()}.
  344. * @return array the performance info.
  345. */
  346. public function get_performance_summary() {
  347. return array(array(
  348. 'contextswithfilters' => count($this->textfilters),
  349. 'filterscreated' => $this->filterscreated,
  350. 'textsfiltered' => $this->textsfiltered,
  351. 'stringsfiltered' => $this->stringsfiltered,
  352. ), array(
  353. 'contextswithfilters' => 'Contexts for which filters were loaded',
  354. 'filterscreated' => 'Filters created',
  355. 'textsfiltered' => 'Pieces of content filtered',
  356. 'stringsfiltered' => 'Strings filtered',
  357. ));
  358. }
  359. }
  360. /**
  361. * Base class for text filters. You just need to override this class and
  362. * implement the filter method.
  363. *
  364. * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
  365. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  366. */
  367. abstract class moodle_text_filter {
  368. /** @var context The context we are in. */
  369. protected $context;
  370. /** @var array Any local configuration for this filter in this context. */
  371. protected $localconfig;
  372. /**
  373. * Set any context-specific configuration for this filter.
  374. *
  375. * @param context $context The current context.
  376. * @param array $localconfig Any context-specific configuration for this filter.
  377. */
  378. public function __construct($context, array $localconfig) {
  379. $this->context = $context;
  380. $this->localconfig = $localconfig;
  381. }
  382. /**
  383. * @deprecated Since Moodle 3.0 MDL-50491. This was used by the old text filtering system, but no more.
  384. */
  385. public function hash() {
  386. throw new coding_exception('moodle_text_filter::hash() can not be used any more');
  387. }
  388. /**
  389. * Setup page with filter requirements and other prepare stuff.
  390. *
  391. * Override this method if the filter needs to setup page
  392. * requirements or needs other stuff to be executed.
  393. *
  394. * Note this method is invoked from {@see setup_page_for_filters()}
  395. * for each piece of text being filtered, so it is responsible
  396. * for controlling its own execution cardinality.
  397. *
  398. * @param moodle_page $page the page we are going to add requirements to.
  399. * @param context $context the context which contents are going to be filtered.
  400. * @since Moodle 2.3
  401. */
  402. public function setup($page, $context) {
  403. // Override me, if needed.
  404. }
  405. /**
  406. * Override this function to actually implement the filtering.
  407. *
  408. * @param string $text some HTML content to process.
  409. * @param array $options options passed to the filters
  410. * @return string the HTML content after the filtering has been applied.
  411. */
  412. public abstract function filter($text, array $options = array());
  413. }
  414. /**
  415. * This is just a little object to define a phrase and some instructions
  416. * for how to process it. Filters can create an array of these to pass
  417. * to the @{link filter_phrases()} function below.
  418. *
  419. * Note that although the fields here are public, you almost certainly should
  420. * never use that. All that is supported is contructing new instances of this
  421. * class, and then passing an array of them to filter_phrases.
  422. *
  423. * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
  424. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  425. */
  426. class filterobject {
  427. /** @var string this is the phrase that should be matched. */
  428. public $phrase;
  429. /** @var bool whether to match complete words. If true, 'T' won't be matched in 'Tim'. */
  430. public $fullmatch;
  431. /** @var bool whether the match needs to be case sensitive. */
  432. public $casesensitive;
  433. /** @var string HTML to insert before any match. */
  434. public $hreftagbegin;
  435. /** @var string HTML to insert after any match. */
  436. public $hreftagend;
  437. /** @var null|string replacement text to go inside begin and end. If not set,
  438. * the body of the replacement will be the original phrase.
  439. */
  440. public $replacementphrase;
  441. /** @var null|string once initialised, holds the regexp for matching this phrase. */
  442. public $workregexp = null;
  443. /** @var null|string once initialised, holds the mangled HTML to replace the regexp with. */
  444. public $workreplacementphrase = null;
  445. /**
  446. * Constructor.
  447. *
  448. * @param string $phrase this is the phrase that should be matched.
  449. * @param string $hreftagbegin HTML to insert before any match. Default '<span class="highlight">'.
  450. * @param string $hreftagend HTML to insert after any match. Default '</span>'.
  451. * @param bool $casesensitive whether the match needs to be case sensitive
  452. * @param bool $fullmatch whether to match complete words. If true, 'T' won't be matched in 'Tim'.
  453. * @param mixed $replacementphrase replacement text to go inside begin and end. If not set,
  454. * the body of the replacement will be the original phrase.
  455. * @param callback $replacementcallback if set, then this will be called just before
  456. * $hreftagbegin, $hreftagend and $replacementphrase are needed, so they can be computed only if required.
  457. * The call made is
  458. * list($linkobject->hreftagbegin, $linkobject->hreftagend, $linkobject->replacementphrase) =
  459. * call_user_func_array($linkobject->replacementcallback, $linkobject->replacementcallbackdata);
  460. * so the return should be an array [$hreftagbegin, $hreftagend, $replacementphrase], the last of which may be null.
  461. * @param array $replacementcallbackdata data to be passed to $replacementcallback (optional).
  462. */
  463. public function __construct($phrase, $hreftagbegin = '<span class="highlight">',
  464. $hreftagend = '</span>',
  465. $casesensitive = false,
  466. $fullmatch = false,
  467. $replacementphrase = null,
  468. $replacementcallback = null,
  469. array $replacementcallbackdata = null) {
  470. $this->phrase = $phrase;
  471. $this->hreftagbegin = $hreftagbegin;
  472. $this->hreftagend = $hreftagend;
  473. $this->casesensitive = !empty($casesensitive);
  474. $this->fullmatch = !empty($fullmatch);
  475. $this->replacementphrase = $replacementphrase;
  476. $this->replacementcallback = $replacementcallback;
  477. $this->replacementcallbackdata = $replacementcallbackdata;
  478. }
  479. }
  480. /**
  481. * Look up the name of this filter
  482. *
  483. * @param string $filter the filter name
  484. * @return string the human-readable name for this filter.
  485. */
  486. function filter_get_name($filter) {
  487. if (strpos($filter, 'filter/') === 0) {
  488. debugging("Old '$filter'' parameter used in filter_get_name()");
  489. $filter = substr($filter, 7);
  490. } else if (strpos($filter, '/') !== false) {
  491. throw new coding_exception('Unknown filter type ' . $filter);
  492. }
  493. if (get_string_manager()->string_exists('filtername', 'filter_' . $filter)) {
  494. return get_string('filtername', 'filter_' . $filter);
  495. } else {
  496. return $filter;
  497. }
  498. }
  499. /**
  500. * Get the names of all the filters installed in this Moodle.
  501. *
  502. * @return array path => filter name from the appropriate lang file. e.g.
  503. * array('tex' => 'TeX Notation');
  504. * sorted in alphabetical order of name.
  505. */
  506. function filter_get_all_installed() {
  507. $filternames = array();
  508. foreach (core_component::get_plugin_list('filter') as $filter => $fulldir) {
  509. if (is_readable("$fulldir/filter.php")) {
  510. $filternames[$filter] = filter_get_name($filter);
  511. }
  512. }
  513. core_collator::asort($filternames);
  514. return $filternames;
  515. }
  516. /**
  517. * Set the global activated state for a text filter.
  518. *
  519. * @param string $filtername The filter name, for example 'tex'.
  520. * @param int $state One of the values TEXTFILTER_ON, TEXTFILTER_OFF or TEXTFILTER_DISABLED.
  521. * @param int $move -1 means up, 0 means the same, 1 means down
  522. */
  523. function filter_set_global_state($filtername, $state, $move = 0) {
  524. global $DB;
  525. // Check requested state is valid.
  526. if (!in_array($state, array(TEXTFILTER_ON, TEXTFILTER_OFF, TEXTFILTER_DISABLED))) {
  527. throw new coding_exception("Illegal option '$state' passed to filter_set_global_state. " .
  528. "Must be one of TEXTFILTER_ON, TEXTFILTER_OFF or TEXTFILTER_DISABLED.");
  529. }
  530. if ($move > 0) {
  531. $move = 1;
  532. } else if ($move < 0) {
  533. $move = -1;
  534. }
  535. if (strpos($filtername, 'filter/') === 0) {
  536. $filtername = substr($filtername, 7);
  537. } else if (strpos($filtername, '/') !== false) {
  538. throw new coding_exception("Invalid filter name '$filtername' used in filter_set_global_state()");
  539. }
  540. $transaction = $DB->start_delegated_transaction();
  541. $syscontext = context_system::instance();
  542. $filters = $DB->get_records('filter_active', array('contextid' => $syscontext->id), 'sortorder ASC');
  543. $on = array();
  544. $off = array();
  545. foreach ($filters as $f) {
  546. if ($f->active == TEXTFILTER_DISABLED) {
  547. $off[$f->filter] = $f;
  548. } else {
  549. $on[$f->filter] = $f;
  550. }
  551. }
  552. // Update the state or add new record.
  553. if (isset($on[$filtername])) {
  554. $filter = $on[$filtername];
  555. if ($filter->active != $state) {
  556. add_to_config_log('filter_active', $filter->active, $state, $filtername);
  557. $filter->active = $state;
  558. $DB->update_record('filter_active', $filter);
  559. if ($filter->active == TEXTFILTER_DISABLED) {
  560. unset($on[$filtername]);
  561. $off = array($filter->filter => $filter) + $off;
  562. }
  563. }
  564. } else if (isset($off[$filtername])) {
  565. $filter = $off[$filtername];
  566. if ($filter->active != $state) {
  567. add_to_config_log('filter_active', $filter->active, $state, $filtername);
  568. $filter->active = $state;
  569. $DB->update_record('filter_active', $filter);
  570. if ($filter->active != TEXTFILTER_DISABLED) {
  571. unset($off[$filtername]);
  572. $on[$filter->filter] = $filter;
  573. }
  574. }
  575. } else {
  576. add_to_config_log('filter_active', '', $state, $filtername);
  577. $filter = new stdClass();
  578. $filter->filter = $filtername;
  579. $filter->contextid = $syscontext->id;
  580. $filter->active = $state;
  581. $filter->sortorder = 99999;
  582. $filter->id = $DB->insert_record('filter_active', $filter);
  583. $filters[$filter->id] = $filter;
  584. if ($state == TEXTFILTER_DISABLED) {
  585. $off[$filter->filter] = $filter;
  586. } else {
  587. $on[$filter->filter] = $filter;
  588. }
  589. }
  590. // Move only active.
  591. if ($move != 0 and isset($on[$filter->filter])) {
  592. $i = 1;
  593. foreach ($on as $f) {
  594. $f->newsortorder = $i;
  595. $i++;
  596. }
  597. $filter->newsortorder = $filter->newsortorder + $move;
  598. foreach ($on as $f) {
  599. if ($f->id == $filter->id) {
  600. continue;
  601. }
  602. if ($f->newsortorder == $filter->newsortorder) {
  603. if ($move == 1) {
  604. $f->newsortorder = $f->newsortorder - 1;
  605. } else {
  606. $f->newsortorder = $f->newsortorder + 1;
  607. }
  608. }
  609. }
  610. core_collator::asort_objects_by_property($on, 'newsortorder', core_collator::SORT_NUMERIC);
  611. }
  612. // Inactive are sorted by filter name.
  613. core_collator::asort_objects_by_property($off, 'filter', core_collator::SORT_NATURAL);
  614. // Update records if necessary.
  615. $i = 1;
  616. foreach ($on as $f) {
  617. if ($f->sortorder != $i) {
  618. $DB->set_field('filter_active', 'sortorder', $i, array('id' => $f->id));
  619. }
  620. $i++;
  621. }
  622. foreach ($off as $f) {
  623. if ($f->sortorder != $i) {
  624. $DB->set_field('filter_active', 'sortorder', $i, array('id' => $f->id));
  625. }
  626. $i++;
  627. }
  628. $transaction->allow_commit();
  629. }
  630. /**
  631. * Returns the active state for a filter in the given context.
  632. *
  633. * @param string $filtername The filter name, for example 'tex'.
  634. * @param integer $contextid The id of the context to get the data for.
  635. * @return int value of active field for the given filter.
  636. */
  637. function filter_get_active_state(string $filtername, $contextid = null): int {
  638. global $DB;
  639. if ($contextid === null) {
  640. $contextid = context_system::instance()->id;
  641. }
  642. if (is_object($contextid)) {
  643. $contextid = $contextid->id;
  644. }
  645. if (strpos($filtername, 'filter/') === 0) {
  646. $filtername = substr($filtername, 7);
  647. } else if (strpos($filtername, '/') !== false) {
  648. throw new coding_exception("Invalid filter name '$filtername' used in filter_is_enabled()");
  649. }
  650. if ($active = $DB->get_field('filter_active', 'active', array('filter' => $filtername, 'contextid' => $contextid))) {
  651. return $active;
  652. }
  653. return TEXTFILTER_DISABLED;
  654. }
  655. /**
  656. * @param string $filtername The filter name, for example 'tex'.
  657. * @return boolean is this filter allowed to be used on this site. That is, the
  658. * admin has set the global 'active' setting to On, or Off, but available.
  659. */
  660. function filter_is_enabled($filtername) {
  661. if (strpos($filtername, 'filter/') === 0) {
  662. $filtername = substr($filtername, 7);
  663. } else if (strpos($filtername, '/') !== false) {
  664. throw new coding_exception("Invalid filter name '$filtername' used in filter_is_enabled()");
  665. }
  666. return array_key_exists($filtername, filter_get_globally_enabled());
  667. }
  668. /**
  669. * Return a list of all the filters that may be in use somewhere.
  670. *
  671. * @return array where the keys and values are both the filter name, like 'tex'.
  672. */
  673. function filter_get_globally_enabled() {
  674. $cache = \cache::make_from_params(\cache_store::MODE_REQUEST, 'core_filter', 'global_filters');
  675. $enabledfilters = $cache->get('enabled');
  676. if ($enabledfilters !== false) {
  677. return $enabledfilters;
  678. }
  679. $filters = filter_get_global_states();
  680. $enabledfilters = array();
  681. foreach ($filters as $filter => $filerinfo) {
  682. if ($filerinfo->active != TEXTFILTER_DISABLED) {
  683. $enabledfilters[$filter] = $filter;
  684. }
  685. }
  686. $cache->set('enabled', $enabledfilters);
  687. return $enabledfilters;
  688. }
  689. /**
  690. * Get the globally enabled filters.
  691. *
  692. * This returns the filters which could be used in any context. Essentially
  693. * the filters which are not disabled for the entire site.
  694. *
  695. * @return array Keys are filter names, and values the config.
  696. */
  697. function filter_get_globally_enabled_filters_with_config() {
  698. global $DB;
  699. $sql = "SELECT f.filter, fc.name, fc.value
  700. FROM {filter_active} f
  701. LEFT JOIN {filter_config} fc
  702. ON fc.filter = f.filter
  703. AND fc.contextid = f.contextid
  704. WHERE f.contextid = :contextid
  705. AND f.active != :disabled
  706. ORDER BY f.sortorder";
  707. $rs = $DB->get_recordset_sql($sql, [
  708. 'contextid' => context_system::instance()->id,
  709. 'disabled' => TEXTFILTER_DISABLED
  710. ]);
  711. // Massage the data into the specified format to return.
  712. $filters = array();
  713. foreach ($rs as $row) {
  714. if (!isset($filters[$row->filter])) {
  715. $filters[$row->filter] = array();
  716. }
  717. if ($row->name !== null) {
  718. $filters[$row->filter][$row->name] = $row->value;
  719. }
  720. }
  721. $rs->close();
  722. return $filters;
  723. }
  724. /**
  725. * Return the names of the filters that should also be applied to strings
  726. * (when they are enabled).
  727. *
  728. * @return array where the keys and values are both the filter name, like 'tex'.
  729. */
  730. function filter_get_string_filters() {
  731. global $CFG;
  732. $stringfilters = array();
  733. if (!empty($CFG->filterall) && !empty($CFG->stringfilters)) {
  734. $stringfilters = explode(',', $CFG->stringfilters);
  735. $stringfilters = array_combine($stringfilters, $stringfilters);
  736. }
  737. return $stringfilters;
  738. }
  739. /**
  740. * Sets whether a particular active filter should be applied to all strings by
  741. * format_string, or just used by format_text.
  742. *
  743. * @param string $filter The filter name, for example 'tex'.
  744. * @param boolean $applytostrings if true, this filter will apply to format_string
  745. * and format_text, when it is enabled.
  746. */
  747. function filter_set_applies_to_strings($filter, $applytostrings) {
  748. $stringfilters = filter_get_string_filters();
  749. $prevfilters = $stringfilters;
  750. $allfilters = core_component::get_plugin_list('filter');
  751. if ($applytostrings) {
  752. $stringfilters[$filter] = $filter;
  753. } else {
  754. unset($stringfilters[$filter]);
  755. }
  756. // Remove missing filters.
  757. foreach ($stringfilters as $filter) {
  758. if (!isset($allfilters[$filter])) {
  759. unset($stringfilters[$filter]);
  760. }
  761. }
  762. if ($prevfilters != $stringfilters) {
  763. set_config('stringfilters', implode(',', $stringfilters));
  764. set_config('filterall', !empty($stringfilters));
  765. }
  766. }
  767. /**
  768. * Set the local activated state for a text filter.
  769. *
  770. * @param string $filter The filter name, for example 'tex'.
  771. * @param integer $contextid The id of the context to get the local config for.
  772. * @param integer $state One of the values TEXTFILTER_ON, TEXTFILTER_OFF or TEXTFILTER_INHERIT.
  773. * @return void
  774. */
  775. function filter_set_local_state($filter, $contextid, $state) {
  776. global $DB;
  777. // Check requested state is valid.
  778. if (!in_array($state, array(TEXTFILTER_ON, TEXTFILTER_OFF, TEXTFILTER_INHERIT))) {
  779. throw new coding_exception("Illegal option '$state' passed to filter_set_local_state. " .
  780. "Must be one of TEXTFILTER_ON, TEXTFILTER_OFF or TEXTFILTER_INHERIT.");
  781. }
  782. if ($contextid == context_system::instance()->id) {
  783. throw new coding_exception('You cannot use filter_set_local_state ' .
  784. 'with $contextid equal to the system context id.');
  785. }
  786. if ($state == TEXTFILTER_INHERIT) {
  787. $DB->delete_records('filter_active', array('filter' => $filter, 'contextid' => $contextid));
  788. return;
  789. }
  790. $rec = $DB->get_record('filter_active', array('filter' => $filter, 'contextid' => $contextid));
  791. $insert = false;
  792. if (empty($rec)) {
  793. $insert = true;
  794. $rec = new stdClass;
  795. $rec->filter = $filter;
  796. $rec->contextid = $contextid;
  797. }
  798. $rec->active = $state;
  799. if ($insert) {
  800. $DB->insert_record('filter_active', $rec);
  801. } else {
  802. $DB->update_record('filter_active', $rec);
  803. }
  804. }
  805. /**
  806. * Set a particular local config variable for a filter in a context.
  807. *
  808. * @param string $filter The filter name, for example 'tex'.
  809. * @param integer $contextid The id of the context to get the local config for.
  810. * @param string $name the setting name.
  811. * @param string $value the corresponding value.
  812. */
  813. function filter_set_local_config($filter, $contextid, $name, $value) {
  814. global $DB;
  815. $rec = $DB->get_record('filter_config', array('filter' => $filter, 'contextid' => $contextid, 'name' => $name));
  816. $insert = false;
  817. if (empty($rec)) {
  818. $insert = true;
  819. $rec = new stdClass;
  820. $rec->filter = $filter;
  821. $rec->contextid = $contextid;
  822. $rec->name = $name;
  823. }
  824. $rec->value = $value;
  825. if ($insert) {
  826. $DB->insert_record('filter_config', $rec);
  827. } else {
  828. $DB->update_record('filter_config', $rec);
  829. }
  830. }
  831. /**
  832. * Remove a particular local config variable for a filter in a context.
  833. *
  834. * @param string $filter The filter name, for example 'tex'.
  835. * @param integer $contextid The id of the context to get the local config for.
  836. * @param string $name the setting name.
  837. */
  838. function filter_unset_local_config($filter, $contextid, $name) {
  839. global $DB;
  840. $DB->delete_records('filter_config', array('filter' => $filter, 'contextid' => $contextid, 'name' => $name));
  841. }
  842. /**
  843. * Get local config variables for a filter in a context. Normally (when your
  844. * filter is running) you don't need to call this, becuase the config is fetched
  845. * for you automatically. You only need this, for example, when you are getting
  846. * the config so you can show the user an editing from.
  847. *
  848. * @param string $filter The filter name, for example 'tex'.
  849. * @param integer $contextid The ID of the context to get the local config for.
  850. * @return array of name => value pairs.
  851. */
  852. function filter_get_local_config($filter, $contextid) {
  853. global $DB;
  854. return $DB->get_records_menu('filter_config', array('filter' => $filter, 'contextid' => $contextid), '', 'name,value');
  855. }
  856. /**
  857. * This function is for use by backup. Gets all the filter information specific
  858. * to one context.
  859. *
  860. * @param int $contextid
  861. * @return array Array with two elements. The first element is an array of objects with
  862. * fields filter and active. These come from the filter_active table. The
  863. * second element is an array of objects with fields filter, name and value
  864. * from the filter_config table.
  865. */
  866. function filter_get_all_local_settings($contextid) {
  867. global $DB;
  868. return array(
  869. $DB->get_records('filter_active', array('contextid' => $contextid), 'filter', 'filter,active'),
  870. $DB->get_records('filter_config', array('contextid' => $contextid), 'filter,name', 'filter,name,value'),
  871. );
  872. }
  873. /**
  874. * Get the list of active filters, in the order that they should be used
  875. * for a particular context, along with any local configuration variables.
  876. *
  877. * @param context $context a context
  878. * @return array an array where the keys are the filter names, for example
  879. * 'tex' and the values are any local
  880. * configuration for that filter, as an array of name => value pairs
  881. * from the filter_config table. In a lot of cases, this will be an
  882. * empty array. So, an example return value for this function might be
  883. * array(tex' => array())
  884. */
  885. function filter_get_active_in_context($context) {
  886. global $DB, $FILTERLIB_PRIVATE;
  887. if (!isset($FILTERLIB_PRIVATE)) {
  888. $FILTERLIB_PRIVATE = new stdClass();
  889. }
  890. // Use cache (this is a within-request cache only) if available. See
  891. // function filter_preload_activities.
  892. if (isset($FILTERLIB_PRIVATE->active) &&
  893. array_key_exists($context->id, $FILTERLIB_PRIVATE->active)) {
  894. return $FILTERLIB_PRIVATE->active[$context->id];
  895. }
  896. $contextids = str_replace('/', ',', trim($context->path, '/'));
  897. // The following SQL is tricky. It is explained on
  898. // http://docs.moodle.org/dev/Filter_enable/disable_by_context.
  899. $sql = "SELECT active.filter, fc.name, fc.value
  900. FROM (SELECT f.filter, MAX(f.sortorder) AS sortorder
  901. FROM {filter_active} f
  902. JOIN {context} ctx ON f.contextid = ctx.id
  903. WHERE ctx.id IN ($contextids)
  904. GROUP BY filter
  905. HAVING MAX(f.active * ctx.depth) > -MIN(f.active * ctx.depth)
  906. ) active
  907. LEFT JOIN {filter_config} fc ON fc.filter = active.filter AND fc.contextid = $context->id
  908. ORDER BY active.sortorder";
  909. $rs = $DB->get_recordset_sql($sql);
  910. // Massage the data into the specified format to return.
  911. $filters = array();
  912. foreach ($rs as $row) {
  913. if (!isset($filters[$row->filter])) {
  914. $filters[$row->filter] = array();
  915. }
  916. if (!is_null($row->name)) {
  917. $filters[$row->filter][$row->name] = $row->value;
  918. }
  919. }
  920. $rs->close();
  921. return $filters;
  922. }
  923. /**
  924. * Preloads the list of active filters for all activities (modules) on the course
  925. * using two database queries.
  926. *
  927. * @param course_modinfo $modinfo Course object from get_fast_modinfo
  928. */
  929. function filter_preload_activities(course_modinfo $modinfo) {
  930. global $DB, $FILTERLIB_PRIVATE;
  931. if (!isset($FILTERLIB_PRIVATE)) {
  932. $FILTERLIB_PRIVATE = new stdClass();
  933. }
  934. // Don't repeat preload.
  935. if (!isset($FILTERLIB_PRIVATE->preloaded)) {
  936. $FILTERLIB_PRIVATE->preloaded = array();
  937. }
  938. if (!empty($FILTERLIB_PRIVATE->preloaded[$modinfo->get_course_id()])) {
  939. return;
  940. }
  941. $FILTERLIB_PRIVATE->preloaded[$modinfo->get_course_id()] = true;
  942. // Get contexts for all CMs.
  943. $cmcontexts = array();
  944. $cmcontextids = array();
  945. foreach ($modinfo->get_cms() as $cm) {
  946. $modulecontext = context_module::instance($cm->id);
  947. $cmcontextids[] = $modulecontext->id;
  948. $cmcontexts[] = $modulecontext;
  949. }
  950. // Get course context and all other parents.
  951. $coursecontext = context_course::instance($modinfo->get_course_id());
  952. $parentcontextids = explode('/', substr($coursecontext->path, 1));
  953. $allcontextids = array_merge($cmcontextids, $parentcontextids);
  954. // Get all filter_active rows relating to all these contexts.
  955. list ($sql, $params) = $DB->get_in_or_equal($allcontextids);
  956. $filteractives = $DB->get_records_select('filter_active', "contextid $sql", $params, 'sortorder');
  957. // Get all filter_config only for the cm contexts.
  958. list ($sql, $params) = $DB->get_in_or_equal($cmcontextids);
  959. $filterconfigs = $DB->get_records_select('filter_config', "contextid $sql", $params);
  960. // Note: I was a bit surprised that filter_config only works for the
  961. // most specific context (i.e. it does not need to be checked for course
  962. // context if we only care about CMs) however basede on code in
  963. // filter_get_active_in_context, this does seem to be correct.
  964. // Build course default active list. Initially this will be an array of
  965. // filter name => active score (where an active score >0 means it's active).
  966. $courseactive = array();
  967. // Also build list of filter_active rows below course level, by contextid.
  968. $remainingactives = array();
  969. // Array lists filters that are banned at top level.
  970. $banned = array();
  971. // Add any active filters in parent contexts to the array.
  972. foreach ($filteractives as $row) {
  973. $depth = array_search($row->contextid, $parentcontextids);
  974. if ($depth !== false) {
  975. // Find entry.
  976. if (!array_key_exists($row->filter, $courseactive)) {
  977. $courseactive[$row->filter] = 0;
  978. }
  979. // This maths copes with reading rows in any order. Turning on/off
  980. // at site level counts 1, at next level down 4, at next level 9,
  981. // then 16, etc. This means the deepest level always wins, except
  982. // against the -9999 at top level.
  983. $courseactive[$row->filter] +=
  984. ($depth + 1) * ($depth + 1) * $row->active;
  985. if ($row->active == TEXTFILTER_DISABLED) {
  986. $banned[$row->filter] = true;
  987. }
  988. } else {
  989. // Build list of other rows indexed by contextid.
  990. if (!array_key_exists($row->contextid, $remainingactives)) {
  991. $remainingactives[$row->contextid] = array();
  992. }
  993. $remainingactives[$row->contextid][] = $row;
  994. }
  995. }
  996. // Chuck away the ones that aren't active.
  997. foreach ($courseactive as $filter => $score) {
  998. if ($score <= 0) {
  999. unset($courseactive[$filter]);
  1000. } else {
  1001. $courseactive[$filter] = array();
  1002. }
  1003. }
  1004. // Loop through the contexts to reconstruct filter_active lists for each
  1005. // cm on the course.
  1006. if (!isset($FILTERLIB_PRIVATE->active)) {
  1007. $FILTERLIB_PRIVATE->active = array();
  1008. }
  1009. foreach ($cmcontextids as $contextid) {
  1010. // Copy course list.
  1011. $FILTERLIB_PRIVATE->active[$contextid] = $courseactive;
  1012. // Are there any changes to the active list?
  1013. if (array_key_exists($contextid, $remainingactives)) {
  1014. foreach ($remainingactives[$contextid] as $row) {
  1015. if ($row->active > 0 && empty($banned[$row->filter])) {
  1016. // If it's marked active for specific context, add entry
  1017. // (doesn't matter if one exists already).
  1018. $FILTERLIB_PRIVATE->active[$contextid][$row->filter] = array();
  1019. } else {
  1020. // If it's marked inactive, remove entry (doesn't matter
  1021. // if it doesn't exist).
  1022. unset($FILTERLIB_PRIVATE->active[$contextid][$row->filter]);
  1023. }
  1024. }
  1025. }
  1026. }
  1027. // Process all config rows to add config data to these entries.
  1028. foreach ($filterconfigs as $row) {
  1029. if (isset($FILTERLIB_PRIVATE->active[$row->contextid][$row->filter])) {
  1030. $FILTERLIB_PRIVATE->active[$row->contextid][$row->filter][$row->name] = $row->value;
  1031. }
  1032. }
  1033. }
  1034. /**
  1035. * List all of the filters that are available in this context, and what the
  1036. * local and inherited states of that filter are.
  1037. *
  1038. * @param context $context a context that is not the system context.
  1039. * @return array an array with filter names, for example 'tex'
  1040. * as keys. and and the values are objects with fields:
  1041. * ->filter filter name, same as the key.
  1042. * ->localstate TEXTFILTER_ON/OFF/INHERIT
  1043. * ->inheritedstate TEXTFILTER_ON/OFF - the state that will be used if localstate is set to TEXTFILTER_INHERIT.
  1044. */
  1045. function filter_get_available_in_context($context) {
  1046. global $DB;
  1047. // The complex logic is working out the active state in the parent context,
  1048. // so strip the current context from the list.
  1049. $contextids = explode('/', trim($context->path, '/'));
  1050. array_pop($contextids);
  1051. $contextids = implode(',', $contextids);
  1052. if (empty($contextids)) {
  1053. throw new coding_exception('filter_get_available_in_context cannot be called with the system context.');
  1054. }
  1055. // The following SQL is tricky, in the same way at the SQL in filter_get_active_in_context.
  1056. $sql = "SELECT parent_states.filter,
  1057. CASE WHEN fa.active IS NULL THEN " . TEXTFILTER_INHERIT . "
  1058. ELSE fa.active END AS localstate,
  1059. parent_states.inheritedstate
  1060. FROM (SELECT f.filter, MAX(f.sortorder) AS sortorder,
  1061. CASE WHEN MAX(f.active * ctx.depth) > -MIN(f.active * ctx.depth) THEN " . TEXTFILTER_ON . "
  1062. ELSE " . TEXTFILTER_OFF . " END AS inheritedstate
  1063. FROM {filter_active} f
  1064. JOIN {context} ctx ON f.contextid = ctx.id
  1065. WHERE ctx.id IN ($contextids)
  1066. GROUP BY f.filter
  1067. HAVING MIN(f.active) > " . TEXTFILTER_DISABLED . "
  1068. ) parent_states
  1069. LEFT JOIN {filter_active} fa ON fa.filter = parent_states.filter AND fa.contextid = $context->id
  1070. ORDER BY parent_states.sortorder";
  1071. return $DB->get_records_sql($sql);
  1072. }
  1073. /**
  1074. * This function is for use by the filter administration page.
  1075. *
  1076. * @return array 'filtername' => object with fields 'filter' (=filtername), 'active' and 'sortorder'
  1077. */
  1078. function filter_get_global_states() {
  1079. global $DB;
  1080. $context = context_system::instance();
  1081. return $DB->get_records('filter_active', array('contextid' => $context->id), 'sortorder', 'filter,active,sortorder');
  1082. }
  1083. /**
  1084. * Delete all the data in the database relating to a filter, prior to deleting it.
  1085. *
  1086. * @param string $filter The filter name, for example 'tex'.
  1087. */
  1088. function filter_delete_all_for_filter($filter) {
  1089. global $DB;
  1090. unset_all_config_for_plugin('filter_' . $filter);
  1091. $DB->delete_records('filter_active', array('filter' => $filter));
  1092. $DB->delete_records('filter_config', array('filter' => $filter));
  1093. }
  1094. /**
  1095. * Delete all the data in the database relating to a context, used when contexts are deleted.
  1096. *
  1097. * @param integer $contextid The id of the context being deleted.
  1098. */
  1099. function filter_delete_all_for_context($contextid) {
  1100. global $DB;
  1101. $DB->delete_records('filter_active', array('contextid' => $contextid));
  1102. $DB->delete_records('filter_config', array('contextid' => $contextid));
  1103. }
  1104. /**
  1105. * Does this filter have a global settings page in the admin tree?
  1106. * (The settings page for a filter must be called, for example, filtersettingfiltertex.)
  1107. *
  1108. * @param string $filter The filter name, for example 'tex'.
  1109. * @return boolean Whether there should be a 'Settings' link on the config page.
  1110. */
  1111. function filter_has_global_settings($filter) {
  1112. global $CFG;
  1113. $settingspath = $CFG->dirroot . '/filter/' . $filter . '/settings.php';
  1114. if (is_readable($settingspath)) {
  1115. return true;
  1116. }
  1117. $settingspath = $CFG->dirroot . '/filter/' . $filter . '/filtersettings.php';
  1118. return is_readable($settingspath);
  1119. }
  1120. /**
  1121. * Does this filter have local (per-context) settings?
  1122. *
  1123. * @param string $filter The filter name, for example 'tex'.
  1124. * @return boolean Whether there should be a 'Settings' link on the manage filters in context page.
  1125. */
  1126. function filter_has_local_settings($filter) {
  1127. global $CFG;
  1128. $settingspath = $CFG->dirroot . '/filter/' . $filter . '/filterlocalsettings.php';
  1129. return is_readable($settingspath);
  1130. }
  1131. /**
  1132. * Certain types of context (block and user) may not have local filter settings.
  1133. * the function checks a context to see whether it may have local config.
  1134. *
  1135. * @param object $context a context.
  1136. * @return boolean whether this context may have local filter settings.
  1137. */
  1138. function filter_context_may_have_filter_settings($context) {
  1139. return $context->contextlevel != CONTEXT_BLOCK && $context->contextlevel != CONTEXT_USER;
  1140. }
  1141. /**
  1142. * Process phrases intelligently found within a HTML text (such as adding links).
  1143. *
  1144. * @param string $text the text that we are filtering
  1145. * @param filterobject[] $linkarray an array of filterobjects
  1146. * @param array $ignoretagsopen an array of opening tags that we should ignore while filtering
  1147. * @param array $ignoretagsclose an array of corresponding closing tags
  1148. * @param bool $overridedefaultignore True to only use tags provided by arguments
  1149. * @param bool $linkarrayalreadyprepared True to say that filter_prepare_phrases_for_filtering
  1150. * has already been called for $linkarray. Default false.
  1151. * @return string
  1152. */
  1153. function filter_phrases($text, $linkarray, $ignoretagsopen = null, $ignoretagsclose = null,
  1154. $overridedefaultignore = false, $linkarrayalreadyprepared = false) {
  1155. global $CFG;
  1156. // Used if $CFG->filtermatchoneperpage is on. Array with keys being the workregexp
  1157. // for things that have already been matched on this page.
  1158. static $usedphrases = [];
  1159. $ignoretags = array(); // To store all the enclosing tags to be completely ignored.
  1160. $tags = array(); // To store all the simple tags to be ignored.
  1161. if (!$linkarrayalreadyprepared) {
  1162. $linkarray = filter_prepare_phrases_for_filtering($linkarray);
  1163. }
  1164. if (!$overridedefaultignore) {
  1165. // A list of open/close tags that we should not replace within.
  1166. // Extended to include <script>, <textarea>, <select> and <a> tags.
  1167. // Regular expression allows tags with or without attributes.
  1168. $filterignoretagsopen = array('<head>', '<nolink>', '<span(\s[^>]*?)?class="nolink"(\s[^>]*?)?>',
  1169. '<script(\s[^>]*?)?>', '<textarea(\s[^>]*?)?>',
  1170. '<select(\s[^>]*?)?>', '<a(\s[^>]*?)?>');
  1171. $filterignoretagsclose = array('</head>', '</nolink>', '</span>',
  1172. '</script>', '</textarea>', '</select>', '</a>');
  1173. } else {
  1174. // Set an empty default list.
  1175. $filterignoretagsopen = array();
  1176. $filterignoretagsclose = array();
  1177. }
  1178. // Add the user defined ignore tags to the default list.
  1179. if ( is_array($ignoretagsopen) ) {
  1180. foreach ($ignoretagsopen as $open) {
  1181. $filterignoretagsopen[] = $open;
  1182. }
  1183. foreach ($ignoretagsclose as $close) {
  1184. $filterignoretagsclose[] = $close;
  1185. }
  1186. }
  1187. // Double up some magic chars to avoid "accidental matches".
  1188. $text = preg_replace('/([#*%])/', '\1\1', $text);
  1189. // Remove everything enclosed by the ignore tags from $text.
  1190. filter_save_ignore_tags($text, $filterignoretagsopen, $filterignoretagsclose, $ignoretags);
  1191. // Remove tags from $text.
  1192. filter_save_tags($text, $tags);
  1193. // Prepare the limit for preg_match calls.
  1194. if (!empty($CFG->filtermatchonepertext) || !empty($CFG->filtermatchoneperpage)) {
  1195. $pregreplacelimit = 1;
  1196. } else {
  1197. $pregreplacelimit = -1; // No limit.
  1198. }
  1199. // Time to cycle through each phrase to be linked.
  1200. foreach ($linkarray as $key => $linkobject) {
  1201. if ($linkobject->workregexp === null) {
  1202. // This is th…

Large files files are truncated, but you can click here to view the full file