PageRenderTime 61ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/filterlib.php

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