[ Index ]

PHP Cross Reference of moodle-2.8

title

Body

[close]

/lib/ -> filterlib.php (source)

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


Generated: Fri Nov 28 20:29:05 2014 Cross-referenced by PHPXref 0.7.1