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