Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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 of functions for web output
19
 *
20
 * Library of all general-purpose Moodle PHP functions and constants
21
 * that produce HTML output
22
 *
23
 * Other main libraries:
24
 * - datalib.php - functions that access the database.
25
 * - moodlelib.php - general-purpose Moodle functions.
26
 *
27
 * @package    core
28
 * @subpackage lib
29
 * @copyright  1999 onwards Martin Dougiamas {@link http://moodle.com}
30
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
31
 */
32
 
33
use Psr\Http\Message\UriInterface;
34
 
35
defined('MOODLE_INTERNAL') || die();
36
 
37
// Constants.
38
 
39
// Define text formatting types ... eventually we can add Wiki, BBcode etc.
40
 
41
/**
42
 * Does all sorts of transformations and filtering.
43
 */
44
define('FORMAT_MOODLE',   '0');
45
 
46
/**
47
 * Plain HTML (with some tags stripped).
48
 */
49
define('FORMAT_HTML',     '1');
50
 
51
/**
52
 * Plain text (even tags are printed in full).
53
 */
54
define('FORMAT_PLAIN',    '2');
55
 
56
/**
57
 * Wiki-formatted text.
58
 * Deprecated: left here just to note that '3' is not used (at the moment)
59
 * and to catch any latent wiki-like text (which generates an error)
60
 * @deprecated since 2005!
61
 */
62
define('FORMAT_WIKI',     '3');
63
 
64
/**
65
 * Markdown-formatted text http://daringfireball.net/projects/markdown/
66
 */
67
define('FORMAT_MARKDOWN', '4');
68
 
69
/**
70
 * A moodle_url comparison using this flag will return true if the base URLs match, params are ignored.
71
 */
72
define('URL_MATCH_BASE', 0);
73
 
74
/**
75
 * A moodle_url comparison using this flag will return true if the base URLs match and the params of url1 are part of url2.
76
 */
77
define('URL_MATCH_PARAMS', 1);
78
 
79
/**
80
 * A moodle_url comparison using this flag will return true if the two URLs are identical, except for the order of the params.
81
 */
82
define('URL_MATCH_EXACT', 2);
83
 
84
// Functions.
85
 
86
/**
87
 * Add quotes to HTML characters.
88
 *
89
 * Returns $var with HTML characters (like "<", ">", etc.) properly quoted.
90
 * Related function {@link p()} simply prints the output of this function.
91
 *
92
 * @param string $var the string potentially containing HTML characters
93
 * @return string
94
 */
95
function s($var) {
96
    if ($var === false) {
97
        return '0';
98
    }
99
 
100
    if ($var === null || $var === '') {
101
        return '';
102
    }
103
 
104
    return preg_replace(
105
        '/&amp;#(\d+|x[0-9a-f]+);/i', '&#$1;',
106
        htmlspecialchars($var, ENT_QUOTES | ENT_HTML401 | ENT_SUBSTITUTE)
107
    );
108
}
109
 
110
/**
111
 * Add quotes to HTML characters.
112
 *
113
 * Prints $var with HTML characters (like "<", ">", etc.) properly quoted.
114
 * This function simply calls & displays {@link s()}.
115
 * @see s()
116
 *
117
 * @param string $var the string potentially containing HTML characters
118
 */
119
function p($var) {
120
    echo s($var);
121
}
122
 
123
/**
124
 * Does proper javascript quoting.
125
 *
126
 * Do not use addslashes anymore, because it does not work when magic_quotes_sybase is enabled.
127
 *
128
 * @param mixed $var String, Array, or Object to add slashes to
129
 * @return mixed quoted result
130
 */
131
function addslashes_js($var) {
132
    if (is_string($var)) {
133
        $var = str_replace('\\', '\\\\', $var);
134
        $var = str_replace(array('\'', '"', "\n", "\r", "\0"), array('\\\'', '\\"', '\\n', '\\r', '\\0'), $var);
135
        $var = str_replace('</', '<\/', $var);   // XHTML compliance.
136
    } else if (is_array($var)) {
137
        $var = array_map('addslashes_js', $var);
138
    } else if (is_object($var)) {
139
        $a = get_object_vars($var);
140
        foreach ($a as $key => $value) {
141
            $a[$key] = addslashes_js($value);
142
        }
143
        $var = (object)$a;
144
    }
145
    return $var;
146
}
147
 
148
/**
149
 * Remove query string from url.
150
 *
151
 * Takes in a URL and returns it without the querystring portion.
152
 *
153
 * @param string $url the url which may have a query string attached.
154
 * @return string The remaining URL.
155
 */
156
function strip_querystring($url) {
157
    if ($url === null || $url === '') {
158
        return '';
159
    }
160
 
161
    if ($commapos = strpos($url, '?')) {
162
        return substr($url, 0, $commapos);
163
    } else {
164
        return $url;
165
    }
166
}
167
 
168
/**
169
 * Returns the name of the current script, WITH the querystring portion.
170
 *
171
 * This function is necessary because PHP_SELF and REQUEST_URI and SCRIPT_NAME
172
 * return different things depending on a lot of things like your OS, Web
173
 * server, and the way PHP is compiled (ie. as a CGI, module, ISAPI, etc.)
174
 * <b>NOTE:</b> This function returns false if the global variables needed are not set.
175
 *
176
 * @return mixed String or false if the global variables needed are not set.
177
 */
178
function me() {
179
    global $ME;
180
    return $ME;
181
}
182
 
183
/**
184
 * Guesses the full URL of the current script.
185
 *
186
 * This function is using $PAGE->url, but may fall back to $FULLME which
187
 * is constructed from  PHP_SELF and REQUEST_URI or SCRIPT_NAME
188
 *
189
 * @return mixed full page URL string or false if unknown
190
 */
191
function qualified_me() {
192
    global $FULLME, $PAGE, $CFG;
193
 
194
    if (isset($PAGE) and $PAGE->has_set_url()) {
195
        // This is the only recommended way to find out current page.
196
        return $PAGE->url->out(false);
197
 
198
    } else {
199
        if ($FULLME === null) {
200
            // CLI script most probably.
201
            return false;
202
        }
203
        if (!empty($CFG->sslproxy)) {
204
            // Return only https links when using SSL proxy.
205
            return preg_replace('/^http:/', 'https:', $FULLME, 1);
206
        } else {
207
            return $FULLME;
208
        }
209
    }
210
}
211
 
212
/**
213
 * Determines whether or not the Moodle site is being served over HTTPS.
214
 *
215
 * This is done simply by checking the value of $CFG->wwwroot, which seems
216
 * to be the only reliable method.
217
 *
218
 * @return boolean True if site is served over HTTPS, false otherwise.
219
 */
220
function is_https() {
221
    global $CFG;
222
 
223
    return (strpos($CFG->wwwroot, 'https://') === 0);
224
}
225
 
226
/**
227
 * Returns the cleaned local URL of the HTTP_REFERER less the URL query string parameters if required.
228
 *
229
 * @param bool $stripquery if true, also removes the query part of the url.
230
 * @return string The resulting referer or empty string.
231
 */
232
function get_local_referer($stripquery = true) {
233
    if (isset($_SERVER['HTTP_REFERER'])) {
234
        $referer = clean_param($_SERVER['HTTP_REFERER'], PARAM_LOCALURL);
235
        if ($stripquery) {
236
            return strip_querystring($referer);
237
        } else {
238
            return $referer;
239
        }
240
    } else {
241
        return '';
242
    }
243
}
244
 
245
/**
246
 * Class for creating and manipulating urls.
247
 *
248
 * It can be used in moodle pages where config.php has been included without any further includes.
249
 *
250
 * It is useful for manipulating urls with long lists of params.
251
 * One situation where it will be useful is a page which links to itself to perform various actions
252
 * and / or to process form data. A moodle_url object :
253
 * can be created for a page to refer to itself with all the proper get params being passed from page call to
254
 * page call and methods can be used to output a url including all the params, optionally adding and overriding
255
 * params and can also be used to
256
 *     - output the url without any get params
257
 *     - and output the params as hidden fields to be output within a form
258
 *
259
 * @copyright 2007 jamiesensei
260
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
261
 * @package core
262
 */
263
class moodle_url {
264
 
265
    /**
266
     * Scheme, ex.: http, https
267
     * @var string
268
     */
269
    protected $scheme = '';
270
 
271
    /**
272
     * Hostname.
273
     * @var string
274
     */
275
    protected $host = '';
276
 
277
    /**
278
     * Port number, empty means default 80 or 443 in case of http.
279
     * @var int
280
     */
281
    protected $port = '';
282
 
283
    /**
284
     * Username for http auth.
285
     * @var string
286
     */
287
    protected $user = '';
288
 
289
    /**
290
     * Password for http auth.
291
     * @var string
292
     */
293
    protected $pass = '';
294
 
295
    /**
296
     * Script path.
297
     * @var string
298
     */
299
    protected $path = '';
300
 
301
    /**
302
     * Optional slash argument value.
303
     * @var string
304
     */
305
    protected $slashargument = '';
306
 
307
    /**
308
     * Anchor, may be also empty, null means none.
309
     * @var string
310
     */
311
    protected $anchor = null;
312
 
313
    /**
314
     * Url parameters as associative array.
315
     * @var array
316
     */
317
    protected $params = array();
318
 
319
    /**
320
     * Create new instance of moodle_url.
321
     *
322
     * @param moodle_url|string $url - moodle_url means make a copy of another
323
     *      moodle_url and change parameters, string means full url or shortened
324
     *      form (ex.: '/course/view.php'). It is strongly encouraged to not include
325
     *      query string because it may result in double encoded values. Use the
326
     *      $params instead. For admin URLs, just use /admin/script.php, this
327
     *      class takes care of the $CFG->admin issue.
328
     * @param array $params these params override current params or add new
329
     * @param string $anchor The anchor to use as part of the URL if there is one.
330
     * @throws moodle_exception
331
     */
332
    public function __construct($url, array $params = null, $anchor = null) {
333
        global $CFG;
334
 
335
        if ($url instanceof moodle_url) {
336
            $this->scheme = $url->scheme;
337
            $this->host = $url->host;
338
            $this->port = $url->port;
339
            $this->user = $url->user;
340
            $this->pass = $url->pass;
341
            $this->path = $url->path;
342
            $this->slashargument = $url->slashargument;
343
            $this->params = $url->params;
344
            $this->anchor = $url->anchor;
345
 
346
        } else {
347
            $url = $url ?? '';
348
            // Detect if anchor used.
349
            $apos = strpos($url, '#');
350
            if ($apos !== false) {
351
                $anchor = substr($url, $apos);
352
                $anchor = ltrim($anchor, '#');
353
                $this->set_anchor($anchor);
354
                $url = substr($url, 0, $apos);
355
            }
356
 
357
            // Normalise shortened form of our url ex.: '/course/view.php'.
358
            if (strpos($url, '/') === 0) {
359
                $url = $CFG->wwwroot.$url;
360
            }
361
 
362
            if ($CFG->admin !== 'admin') {
363
                if (strpos($url, "$CFG->wwwroot/admin/") === 0) {
364
                    $url = str_replace("$CFG->wwwroot/admin/", "$CFG->wwwroot/$CFG->admin/", $url);
365
                }
366
            }
367
 
368
            // Parse the $url.
369
            $parts = parse_url($url);
370
            if ($parts === false) {
371
                throw new moodle_exception('invalidurl');
372
            }
373
            if (isset($parts['query'])) {
374
                // Note: the values may not be correctly decoded, url parameters should be always passed as array.
375
                parse_str(str_replace('&amp;', '&', $parts['query']), $this->params);
376
            }
377
            unset($parts['query']);
378
            foreach ($parts as $key => $value) {
379
                $this->$key = $value;
380
            }
381
 
382
            // Detect slashargument value from path - we do not support directory names ending with .php.
383
            $pos = strpos($this->path, '.php/');
384
            if ($pos !== false) {
385
                $this->slashargument = substr($this->path, $pos + 4);
386
                $this->path = substr($this->path, 0, $pos + 4);
387
            }
388
        }
389
 
390
        $this->params($params);
391
        if ($anchor !== null) {
392
            $this->anchor = (string)$anchor;
393
        }
394
    }
395
 
396
    /**
397
     * Add an array of params to the params for this url.
398
     *
399
     * The added params override existing ones if they have the same name.
400
     *
401
     * @param array $params Defaults to null. If null then returns all params.
402
     * @return array Array of Params for url.
403
     * @throws coding_exception
404
     */
405
    public function params(array $params = null) {
406
        $params = (array)$params;
407
 
408
        foreach ($params as $key => $value) {
409
            if (is_int($key)) {
410
                throw new coding_exception('Url parameters can not have numeric keys!');
411
            }
412
            if (!is_string($value)) {
413
                if (is_array($value)) {
414
                    throw new coding_exception('Url parameters values can not be arrays!');
415
                }
416
                if (is_object($value) and !method_exists($value, '__toString')) {
417
                    throw new coding_exception('Url parameters values can not be objects, unless __toString() is defined!');
418
                }
419
            }
420
            $this->params[$key] = (string)$value;
421
        }
422
        return $this->params;
423
    }
424
 
425
    /**
426
     * Remove all params if no arguments passed.
427
     * Remove selected params if arguments are passed.
428
     *
429
     * Can be called as either remove_params('param1', 'param2')
430
     * or remove_params(array('param1', 'param2')).
431
     *
432
     * @param string[]|string $params,... either an array of param names, or 1..n string params to remove as args.
433
     * @return array url parameters
434
     */
435
    public function remove_params($params = null) {
436
        if (!is_array($params)) {
437
            $params = func_get_args();
438
        }
439
        foreach ($params as $param) {
440
            unset($this->params[$param]);
441
        }
442
        return $this->params;
443
    }
444
 
445
    /**
446
     * Remove all url parameters.
447
     *
448
     * @todo remove the unused param.
449
     * @param array $params Unused param
450
     * @return void
451
     */
452
    public function remove_all_params($params = null) {
453
        $this->params = array();
454
        $this->slashargument = '';
455
    }
456
 
457
    /**
458
     * Add a param to the params for this url.
459
     *
460
     * The added param overrides existing one if they have the same name.
461
     *
462
     * @param string $paramname name
463
     * @param string $newvalue Param value. If new value specified current value is overriden or parameter is added
464
     * @return mixed string parameter value, null if parameter does not exist
465
     */
466
    public function param($paramname, $newvalue = '') {
467
        if (func_num_args() > 1) {
468
            // Set new value.
469
            $this->params(array($paramname => $newvalue));
470
        }
471
        if (isset($this->params[$paramname])) {
472
            return $this->params[$paramname];
473
        } else {
474
            return null;
475
        }
476
    }
477
 
478
    /**
479
     * Merges parameters and validates them
480
     *
481
     * @param array $overrideparams
482
     * @return array merged parameters
483
     * @throws coding_exception
484
     */
485
    protected function merge_overrideparams(array $overrideparams = null) {
486
        $overrideparams = (array)$overrideparams;
487
        $params = $this->params;
488
        foreach ($overrideparams as $key => $value) {
489
            if (is_int($key)) {
490
                throw new coding_exception('Overridden parameters can not have numeric keys!');
491
            }
492
            if (is_array($value)) {
493
                throw new coding_exception('Overridden parameters values can not be arrays!');
494
            }
495
            if (is_object($value) and !method_exists($value, '__toString')) {
496
                throw new coding_exception('Overridden parameters values can not be objects, unless __toString() is defined!');
497
            }
498
            $params[$key] = (string)$value;
499
        }
500
        return $params;
501
    }
502
 
503
    /**
504
     * Get the params as as a query string.
505
     *
506
     * This method should not be used outside of this method.
507
     *
508
     * @param bool $escaped Use &amp; as params separator instead of plain &
509
     * @param array $overrideparams params to add to the output params, these
510
     *      override existing ones with the same name.
511
     * @return string query string that can be added to a url.
512
     */
513
    public function get_query_string($escaped = true, array $overrideparams = null) {
514
        $arr = array();
515
        if ($overrideparams !== null) {
516
            $params = $this->merge_overrideparams($overrideparams);
517
        } else {
518
            $params = $this->params;
519
        }
520
        foreach ($params as $key => $val) {
521
            if (is_array($val)) {
522
                foreach ($val as $index => $value) {
523
                    $arr[] = rawurlencode($key.'['.$index.']')."=".rawurlencode($value);
524
                }
525
            } else {
526
                if (isset($val) && $val !== '') {
527
                    $arr[] = rawurlencode($key)."=".rawurlencode($val);
528
                } else {
529
                    $arr[] = rawurlencode($key);
530
                }
531
            }
532
        }
533
        if ($escaped) {
534
            return implode('&amp;', $arr);
535
        } else {
536
            return implode('&', $arr);
537
        }
538
    }
539
 
540
    /**
541
     * Get the url params as an array of key => value pairs.
542
     *
543
     * This helps in handling cases where url params contain arrays.
544
     *
545
     * @return array params array for templates.
546
     */
547
    public function export_params_for_template(): array {
548
        $data = [];
549
        foreach ($this->params as $key => $val) {
550
            if (is_array($val)) {
551
                foreach ($val as $index => $value) {
552
                    $data[] = ['name' => $key.'['.$index.']', 'value' => $value];
553
                }
554
            } else {
555
                $data[] = ['name' => $key, 'value' => $val];
556
            }
557
        }
558
        return $data;
559
    }
560
 
561
    /**
562
     * Shortcut for printing of encoded URL.
563
     *
564
     * @return string
565
     */
566
    public function __toString() {
567
        return $this->out(true);
568
    }
569
 
570
    /**
571
     * Output url.
572
     *
573
     * If you use the returned URL in HTML code, you want the escaped ampersands. If you use
574
     * the returned URL in HTTP headers, you want $escaped=false.
575
     *
576
     * @param bool $escaped Use &amp; as params separator instead of plain &
577
     * @param array $overrideparams params to add to the output url, these override existing ones with the same name.
578
     * @return string Resulting URL
579
     */
580
    public function out($escaped = true, array $overrideparams = null) {
581
 
582
        global $CFG;
583
 
584
        if (!is_bool($escaped)) {
585
            debugging('Escape parameter must be of type boolean, '.gettype($escaped).' given instead.');
586
        }
587
 
588
        $url = $this;
589
 
590
        // Allow url's to be rewritten by a plugin.
591
        if (isset($CFG->urlrewriteclass) && !isset($CFG->upgraderunning)) {
592
            $class = $CFG->urlrewriteclass;
593
            $pluginurl = $class::url_rewrite($url);
594
            if ($pluginurl instanceof moodle_url) {
595
                $url = $pluginurl;
596
            }
597
        }
598
 
599
        return $url->raw_out($escaped, $overrideparams);
600
 
601
    }
602
 
603
    /**
604
     * Output url without any rewrites
605
     *
606
     * This is identical in signature and use to out() but doesn't call the rewrite handler.
607
     *
608
     * @param bool $escaped Use &amp; as params separator instead of plain &
609
     * @param array $overrideparams params to add to the output url, these override existing ones with the same name.
610
     * @return string Resulting URL
611
     */
612
    public function raw_out($escaped = true, array $overrideparams = null) {
613
        if (!is_bool($escaped)) {
614
            debugging('Escape parameter must be of type boolean, '.gettype($escaped).' given instead.');
615
        }
616
 
617
        $uri = $this->out_omit_querystring().$this->slashargument;
618
 
619
        $querystring = $this->get_query_string($escaped, $overrideparams);
620
        if ($querystring !== '') {
621
            $uri .= '?' . $querystring;
622
        }
623
 
624
        $uri .= $this->get_encoded_anchor();
625
 
626
        return $uri;
627
    }
628
 
629
    /**
630
     * Encode the anchor according to RFC 3986.
631
     *
632
     * @return string The encoded anchor
633
     */
634
    public function get_encoded_anchor(): string {
635
        if (is_null($this->anchor)) {
636
            return '';
637
        }
638
 
639
        // RFC 3986 allows the following characters in a fragment without them being encoded:
640
        // pct-encoded: "%" HEXDIG HEXDIG
641
        // unreserved:  ALPHA / DIGIT / "-" / "." / "_" / "~" /
642
        // sub-delims:  "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" / ":" / "@"
643
        // fragment:    "/" / "?"
644
        //
645
        // All other characters should be encoded.
646
        // These should not be encoded in the fragment unless they were already encoded.
647
 
648
        // The following characters are allowed in the fragment without encoding.
649
        // In addition to this list is pct-encoded, but we can't easily handle this with a regular expression.
650
        $allowed = 'a-zA-Z0-9\\-._~!$&\'()*+,;=:@\/?';
651
        $anchor = '#';
652
 
653
        $remainder = $this->anchor;
654
        do {
655
            // Split the string on any %.
656
            $parts = explode('%', $remainder, 2);
657
            $anchorparts = array_shift($parts);
658
 
659
            // The first part can go through our preg_replace_callback to quote any relevant characters.
660
            $anchor .= preg_replace_callback(
661
                '/[^' . $allowed . ']/',
662
                fn ($matches) => rawurlencode($matches[0]),
663
                $anchorparts,
664
            );
665
 
666
            // The second part _might_ be a valid pct-encoded character.
667
            if (count($parts) === 0) {
668
                break;
669
            }
670
 
671
            // If the second part is a valid pct-encoded character, append it to the anchor.
672
            $remainder = array_shift($parts);
673
            if (preg_match('/^[a-fA-F0-9]{2}/', $remainder, $matches)) {
674
                $anchor .= "%{$matches[0]}";
675
                $remainder = substr($remainder, 2);
676
            } else {
677
                // This was not a valid pct-encoded character. Encode the % and continue with the next part.
678
                $anchor .= rawurlencode('%');
679
            }
680
        } while (strlen($remainder) > 0);
681
 
682
        return $anchor;
683
    }
684
 
685
    /**
686
     * Returns url without parameters, everything before '?'.
687
     *
688
     * @param bool $includeanchor if {@link self::anchor} is defined, should it be returned?
689
     * @return string
690
     */
691
    public function out_omit_querystring($includeanchor = false) {
692
 
693
        $uri = $this->scheme ? $this->scheme.':'.((strtolower($this->scheme) == 'mailto') ? '':'//'): '';
694
        $uri .= $this->user ? $this->user.($this->pass? ':'.$this->pass:'').'@':'';
695
        $uri .= $this->host ? $this->host : '';
696
        $uri .= $this->port ? ':'.$this->port : '';
697
        $uri .= $this->path ? $this->path : '';
698
        if ($includeanchor) {
699
            $uri .= $this->get_encoded_anchor();
700
        }
701
 
702
        return $uri;
703
    }
704
 
705
    /**
706
     * Compares this moodle_url with another.
707
     *
708
     * See documentation of constants for an explanation of the comparison flags.
709
     *
710
     * @param moodle_url $url The moodle_url object to compare
711
     * @param int $matchtype The type of comparison (URL_MATCH_BASE, URL_MATCH_PARAMS, URL_MATCH_EXACT)
712
     * @return bool
713
     */
714
    public function compare(moodle_url $url, $matchtype = URL_MATCH_EXACT) {
715
 
716
        $baseself = $this->out_omit_querystring();
717
        $baseother = $url->out_omit_querystring();
718
 
719
        // Append index.php if there is no specific file.
720
        if (substr($baseself, -1) == '/') {
721
            $baseself .= 'index.php';
722
        }
723
        if (substr($baseother, -1) == '/') {
724
            $baseother .= 'index.php';
725
        }
726
 
727
        // Compare the two base URLs.
728
        if ($baseself != $baseother) {
729
            return false;
730
        }
731
 
732
        if ($matchtype == URL_MATCH_BASE) {
733
            return true;
734
        }
735
 
736
        $urlparams = $url->params();
737
        foreach ($this->params() as $param => $value) {
738
            if ($param == 'sesskey') {
739
                continue;
740
            }
741
            if (!array_key_exists($param, $urlparams) || $urlparams[$param] != $value) {
742
                return false;
743
            }
744
        }
745
 
746
        if ($matchtype == URL_MATCH_PARAMS) {
747
            return true;
748
        }
749
 
750
        foreach ($urlparams as $param => $value) {
751
            if ($param == 'sesskey') {
752
                continue;
753
            }
754
            if (!array_key_exists($param, $this->params()) || $this->param($param) != $value) {
755
                return false;
756
            }
757
        }
758
 
759
        if ($url->anchor !== $this->anchor) {
760
            return false;
761
        }
762
 
763
        return true;
764
    }
765
 
766
    /**
767
     * Sets the anchor for the URI (the bit after the hash)
768
     *
769
     * @param string $anchor null means remove previous
770
     */
771
    public function set_anchor($anchor) {
772
        if (is_null($anchor)) {
773
            // Remove.
774
            $this->anchor = null;
775
        } else {
776
            $this->anchor = $anchor;
777
        }
778
    }
779
 
780
    /**
781
     * Sets the scheme for the URI (the bit before ://)
782
     *
783
     * @param string $scheme
784
     */
785
    public function set_scheme($scheme) {
786
        // See http://www.ietf.org/rfc/rfc3986.txt part 3.1.
787
        if (preg_match('/^[a-zA-Z][a-zA-Z0-9+.-]*$/', $scheme)) {
788
            $this->scheme = $scheme;
789
        } else {
790
            throw new coding_exception('Bad URL scheme.');
791
        }
792
    }
793
 
794
    /**
795
     * Sets the url slashargument value.
796
     *
797
     * @param string $path usually file path
798
     * @param string $parameter name of page parameter if slasharguments not supported
799
     * @param bool $supported usually null, then it depends on $CFG->slasharguments, use true or false for other servers
800
     * @return void
801
     */
802
    public function set_slashargument($path, $parameter = 'file', $supported = null) {
803
        global $CFG;
804
        if (is_null($supported)) {
805
            $supported = !empty($CFG->slasharguments);
806
        }
807
 
808
        if ($supported) {
809
            $parts = explode('/', $path);
810
            $parts = array_map('rawurlencode', $parts);
811
            $path  = implode('/', $parts);
812
            $this->slashargument = $path;
813
            unset($this->params[$parameter]);
814
 
815
        } else {
816
            $this->slashargument = '';
817
            $this->params[$parameter] = $path;
818
        }
819
    }
820
 
821
    // Static factory methods.
822
 
823
    /**
824
     * Create a new moodle_url instance from a UriInterface.
825
     *
826
     * @param UriInterface $uri
827
     * @return self
828
     */
829
    public static function from_uri(UriInterface $uri): self {
830
        $url = new self(
831
            url: $uri->getScheme() . '://' . $uri->getAuthority() . $uri->getPath(),
832
            anchor: $uri->getFragment() ?: null,
833
        );
834
 
835
        $params = $uri->getQuery();
836
        foreach (explode('&', $params) as $param) {
837
            $url->param(...explode('=', $param, 2));
838
        }
839
 
840
        return $url;
841
    }
842
 
843
    /**
844
     * General moodle file url.
845
     *
846
     * @param string $urlbase the script serving the file
847
     * @param string $path
848
     * @param bool $forcedownload
849
     * @return moodle_url
850
     */
851
    public static function make_file_url($urlbase, $path, $forcedownload = false) {
852
        $params = array();
853
        if ($forcedownload) {
854
            $params['forcedownload'] = 1;
855
        }
856
        $url = new moodle_url($urlbase, $params);
857
        $url->set_slashargument($path);
858
        return $url;
859
    }
860
 
861
    /**
862
     * Factory method for creation of url pointing to plugin file.
863
     *
864
     * Please note this method can be used only from the plugins to
865
     * create urls of own files, it must not be used outside of plugins!
866
     *
867
     * @param int $contextid
868
     * @param string $component
869
     * @param string $area
870
     * @param ?int $itemid
871
     * @param string $pathname
872
     * @param string $filename
873
     * @param bool $forcedownload
874
     * @param mixed $includetoken Whether to use a user token when displaying this group image.
875
     *                True indicates to generate a token for current user, and integer value indicates to generate a token for the
876
     *                user whose id is the value indicated.
877
     *                If the group picture is included in an e-mail or some other location where the audience is a specific
878
     *                user who will not be logged in when viewing, then we use a token to authenticate the user.
879
     * @return moodle_url
880
     */
881
    public static function make_pluginfile_url($contextid, $component, $area, $itemid, $pathname, $filename,
882
                                               $forcedownload = false, $includetoken = false) {
883
        global $CFG, $USER;
884
 
885
        $path = [];
886
 
887
        if ($includetoken) {
888
            $urlbase = "$CFG->wwwroot/tokenpluginfile.php";
889
            $userid = $includetoken === true ? $USER->id : $includetoken;
890
            $token = get_user_key('core_files', $userid);
891
            if ($CFG->slasharguments) {
892
                $path[] = $token;
893
            }
894
        } else {
895
            $urlbase = "$CFG->wwwroot/pluginfile.php";
896
        }
897
        $path[] = $contextid;
898
        $path[] = $component;
899
        $path[] = $area;
900
 
901
        if ($itemid !== null) {
902
            $path[] = $itemid;
903
        }
904
 
905
        $path = "/" . implode('/', $path) . "{$pathname}{$filename}";
906
 
907
        $url = self::make_file_url($urlbase, $path, $forcedownload, $includetoken);
908
        if ($includetoken && empty($CFG->slasharguments)) {
909
            $url->param('token', $token);
910
        }
911
        return $url;
912
    }
913
 
914
    /**
915
     * Factory method for creation of url pointing to plugin file.
916
     * This method is the same that make_pluginfile_url but pointing to the webservice pluginfile.php script.
917
     * It should be used only in external functions.
918
     *
919
     * @since  2.8
920
     * @param int $contextid
921
     * @param string $component
922
     * @param string $area
923
     * @param int $itemid
924
     * @param string $pathname
925
     * @param string $filename
926
     * @param bool $forcedownload
927
     * @return moodle_url
928
     */
929
    public static function make_webservice_pluginfile_url($contextid, $component, $area, $itemid, $pathname, $filename,
930
                                               $forcedownload = false) {
931
        global $CFG;
932
        $urlbase = "$CFG->wwwroot/webservice/pluginfile.php";
933
        if ($itemid === null) {
934
            return self::make_file_url($urlbase, "/$contextid/$component/$area".$pathname.$filename, $forcedownload);
935
        } else {
936
            return self::make_file_url($urlbase, "/$contextid/$component/$area/$itemid".$pathname.$filename, $forcedownload);
937
        }
938
    }
939
 
940
    /**
941
     * Factory method for creation of url pointing to draft file of current user.
942
     *
943
     * @param int $draftid draft item id
944
     * @param string $pathname
945
     * @param string $filename
946
     * @param bool $forcedownload
947
     * @return moodle_url
948
     */
949
    public static function make_draftfile_url($draftid, $pathname, $filename, $forcedownload = false) {
950
        global $CFG, $USER;
951
        $urlbase = "$CFG->wwwroot/draftfile.php";
952
        $context = context_user::instance($USER->id);
953
 
954
        return self::make_file_url($urlbase, "/$context->id/user/draft/$draftid".$pathname.$filename, $forcedownload);
955
    }
956
 
957
    /**
958
     * Factory method for creating of links to legacy course files.
959
     *
960
     * @param int $courseid
961
     * @param string $filepath
962
     * @param bool $forcedownload
963
     * @return moodle_url
964
     */
965
    public static function make_legacyfile_url($courseid, $filepath, $forcedownload = false) {
966
        global $CFG;
967
 
968
        $urlbase = "$CFG->wwwroot/file.php";
969
        return self::make_file_url($urlbase, '/'.$courseid.'/'.$filepath, $forcedownload);
970
    }
971
 
972
    /**
973
     * Checks if URL is relative to $CFG->wwwroot.
974
     *
975
     * @return bool True if URL is relative to $CFG->wwwroot; otherwise, false.
976
     */
977
    public function is_local_url(): bool {
978
        global $CFG;
979
 
980
        $url = $this->out();
981
        // Does URL start with wwwroot? Otherwise, URL isn't relative to wwwroot.
982
        return ( ($url === $CFG->wwwroot) || (strpos($url, $CFG->wwwroot.'/') === 0) );
983
    }
984
 
985
    /**
986
     * Returns URL as relative path from $CFG->wwwroot
987
     *
988
     * Can be used for passing around urls with the wwwroot stripped
989
     *
990
     * @param boolean $escaped Use &amp; as params separator instead of plain &
991
     * @param array $overrideparams params to add to the output url, these override existing ones with the same name.
992
     * @return string Resulting URL
993
     * @throws coding_exception if called on a non-local url
994
     */
995
    public function out_as_local_url($escaped = true, array $overrideparams = null) {
996
        global $CFG;
997
 
998
        // URL should be relative to wwwroot. If not then throw exception.
999
        if ($this->is_local_url()) {
1000
            $url = $this->out($escaped, $overrideparams);
1001
            $localurl = substr($url, strlen($CFG->wwwroot));
1002
            return !empty($localurl) ? $localurl : '';
1003
        } else {
1004
            throw new coding_exception('out_as_local_url called on a non-local URL');
1005
        }
1006
    }
1007
 
1008
    /**
1009
     * Returns the 'path' portion of a URL. For example, if the URL is
1010
     * http://www.example.org:447/my/file/is/here.txt?really=1 then this will
1011
     * return '/my/file/is/here.txt'.
1012
     *
1013
     * By default the path includes slash-arguments (for example,
1014
     * '/myfile.php/extra/arguments') so it is what you would expect from a
1015
     * URL path. If you don't want this behaviour, you can opt to exclude the
1016
     * slash arguments. (Be careful: if the $CFG variable slasharguments is
1017
     * disabled, these URLs will have a different format and you may need to
1018
     * look at the 'file' parameter too.)
1019
     *
1020
     * @param bool $includeslashargument If true, includes slash arguments
1021
     * @return string Path of URL
1022
     */
1023
    public function get_path($includeslashargument = true) {
1024
        return $this->path . ($includeslashargument ? $this->slashargument : '');
1025
    }
1026
 
1027
    /**
1028
     * Returns a given parameter value from the URL.
1029
     *
1030
     * @param string $name Name of parameter
1031
     * @return string Value of parameter or null if not set
1032
     */
1033
    public function get_param($name) {
1034
        if (array_key_exists($name, $this->params)) {
1035
            return $this->params[$name];
1036
        } else {
1037
            return null;
1038
        }
1039
    }
1040
 
1041
    /**
1042
     * Returns the 'scheme' portion of a URL. For example, if the URL is
1043
     * http://www.example.org:447/my/file/is/here.txt?really=1 then this will
1044
     * return 'http' (without the colon).
1045
     *
1046
     * @return string Scheme of the URL.
1047
     */
1048
    public function get_scheme() {
1049
        return $this->scheme;
1050
    }
1051
 
1052
    /**
1053
     * Returns the 'host' portion of a URL. For example, if the URL is
1054
     * http://www.example.org:447/my/file/is/here.txt?really=1 then this will
1055
     * return 'www.example.org'.
1056
     *
1057
     * @return string Host of the URL.
1058
     */
1059
    public function get_host() {
1060
        return $this->host;
1061
    }
1062
 
1063
    /**
1064
     * Returns the 'port' portion of a URL. For example, if the URL is
1065
     * http://www.example.org:447/my/file/is/here.txt?really=1 then this will
1066
     * return '447'.
1067
     *
1068
     * @return string Port of the URL.
1069
     */
1070
    public function get_port() {
1071
        return $this->port;
1072
    }
1073
}
1074
 
1075
/**
1076
 * Determine if there is data waiting to be processed from a form
1077
 *
1078
 * Used on most forms in Moodle to check for data
1079
 * Returns the data as an object, if it's found.
1080
 * This object can be used in foreach loops without
1081
 * casting because it's cast to (array) automatically
1082
 *
1083
 * Checks that submitted POST data exists and returns it as object.
1084
 *
1085
 * @return mixed false or object
1086
 */
1087
function data_submitted() {
1088
 
1089
    if (empty($_POST)) {
1090
        return false;
1091
    } else {
1092
        return (object)fix_utf8($_POST);
1093
    }
1094
}
1095
 
1096
/**
1097
 * Given some normal text this function will break up any
1098
 * long words to a given size by inserting the given character
1099
 *
1100
 * It's multibyte savvy and doesn't change anything inside html tags.
1101
 *
1102
 * @param string $string the string to be modified
1103
 * @param int $maxsize maximum length of the string to be returned
1104
 * @param string $cutchar the string used to represent word breaks
1105
 * @return string
1106
 */
1107
function break_up_long_words($string, $maxsize=20, $cutchar=' ') {
1108
 
1109
    // First of all, save all the tags inside the text to skip them.
1110
    $tags = array();
1111
    filter_save_tags($string, $tags);
1112
 
1113
    // Process the string adding the cut when necessary.
1114
    $output = '';
1115
    $length = core_text::strlen($string);
1116
    $wordlength = 0;
1117
 
1118
    for ($i=0; $i<$length; $i++) {
1119
        $char = core_text::substr($string, $i, 1);
1120
        if ($char == ' ' or $char == "\t" or $char == "\n" or $char == "\r" or $char == "<" or $char == ">") {
1121
            $wordlength = 0;
1122
        } else {
1123
            $wordlength++;
1124
            if ($wordlength > $maxsize) {
1125
                $output .= $cutchar;
1126
                $wordlength = 0;
1127
            }
1128
        }
1129
        $output .= $char;
1130
    }
1131
 
1132
    // Finally load the tags back again.
1133
    if (!empty($tags)) {
1134
        $output = str_replace(array_keys($tags), $tags, $output);
1135
    }
1136
 
1137
    return $output;
1138
}
1139
 
1140
/**
1141
 * Try and close the current window using JavaScript, either immediately, or after a delay.
1142
 *
1143
 * Echo's out the resulting XHTML & javascript
1144
 *
1145
 * @param integer $delay a delay in seconds before closing the window. Default 0.
1146
 * @param boolean $reloadopener if true, we will see if this window was a pop-up, and try
1147
 *      to reload the parent window before this one closes.
1148
 */
1149
function close_window($delay = 0, $reloadopener = false) {
1150
    global $PAGE, $OUTPUT;
1151
 
1152
    if (!$PAGE->headerprinted) {
1153
        $PAGE->set_title(get_string('closewindow'));
1154
        echo $OUTPUT->header();
1155
    } else {
1156
        $OUTPUT->container_end_all(false);
1157
    }
1158
 
1159
    if ($reloadopener) {
1160
        // Trigger the reload immediately, even if the reload is after a delay.
1161
        $PAGE->requires->js_function_call('window.opener.location.reload', array(true));
1162
    }
1163
    $OUTPUT->notification(get_string('windowclosing'), 'notifysuccess');
1164
 
1165
    $PAGE->requires->js_function_call('close_window', array(new stdClass()), false, $delay);
1166
 
1167
    echo $OUTPUT->footer();
1168
    exit;
1169
}
1170
 
1171
/**
1172
 * Returns a string containing a link to the user documentation for the current page.
1173
 *
1174
 * Also contains an icon by default. Shown to teachers and admin only.
1175
 *
1176
 * @param string $text The text to be displayed for the link
1177
 * @return string The link to user documentation for this current page
1178
 */
1179
function page_doc_link($text='') {
1180
    global $OUTPUT, $PAGE;
1181
    $path = page_get_doc_link_path($PAGE);
1182
    if (!$path) {
1183
        return '';
1184
    }
1185
    return $OUTPUT->doc_link($path, $text);
1186
}
1187
 
1188
/**
1189
 * Returns the path to use when constructing a link to the docs.
1190
 *
1191
 * @since Moodle 2.5.1 2.6
1192
 * @param moodle_page $page
1193
 * @return string
1194
 */
1195
function page_get_doc_link_path(moodle_page $page) {
1196
    global $CFG;
1197
 
1198
    if (empty($CFG->docroot) || during_initial_install()) {
1199
        return '';
1200
    }
1201
    if (!has_capability('moodle/site:doclinks', $page->context)) {
1202
        return '';
1203
    }
1204
 
1205
    $path = $page->docspath;
1206
    if (!$path) {
1207
        return '';
1208
    }
1209
    return $path;
1210
}
1211
 
1212
 
1213
/**
1214
 * Validates an email to make sure it makes sense.
1215
 *
1216
 * @param string $address The email address to validate.
1217
 * @return boolean
1218
 */
1219
function validate_email($address) {
1220
    global $CFG;
1221
 
1222
    if ($address === null || $address === false || $address === '') {
1223
        return false;
1224
    }
1225
 
1226
    require_once("{$CFG->libdir}/phpmailer/moodle_phpmailer.php");
1227
 
1228
    return moodle_phpmailer::validateAddress($address ?? '') && !preg_match('/[<>]/', $address);
1229
}
1230
 
1231
/**
1232
 * Extracts file argument either from file parameter or PATH_INFO
1233
 *
1234
 * Note: $scriptname parameter is not needed anymore
1235
 *
1236
 * @return string file path (only safe characters)
1237
 */
1238
function get_file_argument() {
1239
    global $SCRIPT;
1240
 
1241
    $relativepath = false;
1242
    $hasforcedslashargs = false;
1243
 
1244
    if (isset($_SERVER['REQUEST_URI']) && !empty($_SERVER['REQUEST_URI'])) {
1245
        // Checks whether $_SERVER['REQUEST_URI'] contains '/pluginfile.php/'
1246
        // instead of '/pluginfile.php?', when serving a file from e.g. mod_imscp or mod_scorm.
1247
        if ((strpos($_SERVER['REQUEST_URI'], '/pluginfile.php/') !== false)
1248
                && isset($_SERVER['PATH_INFO']) && !empty($_SERVER['PATH_INFO'])) {
1249
            // Exclude edge cases like '/pluginfile.php/?file='.
1250
            $args = explode('/', ltrim($_SERVER['PATH_INFO'], '/'));
1251
            $hasforcedslashargs = (count($args) > 2); // Always at least: context, component and filearea.
1252
        }
1253
    }
1254
    if (!$hasforcedslashargs) {
1255
        $relativepath = optional_param('file', false, PARAM_PATH);
1256
    }
1257
 
1258
    if ($relativepath !== false and $relativepath !== '') {
1259
        return $relativepath;
1260
    }
1261
    $relativepath = false;
1262
 
1263
    // Then try extract file from the slasharguments.
1264
    if (stripos($_SERVER['SERVER_SOFTWARE'], 'iis') !== false) {
1265
        // NOTE: IIS tends to convert all file paths to single byte DOS encoding,
1266
        //       we can not use other methods because they break unicode chars,
1267
        //       the only ways are to use URL rewriting
1268
        //       OR
1269
        //       to properly set the 'FastCGIUtf8ServerVariables' registry key.
1270
        if (isset($_SERVER['PATH_INFO']) and $_SERVER['PATH_INFO'] !== '') {
1271
            // Check that PATH_INFO works == must not contain the script name.
1272
            if (strpos($_SERVER['PATH_INFO'], $SCRIPT) === false) {
1273
                $relativepath = clean_param(urldecode($_SERVER['PATH_INFO']), PARAM_PATH);
1274
            }
1275
        }
1276
    } else {
1277
        // All other apache-like servers depend on PATH_INFO.
1278
        if (isset($_SERVER['PATH_INFO'])) {
1279
            if (isset($_SERVER['SCRIPT_NAME']) and strpos($_SERVER['PATH_INFO'], $_SERVER['SCRIPT_NAME']) === 0) {
1280
                $relativepath = substr($_SERVER['PATH_INFO'], strlen($_SERVER['SCRIPT_NAME']));
1281
            } else {
1282
                $relativepath = $_SERVER['PATH_INFO'];
1283
            }
1284
            $relativepath = clean_param($relativepath, PARAM_PATH);
1285
        }
1286
    }
1287
 
1288
    return $relativepath;
1289
}
1290
 
1291
/**
1292
 * Just returns an array of text formats suitable for a popup menu
1293
 *
1294
 * @return array
1295
 */
1296
function format_text_menu() {
1297
    return array (FORMAT_MOODLE => get_string('formattext'),
1298
                  FORMAT_HTML => get_string('formathtml'),
1299
                  FORMAT_PLAIN => get_string('formatplain'),
1300
                  FORMAT_MARKDOWN => get_string('formatmarkdown'));
1301
}
1302
 
1303
/**
1304
 * Given text in a variety of format codings, this function returns the text as safe HTML.
1305
 *
1306
 * This function should mainly be used for long strings like posts,
1307
 * answers, glossary items etc. For short strings {@link format_string()}.
1308
 *
1309
 * <pre>
1310
 * Options:
1311
 *      trusted     :   If true the string won't be cleaned. Default false required noclean=true.
1312
 *      noclean     :   If true the string won't be cleaned, unless $CFG->forceclean is set. Default false required trusted=true.
1313
 *      filter      :   If true the string will be run through applicable filters as well. Default true.
1314
 *      para        :   If true then the returned string will be wrapped in div tags. Default true.
1315
 *      newlines    :   If true then lines newline breaks will be converted to HTML newline breaks. Default true.
1316
 *      context     :   The context that will be used for filtering.
1317
 *      overflowdiv :   If set to true the formatted text will be encased in a div
1318
 *                      with the class no-overflow before being returned. Default false.
1319
 *      allowid     :   If true then id attributes will not be removed, even when
1320
 *                      using htmlpurifier. Default false.
1321
 *      blanktarget :   If true all <a> tags will have target="_blank" added unless target is explicitly specified.
1322
 * </pre>
1323
 *
1324
 * @param string $text The text to be formatted. This is raw text originally from user input.
1325
 * @param int $format Identifier of the text format to be used
1326
 *            [FORMAT_MOODLE, FORMAT_HTML, FORMAT_PLAIN, FORMAT_MARKDOWN]
1327
 * @param stdClass|array $options text formatting options
1328
 * @param int $courseiddonotuse deprecated course id, use context option instead
1329
 * @return string
1330
 */
1331
function format_text($text, $format = FORMAT_MOODLE, $options = null, $courseiddonotuse = null) {
1332
    global $CFG;
1333
 
1334
    // Manually include the formatting class for now until after the release after 4.5 LTS.
1335
    require_once("{$CFG->libdir}/classes/formatting.php");
1336
 
1337
    if ($format === FORMAT_WIKI) {
1338
        // This format was deprecated in Moodle 1.5.
1339
        throw new \coding_exception(
1340
            'Wiki-like formatting is not supported.'
1341
        );
1342
    }
1343
 
1344
    if ($options instanceof \core\context) {
1345
        // A common mistake has been to call this function with a context object.
1346
        // This has never been expected, or nor supported.
1347
        debugging(
1348
            'The options argument should not be a context object directly. ' .
1349
                ' Please pass an array with a context key instead.',
1350
            DEBUG_DEVELOPER,
1351
        );
1352
        $params['context'] = $options;
1353
        $options = [];
1354
    }
1355
 
1356
    if ($options) {
1357
        $options = (array) $options;
1358
    }
1359
 
1360
    if (empty($CFG->version) || $CFG->version < 2013051400 || during_initial_install()) {
1361
        // Do not filter anything during installation or before upgrade completes.
1362
        $params['context'] = null;
1363
    } else if ($options && isset($options['context'])) { // First by explicit passed context option.
1364
        if (is_numeric($options['context'])) {
1365
            // A contextid was passed.
1366
            $params['context'] = \core\context::instance_by_id($options['context']);
1367
        } else if ($options['context'] instanceof \core\context) {
1368
            $params['context'] = $options['context'];
1369
        } else {
1370
            debugging(
1371
                'Unknown context passed to format_text(). Content will not be filtered.',
1372
                DEBUG_DEVELOPER,
1373
            );
1374
        }
1375
 
1376
        // Unset the context from $options to prevent it overriding the configured value.
1377
        unset($options['context']);
1378
    } else if ($courseiddonotuse) {
1379
        // Legacy courseid.
1380
        $params['context'] = \core\context\course::instance($courseiddonotuse);
1381
        debugging(
1382
            "Passing a courseid to format_text() is deprecated, please pass a context instead.",
1383
            DEBUG_DEVELOPER,
1384
        );
1385
    }
1386
 
1387
    $params['text'] =  $text;
1388
 
1389
    if ($options) {
1390
        // The smiley option was deprecated in Moodle 2.0.
1391
        if (array_key_exists('smiley', $options)) {
1392
            unset($options['smiley']);
1393
            debugging(
1394
                'The smiley option is deprecated and no longer used.',
1395
                DEBUG_DEVELOPER,
1396
            );
1397
        }
1398
 
1399
        // The nocache option was deprecated in Moodle 2.3 in MDL-34347.
1400
        if (array_key_exists('nocache', $options)) {
1401
            unset($options['nocache']);
1402
            debugging(
1403
                'The nocache option is deprecated and no longer used.',
1404
                DEBUG_DEVELOPER,
1405
            );
1406
        }
1407
 
1408
        $validoptions = [
1409
            'text',
1410
            'format',
1411
            'context',
1412
            'trusted',
1413
            'clean',
1414
            'filter',
1415
            'para',
1416
            'newlines',
1417
            'overflowdiv',
1418
            'blanktarget',
1419
            'allowid',
1420
            'noclean',
1421
        ];
1422
 
1423
        $invalidoptions = array_diff(array_keys($options), $validoptions);
1424
        if ($invalidoptions) {
1425
            debugging(sprintf(
1426
                'The following options are not valid: %s',
1427
                implode(', ', $invalidoptions),
1428
            ), DEBUG_DEVELOPER);
1429
            foreach ($invalidoptions as $option) {
1430
                unset($options[$option]);
1431
            }
1432
        }
1433
 
1434
        foreach ($options as $option => $value) {
1435
            $params[$option] = $value;
1436
        }
1437
 
1438
        // The noclean option has been renamed to clean.
1439
        if (array_key_exists('noclean', $params)) {
1440
            $params['clean'] = !$params['noclean'];
1441
            unset($params['noclean']);
1442
        }
1443
    }
1444
 
1445
    if ($format !== null) {
1446
        $params['format'] = $format;
1447
    }
1448
 
1449
    return \core\di::get(\core\formatting::class)->format_text(...$params);
1450
}
1451
 
1452
/**
1453
 * Resets some data related to filters, called during upgrade or when general filter settings change.
1454
 *
1455
 * @param bool $phpunitreset true means called from our PHPUnit integration test reset
1456
 * @return void
1457
 */
1458
function reset_text_filters_cache($phpunitreset = false) {
1459
    global $CFG, $DB;
1460
 
1461
    if ($phpunitreset) {
1462
        // HTMLPurifier does not change, DB is already reset to defaults,
1463
        // nothing to do here, the dataroot was cleared too.
1464
        return;
1465
    }
1466
 
1467
    // The purge_all_caches() deals with cachedir and localcachedir purging,
1468
    // the individual filter caches are invalidated as necessary elsewhere.
1469
 
1470
    // Update $CFG->filterall cache flag.
1471
    if (empty($CFG->stringfilters)) {
1472
        set_config('filterall', 0);
1473
        return;
1474
    }
1475
    $installedfilters = core_component::get_plugin_list('filter');
1476
    $filters = explode(',', $CFG->stringfilters);
1477
    foreach ($filters as $filter) {
1478
        if (isset($installedfilters[$filter])) {
1479
            set_config('filterall', 1);
1480
            return;
1481
        }
1482
    }
1483
    set_config('filterall', 0);
1484
}
1485
 
1486
/**
1487
 * Given a simple string, this function returns the string
1488
 * processed by enabled string filters if $CFG->filterall is enabled
1489
 *
1490
 * This function should be used to print short strings (non html) that
1491
 * need filter processing e.g. activity titles, post subjects,
1492
 * glossary concepts.
1493
 *
1494
 * @staticvar bool $strcache
1495
 * @param string $string The string to be filtered. Should be plain text, expect
1496
 * possibly for multilang tags.
1497
 * @param ?bool $striplinks To strip any link in the result text. Moodle 1.8 default changed from false to true! MDL-8713
1498
 * @param array $options options array/object or courseid
1499
 * @return string
1500
 */
1501
function format_string($string, $striplinks = true, $options = null) {
1502
    global $CFG;
1503
 
1504
    // Manually include the formatting class for now until after the release after 4.5 LTS.
1505
    require_once("{$CFG->libdir}/classes/formatting.php");
1506
 
1507
    $params = [
1508
        'string' => $string,
1509
        'striplinks' => (bool) $striplinks,
1510
    ];
1511
 
1512
    // This method only expects either:
1513
    // - an array of options;
1514
    // - a stdClass of options to be cast to an array; or
1515
    // - an integer courseid.
1516
    if ($options instanceof \core\context) {
1517
        // A common mistake has been to call this function with a context object.
1518
        // This has never been expected, or nor supported.
1519
        debugging(
1520
            'The options argument should not be a context object directly. ' .
1521
                ' Please pass an array with a context key instead.',
1522
            DEBUG_DEVELOPER,
1523
        );
1524
        $params['context'] = $options;
1525
        $options = [];
1526
    } else if (is_numeric($options)) {
1527
        // Legacy courseid usage.
1528
        $params['context'] = \core\context\course::instance($options);
1529
        $options = [];
1530
    } else if (is_array($options) || is_a($options, \stdClass::class)) {
1531
        $options = (array) $options;
1532
        if (isset($options['context'])) {
1533
            if (is_numeric($options['context'])) {
1534
                // A contextid was passed usage.
1535
                $params['context'] = \core\context::instance_by_id($options['context']);
1536
            } else if ($options['context'] instanceof \core\context) {
1537
                $params['context'] = $options['context'];
1538
            } else {
1539
                debugging(
1540
                    'An invalid value for context was provided.',
1541
                    DEBUG_DEVELOPER,
1542
                );
1543
            }
1544
        }
1545
    } else if ($options !== null) {
1546
        // Something else was passed, so we'll just use an empty array.
1547
        debugging(sprintf(
1548
            'The options argument should be an Array, or stdclass. %s passed.',
1549
            gettype($options),
1550
        ), DEBUG_DEVELOPER);
1551
 
1552
        // Attempt to cast to array since we always used to, but throw in some debugging.
1553
        $options = array_filter(
1554
            (array) $options,
1555
            fn ($key) => !is_numeric($key),
1556
            ARRAY_FILTER_USE_KEY,
1557
        );
1558
    }
1559
 
1560
    if (isset($options['filter'])) {
1561
        $params['filter'] = (bool) $options['filter'];
1562
    } else {
1563
        $params['filter'] = true;
1564
    }
1565
 
1566
    if (isset($options['escape'])) {
1567
        $params['escape'] = (bool) $options['escape'];
1568
    } else {
1569
        $params['escape'] = true;
1570
    }
1571
 
1572
    $validoptions = [
1573
        'string',
1574
        'striplinks',
1575
        'context',
1576
        'filter',
1577
        'escape',
1578
    ];
1579
 
1580
    if ($options) {
1581
        $invalidoptions = array_diff(array_keys($options), $validoptions);
1582
        if ($invalidoptions) {
1583
            debugging(sprintf(
1584
                'The following options are not valid: %s',
1585
                implode(', ', $invalidoptions),
1586
            ), DEBUG_DEVELOPER);
1587
        }
1588
    }
1589
 
1590
    return \core\di::get(\core\formatting::class)->format_string(
1591
        ...$params,
1592
    );
1593
}
1594
 
1595
/**
1596
 * Given a string, performs a negative lookahead looking for any ampersand character
1597
 * that is not followed by a proper HTML entity. If any is found, it is replaced
1598
 * by &amp;. The string is then returned.
1599
 *
1600
 * @param string $string
1601
 * @return string
1602
 */
1603
function replace_ampersands_not_followed_by_entity($string) {
1604
    return preg_replace("/\&(?![a-zA-Z0-9#]{1,8};)/", "&amp;", $string ?? '');
1605
}
1606
 
1607
/**
1608
 * Given a string, replaces all <a>.*</a> by .* and returns the string.
1609
 *
1610
 * @param string $string
1611
 * @return string
1612
 */
1613
function strip_links($string) {
1614
    return preg_replace('/(<a\s[^>]+?>)(.+?)(<\/a>)/is', '$2', $string);
1615
}
1616
 
1617
/**
1618
 * This expression turns links into something nice in a text format. (Russell Jungwirth)
1619
 *
1620
 * @param string $string
1621
 * @return string
1622
 */
1623
function wikify_links($string) {
1624
    return preg_replace('~(<a [^<]*href=["|\']?([^ "\']*)["|\']?[^>]*>([^<]*)</a>)~i', '$3 [ $2 ]', $string);
1625
}
1626
 
1627
/**
1628
 * Given text in a variety of format codings, this function returns the text as plain text suitable for plain email.
1629
 *
1630
 * @param string $text The text to be formatted. This is raw text originally from user input.
1631
 * @param int $format Identifier of the text format to be used
1632
 *            [FORMAT_MOODLE, FORMAT_HTML, FORMAT_PLAIN, FORMAT_WIKI, FORMAT_MARKDOWN]
1633
 * @return string
1634
 */
1635
function format_text_email($text, $format) {
1636
 
1637
    switch ($format) {
1638
 
1639
        case FORMAT_PLAIN:
1640
            return $text;
1641
            break;
1642
 
1643
        case FORMAT_WIKI:
1644
            // There should not be any of these any more!
1645
            $text = wikify_links($text);
1646
            return core_text::entities_to_utf8(strip_tags($text), true);
1647
            break;
1648
 
1649
        case FORMAT_HTML:
1650
            return html_to_text($text);
1651
            break;
1652
 
1653
        case FORMAT_MOODLE:
1654
        case FORMAT_MARKDOWN:
1655
        default:
1656
            $text = wikify_links($text);
1657
            return core_text::entities_to_utf8(strip_tags($text), true);
1658
            break;
1659
    }
1660
}
1661
 
1662
/**
1663
 * Formats activity intro text
1664
 *
1665
 * @param string $module name of module
1666
 * @param object $activity instance of activity
1667
 * @param int $cmid course module id
1668
 * @param bool $filter filter resulting html text
1669
 * @return string
1670
 */
1671
function format_module_intro($module, $activity, $cmid, $filter=true) {
1672
    global $CFG;
1673
    require_once("$CFG->libdir/filelib.php");
1674
    $context = context_module::instance($cmid);
1675
    $options = array('noclean' => true, 'para' => false, 'filter' => $filter, 'context' => $context, 'overflowdiv' => true);
1676
    $intro = file_rewrite_pluginfile_urls($activity->intro, 'pluginfile.php', $context->id, 'mod_'.$module, 'intro', null);
1677
    return trim(format_text($intro, $activity->introformat, $options, null));
1678
}
1679
 
1680
/**
1681
 * Removes the usage of Moodle files from a text.
1682
 *
1683
 * In some rare cases we need to re-use a text that already has embedded links
1684
 * to some files hosted within Moodle. But the new area in which we will push
1685
 * this content does not support files... therefore we need to remove those files.
1686
 *
1687
 * @param string $source The text
1688
 * @return string The stripped text
1689
 */
1690
function strip_pluginfile_content($source) {
1691
    $baseurl = '@@PLUGINFILE@@';
1692
    // Looking for something like < .* "@@pluginfile@@.*" .* >
1693
    $pattern = '$<[^<>]+["\']' . $baseurl . '[^"\']*["\'][^<>]*>$';
1694
    $stripped = preg_replace($pattern, '', $source);
1695
    // Use purify html to rebalence potentially mismatched tags and generally cleanup.
1696
    return purify_html($stripped);
1697
}
1698
 
1699
/**
1700
 * Legacy function, used for cleaning of old forum and glossary text only.
1701
 *
1702
 * @param string $text text that may contain legacy TRUSTTEXT marker
1703
 * @return string text without legacy TRUSTTEXT marker
1704
 */
1705
function trusttext_strip($text) {
1706
    if (!is_string($text)) {
1707
        // This avoids the potential for an endless loop below.
1708
        throw new coding_exception('trusttext_strip parameter must be a string');
1709
    }
1710
    while (true) { // Removing nested TRUSTTEXT.
1711
        $orig = $text;
1712
        $text = str_replace('#####TRUSTTEXT#####', '', $text);
1713
        if (strcmp($orig, $text) === 0) {
1714
            return $text;
1715
        }
1716
    }
1717
}
1718
 
1719
/**
1720
 * Must be called before editing of all texts with trust flag. Removes all XSS nasties from texts stored in database if needed.
1721
 *
1722
 * @param stdClass $object data object with xxx, xxxformat and xxxtrust fields
1723
 * @param string $field name of text field
1724
 * @param context $context active context
1725
 * @return stdClass updated $object
1726
 */
1727
function trusttext_pre_edit($object, $field, $context) {
1728
    $trustfield  = $field.'trust';
1729
    $formatfield = $field.'format';
1730
 
1731
    if ($object->$formatfield == FORMAT_MARKDOWN) {
1732
        // We do not have a way to sanitise Markdown texts,
1733
        // luckily editors for this format should not have XSS problems.
1734
        return $object;
1735
    }
1736
 
1737
    if (!$object->$trustfield or !trusttext_trusted($context)) {
1738
        $object->$field = clean_text($object->$field, $object->$formatfield);
1739
    }
1740
 
1741
    return $object;
1742
}
1743
 
1744
/**
1745
 * Is current user trusted to enter no dangerous XSS in this context?
1746
 *
1747
 * Please note the user must be in fact trusted everywhere on this server!!
1748
 *
1749
 * @param context $context
1750
 * @return bool true if user trusted
1751
 */
1752
function trusttext_trusted($context) {
1753
    return (trusttext_active() and has_capability('moodle/site:trustcontent', $context));
1754
}
1755
 
1756
/**
1757
 * Is trusttext feature active?
1758
 *
1759
 * @return bool
1760
 */
1761
function trusttext_active() {
1762
    global $CFG;
1763
 
1764
    return !empty($CFG->enabletrusttext);
1765
}
1766
 
1767
/**
1768
 * Cleans raw text removing nasties.
1769
 *
1770
 * Given raw text (eg typed in by a user) this function cleans it up and removes any nasty tags that could mess up
1771
 * Moodle pages through XSS attacks.
1772
 *
1773
 * The result must be used as a HTML text fragment, this function can not cleanup random
1774
 * parts of html tags such as url or src attributes.
1775
 *
1776
 * NOTE: the format parameter was deprecated because we can safely clean only HTML.
1777
 *
1778
 * @param string $text The text to be cleaned
1779
 * @param int|string $format deprecated parameter, should always contain FORMAT_HTML or FORMAT_MOODLE
1780
 * @param array $options Array of options; currently only option supported is 'allowid' (if true,
1781
 *   does not remove id attributes when cleaning)
1782
 * @return string The cleaned up text
1783
 */
1784
function clean_text($text, $format = FORMAT_HTML, $options = array()) {
1785
    $text = (string)$text;
1786
 
1787
    if ($format != FORMAT_HTML and $format != FORMAT_HTML) {
1788
        // TODO: we need to standardise cleanup of text when loading it into editor first.
1789
        // debugging('clean_text() is designed to work only with html');.
1790
    }
1791
 
1792
    if ($format == FORMAT_PLAIN) {
1793
        return $text;
1794
    }
1795
 
1796
    if (is_purify_html_necessary($text)) {
1797
        $text = purify_html($text, $options);
1798
    }
1799
 
1800
    // Originally we tried to neutralise some script events here, it was a wrong approach because
1801
    // it was trivial to work around that (for example using style based XSS exploits).
1802
    // We must not give false sense of security here - all developers MUST understand how to use
1803
    // rawurlencode(), htmlentities(), htmlspecialchars(), p(), s(), moodle_url, html_writer and friends!!!
1804
 
1805
    return $text;
1806
}
1807
 
1808
/**
1809
 * Is it necessary to use HTMLPurifier?
1810
 *
1811
 * @private
1812
 * @param string $text
1813
 * @return bool false means html is safe and valid, true means use HTMLPurifier
1814
 */
1815
function is_purify_html_necessary($text) {
1816
    if ($text === '') {
1817
        return false;
1818
    }
1819
 
1820
    if ($text === (string)((int)$text)) {
1821
        return false;
1822
    }
1823
 
1824
    if (strpos($text, '&') !== false or preg_match('|<[^pesb/]|', $text)) {
1825
        // We need to normalise entities or other tags except p, em, strong and br present.
1826
        return true;
1827
    }
1828
 
1829
    $altered = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8', true);
1830
    if ($altered === $text) {
1831
        // No < > or other special chars means this must be safe.
1832
        return false;
1833
    }
1834
 
1835
    // Let's try to convert back some safe html tags.
1836
    $altered = preg_replace('|&lt;p&gt;(.*?)&lt;/p&gt;|m', '<p>$1</p>', $altered);
1837
    if ($altered === $text) {
1838
        return false;
1839
    }
1840
    $altered = preg_replace('|&lt;em&gt;([^<>]+?)&lt;/em&gt;|m', '<em>$1</em>', $altered);
1841
    if ($altered === $text) {
1842
        return false;
1843
    }
1844
    $altered = preg_replace('|&lt;strong&gt;([^<>]+?)&lt;/strong&gt;|m', '<strong>$1</strong>', $altered);
1845
    if ($altered === $text) {
1846
        return false;
1847
    }
1848
    $altered = str_replace('&lt;br /&gt;', '<br />', $altered);
1849
    if ($altered === $text) {
1850
        return false;
1851
    }
1852
 
1853
    return true;
1854
}
1855
 
1856
/**
1857
 * KSES replacement cleaning function - uses HTML Purifier.
1858
 *
1859
 * @param string $text The (X)HTML string to purify
1860
 * @param array $options Array of options; currently only option supported is 'allowid' (if set,
1861
 *   does not remove id attributes when cleaning)
1862
 * @return string
1863
 */
1864
function purify_html($text, $options = array()) {
1865
    global $CFG;
1866
 
1867
    $text = (string)$text;
1868
 
1869
    static $purifiers = array();
1870
    static $caches = array();
1871
 
1872
    // Purifier code can change only during major version upgrade.
1873
    $version = empty($CFG->version) ? 0 : $CFG->version;
1874
    $cachedir = "$CFG->localcachedir/htmlpurifier/$version";
1875
    if (!file_exists($cachedir)) {
1876
        // Purging of caches may remove the cache dir at any time,
1877
        // luckily file_exists() results should be cached for all existing directories.
1878
        $purifiers = array();
1879
        $caches = array();
1880
        gc_collect_cycles();
1881
 
1882
        make_localcache_directory('htmlpurifier', false);
1883
        check_dir_exists($cachedir);
1884
    }
1885
 
1886
    $allowid = empty($options['allowid']) ? 0 : 1;
1887
    $allowobjectembed = empty($CFG->allowobjectembed) ? 0 : 1;
1888
 
1889
    $type = 'type_'.$allowid.'_'.$allowobjectembed;
1890
 
1891
    if (!array_key_exists($type, $caches)) {
1892
        $caches[$type] = cache::make('core', 'htmlpurifier', array('type' => $type));
1893
    }
1894
    $cache = $caches[$type];
1895
 
1896
    // Add revision number and all options to the text key so that it is compatible with local cluster node caches.
1897
    $key = "|$version|$allowobjectembed|$allowid|$text";
1898
    $filteredtext = $cache->get($key);
1899
 
1900
    if ($filteredtext === true) {
1901
        // The filtering did not change the text last time, no need to filter anything again.
1902
        return $text;
1903
    } else if ($filteredtext !== false) {
1904
        return $filteredtext;
1905
    }
1906
 
1907
    if (empty($purifiers[$type])) {
1908
        require_once $CFG->libdir.'/htmlpurifier/HTMLPurifier.safe-includes.php';
1909
        require_once $CFG->libdir.'/htmlpurifier/locallib.php';
1910
        $config = HTMLPurifier_Config::createDefault();
1911
 
1912
        $config->set('HTML.DefinitionID', 'moodlehtml');
1913
        $config->set('HTML.DefinitionRev', 7);
1914
        $config->set('Cache.SerializerPath', $cachedir);
1915
        $config->set('Cache.SerializerPermissions', $CFG->directorypermissions);
1916
        $config->set('Core.NormalizeNewlines', false);
1917
        $config->set('Core.ConvertDocumentToFragment', true);
1918
        $config->set('Core.Encoding', 'UTF-8');
1919
        $config->set('HTML.Doctype', 'XHTML 1.0 Transitional');
1920
        $config->set('URI.AllowedSchemes', array(
1921
            'http' => true,
1922
            'https' => true,
1923
            'ftp' => true,
1924
            'irc' => true,
1925
            'nntp' => true,
1926
            'news' => true,
1927
            'rtsp' => true,
1928
            'rtmp' => true,
1929
            'teamspeak' => true,
1930
            'gopher' => true,
1931
            'mms' => true,
1932
            'mailto' => true
1933
        ));
1934
        $config->set('Attr.AllowedFrameTargets', array('_blank'));
1935
 
1936
        if ($allowobjectembed) {
1937
            $config->set('HTML.SafeObject', true);
1938
            $config->set('Output.FlashCompat', true);
1939
            $config->set('HTML.SafeEmbed', true);
1940
        }
1941
 
1942
        if ($allowid) {
1943
            $config->set('Attr.EnableID', true);
1944
        }
1945
 
1946
        if ($def = $config->maybeGetRawHTMLDefinition()) {
1947
            $def->addElement('nolink', 'Inline', 'Flow', array());                      // Skip our filters inside.
1948
            $def->addElement('tex', 'Inline', 'Inline', array());                       // Tex syntax, equivalent to $$xx$$.
1949
            $def->addElement('algebra', 'Inline', 'Inline', array());                   // Algebra syntax, equivalent to @@xx@@.
1950
            $def->addElement('lang', 'Block', 'Flow', array(), array('lang'=>'CDATA')); // Original multilang style - only our hacked lang attribute.
1951
            $def->addAttribute('span', 'xxxlang', 'CDATA');                             // Current very problematic multilang.
1952
            // Enable the bidirectional isolate element and its span equivalent.
1953
            $def->addElement('bdi', 'Inline', 'Flow', 'Common');
1954
            $def->addAttribute('span', 'dir', 'Enum#ltr,rtl,auto');
1955
 
1956
            // Media elements.
1957
            // https://html.spec.whatwg.org/#the-video-element
1958
            $def->addElement('video', 'Inline', 'Optional: #PCDATA | Flow | source | track', 'Common', [
1959
                'src' => 'URI',
1960
                'crossorigin' => 'Enum#anonymous,use-credentials',
1961
                'poster' => 'URI',
1962
                'preload' => 'Enum#auto,metadata,none',
1963
                'autoplay' => 'Bool',
1964
                'playsinline' => 'Bool',
1965
                'loop' => 'Bool',
1966
                'muted' => 'Bool',
1967
                'controls' => 'Bool',
1968
                'width' => 'Length',
1969
                'height' => 'Length',
1970
            ]);
1971
            // https://html.spec.whatwg.org/#the-audio-element
1972
            $def->addElement('audio', 'Inline', 'Optional: #PCDATA | Flow | source | track', 'Common', [
1973
                'src' => 'URI',
1974
                'crossorigin' => 'Enum#anonymous,use-credentials',
1975
                'preload' => 'Enum#auto,metadata,none',
1976
                'autoplay' => 'Bool',
1977
                'loop' => 'Bool',
1978
                'muted' => 'Bool',
1979
                'controls' => 'Bool'
1980
            ]);
1981
            // https://html.spec.whatwg.org/#the-source-element
1982
            $def->addElement('source', false, 'Empty', null, [
1983
                'src' => 'URI',
1984
                'type' => 'Text'
1985
            ]);
1986
            // https://html.spec.whatwg.org/#the-track-element
1987
            $def->addElement('track', false, 'Empty', null, [
1988
                'src' => 'URI',
1989
                'kind' => 'Enum#subtitles,captions,descriptions,chapters,metadata',
1990
                'srclang' => 'Text',
1991
                'label' => 'Text',
1992
                'default' => 'Bool',
1993
            ]);
1994
 
1995
            // Use the built-in Ruby module to add annotation support.
1996
            $def->manager->addModule(new HTMLPurifier_HTMLModule_Ruby());
1997
        }
1998
 
1999
        $purifier = new HTMLPurifier($config);
2000
        $purifiers[$type] = $purifier;
2001
    } else {
2002
        $purifier = $purifiers[$type];
2003
    }
2004
 
2005
    $multilang = (strpos($text, 'class="multilang"') !== false);
2006
 
2007
    $filteredtext = $text;
2008
    if ($multilang) {
2009
        $filteredtextregex = '/<span(\s+lang="([a-zA-Z0-9_-]+)"|\s+class="multilang"){2}\s*>/';
2010
        $filteredtext = preg_replace($filteredtextregex, '<span xxxlang="${2}">', $filteredtext);
2011
    }
2012
    $filteredtext = (string)$purifier->purify($filteredtext);
2013
    if ($multilang) {
2014
        $filteredtext = preg_replace('/<span xxxlang="([a-zA-Z0-9_-]+)">/', '<span lang="${1}" class="multilang">', $filteredtext);
2015
    }
2016
 
2017
    if ($text === $filteredtext) {
2018
        // No need to store the filtered text, next time we will just return unfiltered text
2019
        // because it was not changed by purifying.
2020
        $cache->set($key, true);
2021
    } else {
2022
        $cache->set($key, $filteredtext);
2023
    }
2024
 
2025
    return $filteredtext;
2026
}
2027
 
2028
/**
2029
 * Given plain text, makes it into HTML as nicely as possible.
2030
 *
2031
 * May contain HTML tags already.
2032
 *
2033
 * Do not abuse this function. It is intended as lower level formatting feature used
2034
 * by {@link format_text()} to convert FORMAT_MOODLE to HTML. You are supposed
2035
 * to call format_text() in most of cases.
2036
 *
2037
 * @param string $text The string to convert.
2038
 * @param boolean $smileyignored Was used to determine if smiley characters should convert to smiley images, ignored now
2039
 * @param boolean $para If true then the returned string will be wrapped in div tags
2040
 * @param boolean $newlines If true then lines newline breaks will be converted to HTML newline breaks.
2041
 * @return string
2042
 */
2043
function text_to_html($text, $smileyignored = null, $para = true, $newlines = true) {
2044
    // Remove any whitespace that may be between HTML tags.
2045
    $text = preg_replace("~>([[:space:]]+)<~i", "><", $text);
2046
 
2047
    // Remove any returns that precede or follow HTML tags.
2048
    $text = preg_replace("~([\n\r])<~i", " <", $text);
2049
    $text = preg_replace("~>([\n\r])~i", "> ", $text);
2050
 
2051
    // Make returns into HTML newlines.
2052
    if ($newlines) {
2053
        $text = nl2br($text);
2054
    }
2055
 
2056
    // Wrap the whole thing in a div if required.
2057
    if ($para) {
2058
        // In 1.9 this was changed from a p => div.
2059
        return '<div class="text_to_html">'.$text.'</div>';
2060
    } else {
2061
        return $text;
2062
    }
2063
}
2064
 
2065
/**
2066
 * Given Markdown formatted text, make it into XHTML using external function
2067
 *
2068
 * @param string $text The markdown formatted text to be converted.
2069
 * @return string Converted text
2070
 */
2071
function markdown_to_html($text) {
2072
    global $CFG;
2073
 
2074
    if ($text === '' or $text === null) {
2075
        return $text;
2076
    }
2077
 
2078
    require_once($CFG->libdir .'/markdown/MarkdownInterface.php');
2079
    require_once($CFG->libdir .'/markdown/Markdown.php');
2080
    require_once($CFG->libdir .'/markdown/MarkdownExtra.php');
2081
 
2082
    return \Michelf\MarkdownExtra::defaultTransform($text);
2083
}
2084
 
2085
/**
2086
 * Given HTML text, make it into plain text using external function
2087
 *
2088
 * @param string $html The text to be converted.
2089
 * @param integer $width Width to wrap the text at. (optional, default 75 which
2090
 *      is a good value for email. 0 means do not limit line length.)
2091
 * @param boolean $dolinks By default, any links in the HTML are collected, and
2092
 *      printed as a list at the end of the HTML. If you don't want that, set this
2093
 *      argument to false.
2094
 * @return string plain text equivalent of the HTML.
2095
 */
2096
function html_to_text($html, $width = 75, $dolinks = true) {
2097
    global $CFG;
2098
 
2099
    require_once($CFG->libdir .'/html2text/lib.php');
2100
 
2101
    $options = array(
2102
        'width'     => $width,
2103
        'do_links'  => 'table',
2104
    );
2105
 
2106
    if (empty($dolinks)) {
2107
        $options['do_links'] = 'none';
2108
    }
2109
    $h2t = new core_html2text($html, $options);
2110
    $result = $h2t->getText();
2111
 
2112
    return $result;
2113
}
2114
 
2115
/**
2116
 * Converts texts or strings to plain text.
2117
 *
2118
 * - When used to convert user input introduced in an editor the text format needs to be passed in $contentformat like we usually
2119
 *   do in format_text.
2120
 * - When this function is used for strings that are usually passed through format_string before displaying them
2121
 *   we need to set $contentformat to false. This will execute html_to_text as these strings can contain multilang tags if
2122
 *   multilang filter is applied to headings.
2123
 *
2124
 * @param string $content The text as entered by the user
2125
 * @param int|false $contentformat False for strings or the text format: FORMAT_MOODLE/FORMAT_HTML/FORMAT_PLAIN/FORMAT_MARKDOWN
2126
 * @return string Plain text.
2127
 */
2128
function content_to_text($content, $contentformat) {
2129
 
2130
    switch ($contentformat) {
2131
        case FORMAT_PLAIN:
2132
            // Nothing here.
2133
            break;
2134
        case FORMAT_MARKDOWN:
2135
            $content = markdown_to_html($content);
2136
            $content = html_to_text($content, 75, false);
2137
            break;
2138
        default:
2139
            // FORMAT_HTML, FORMAT_MOODLE and $contentformat = false, the later one are strings usually formatted through
2140
            // format_string, we need to convert them from html because they can contain HTML (multilang filter).
2141
            $content = html_to_text($content, 75, false);
2142
    }
2143
 
2144
    return trim($content, "\r\n ");
2145
}
2146
 
2147
/**
2148
 * Factory method for extracting draft file links from arbitrary text using regular expressions. Only text
2149
 * is required; other file fields may be passed to filter.
2150
 *
2151
 * @param string $text Some html content.
2152
 * @param bool $forcehttps force https urls.
2153
 * @param int $contextid This parameter and the next three identify the file area to save to.
2154
 * @param string $component The component name.
2155
 * @param string $filearea The filearea.
2156
 * @param int $itemid The item id for the filearea.
2157
 * @param string $filename The specific filename of the file.
2158
 * @return array
2159
 */
2160
function extract_draft_file_urls_from_text($text, $forcehttps = false, $contextid = null, $component = null,
2161
                                           $filearea = null, $itemid = null, $filename = null) {
2162
    global $CFG;
2163
 
2164
    $wwwroot = $CFG->wwwroot;
2165
    if ($forcehttps) {
2166
        $wwwroot = str_replace('http://', 'https://', $wwwroot);
2167
    }
2168
    $urlstring = '/' . preg_quote($wwwroot, '/');
2169
 
2170
    $urlbase = preg_quote('draftfile.php');
2171
    $urlstring .= "\/(?<urlbase>{$urlbase})";
2172
 
2173
    if (is_null($contextid)) {
2174
        $contextid = '[0-9]+';
2175
    }
2176
    $urlstring .= "\/(?<contextid>{$contextid})";
2177
 
2178
    if (is_null($component)) {
2179
        $component = '[a-z_]+';
2180
    }
2181
    $urlstring .= "\/(?<component>{$component})";
2182
 
2183
    if (is_null($filearea)) {
2184
        $filearea = '[a-z_]+';
2185
    }
2186
    $urlstring .= "\/(?<filearea>{$filearea})";
2187
 
2188
    if (is_null($itemid)) {
2189
        $itemid = '[0-9]+';
2190
    }
2191
    $urlstring .= "\/(?<itemid>{$itemid})";
2192
 
2193
    // Filename matching magic based on file_rewrite_urls_to_pluginfile().
2194
    if (is_null($filename)) {
2195
        $filename = '[^\'\",&<>|`\s:\\\\]+';
2196
    }
2197
    $urlstring .= "\/(?<filename>{$filename})/";
2198
 
2199
    // Regular expression which matches URLs and returns their components.
2200
    preg_match_all($urlstring, $text, $urls, PREG_SET_ORDER);
2201
    return $urls;
2202
}
2203
 
2204
/**
2205
 * This function will highlight search words in a given string
2206
 *
2207
 * It cares about HTML and will not ruin links.  It's best to use
2208
 * this function after performing any conversions to HTML.
2209
 *
2210
 * @param string $needle The search string. Syntax like "word1 +word2 -word3" is dealt with correctly.
2211
 * @param string $haystack The string (HTML) within which to highlight the search terms.
2212
 * @param boolean $matchcase whether to do case-sensitive. Default case-insensitive.
2213
 * @param string $prefix the string to put before each search term found.
2214
 * @param string $suffix the string to put after each search term found.
2215
 * @return string The highlighted HTML.
2216
 */
2217
function highlight($needle, $haystack, $matchcase = false,
2218
        $prefix = '<span class="highlight">', $suffix = '</span>') {
2219
 
2220
    // Quick bail-out in trivial cases.
2221
    if (empty($needle) or empty($haystack)) {
2222
        return $haystack;
2223
    }
2224
 
2225
    // Break up the search term into words, discard any -words and build a regexp.
2226
    $words = preg_split('/ +/', trim($needle));
2227
    foreach ($words as $index => $word) {
2228
        if (strpos($word, '-') === 0) {
2229
            unset($words[$index]);
2230
        } else if (strpos($word, '+') === 0) {
2231
            $words[$index] = '\b' . preg_quote(ltrim($word, '+'), '/') . '\b'; // Match only as a complete word.
2232
        } else {
2233
            $words[$index] = preg_quote($word, '/');
2234
        }
2235
    }
2236
    $regexp = '/(' . implode('|', $words) . ')/u'; // Char u is to do UTF-8 matching.
2237
    if (!$matchcase) {
2238
        $regexp .= 'i';
2239
    }
2240
 
2241
    // Another chance to bail-out if $search was only -words.
2242
    if (empty($words)) {
2243
        return $haystack;
2244
    }
2245
 
2246
    // Split the string into HTML tags and real content.
2247
    $chunks = preg_split('/((?:<[^>]*>)+)/', $haystack, -1, PREG_SPLIT_DELIM_CAPTURE);
2248
 
2249
    // We have an array of alternating blocks of text, then HTML tags, then text, ...
2250
    // Loop through replacing search terms in the text, and leaving the HTML unchanged.
2251
    $ishtmlchunk = false;
2252
    $result = '';
2253
    foreach ($chunks as $chunk) {
2254
        if ($ishtmlchunk) {
2255
            $result .= $chunk;
2256
        } else {
2257
            $result .= preg_replace($regexp, $prefix . '$1' . $suffix, $chunk);
2258
        }
2259
        $ishtmlchunk = !$ishtmlchunk;
2260
    }
2261
 
2262
    return $result;
2263
}
2264
 
2265
/**
2266
 * This function will highlight instances of $needle in $haystack
2267
 *
2268
 * It's faster that the above function {@link highlight()} and doesn't care about
2269
 * HTML or anything.
2270
 *
2271
 * @param string $needle The string to search for
2272
 * @param string $haystack The string to search for $needle in
2273
 * @return string The highlighted HTML
2274
 */
2275
function highlightfast($needle, $haystack) {
2276
 
2277
    if (empty($needle) or empty($haystack)) {
2278
        return $haystack;
2279
    }
2280
 
2281
    $parts = explode(core_text::strtolower($needle), core_text::strtolower($haystack));
2282
 
2283
    if (count($parts) === 1) {
2284
        return $haystack;
2285
    }
2286
 
2287
    $pos = 0;
2288
 
2289
    foreach ($parts as $key => $part) {
2290
        $parts[$key] = substr($haystack, $pos, strlen($part));
2291
        $pos += strlen($part);
2292
 
2293
        $parts[$key] .= '<span class="highlight">'.substr($haystack, $pos, strlen($needle)).'</span>';
2294
        $pos += strlen($needle);
2295
    }
2296
 
2297
    return str_replace('<span class="highlight"></span>', '', join('', $parts));
2298
}
2299
 
2300
/**
2301
 * Converts a language code to hyphen-separated format in accordance to the
2302
 * {@link https://datatracker.ietf.org/doc/html/rfc5646#section-2.1 BCP47 syntax}.
2303
 *
2304
 * For additional information, check out
2305
 * {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang MDN web docs - lang}.
2306
 *
2307
 * @param string $langcode The language code to convert.
2308
 * @return string
2309
 */
2310
function get_html_lang_attribute_value(string $langcode): string {
2311
    $langcode = clean_param($langcode, PARAM_LANG);
2312
    if ($langcode === '') {
2313
        return 'en';
2314
    }
2315
 
2316
    // Grab language ISO code from lang config. If it differs from English, then it's been specified and we can return it.
2317
    $langiso = (string) (new lang_string('iso6391', 'core_langconfig', null, $langcode));
2318
    if ($langiso !== 'en') {
2319
        return $langiso;
2320
    }
2321
 
2322
    // Where we cannot determine the value from lang config, use the first two characters from the lang code.
2323
    return substr($langcode, 0, 2);
2324
}
2325
 
2326
/**
2327
 * Return a string containing 'lang', xml:lang and optionally 'dir' HTML attributes.
2328
 *
2329
 * Internationalisation, for print_header and backup/restorelib.
2330
 *
2331
 * @param bool $dir Default false
2332
 * @return string Attributes
2333
 */
2334
function get_html_lang($dir = false) {
2335
    global $CFG;
2336
 
2337
    $currentlang = current_language();
2338
    if (isset($CFG->lang) && $currentlang !== $CFG->lang && !get_string_manager()->translation_exists($currentlang)) {
2339
        // Use the default site language when the current language is not available.
2340
        $currentlang = $CFG->lang;
2341
        // Fix the current language.
2342
        fix_current_language($currentlang);
2343
    }
2344
 
2345
    $direction = '';
2346
    if ($dir) {
2347
        if (right_to_left()) {
2348
            $direction = ' dir="rtl"';
2349
        } else {
2350
            $direction = ' dir="ltr"';
2351
        }
2352
    }
2353
 
2354
    // Accessibility: added the 'lang' attribute to $direction, used in theme <html> tag.
2355
    $language = get_html_lang_attribute_value($currentlang);
2356
    @header('Content-Language: '.$language);
2357
    return ($direction.' lang="'.$language.'" xml:lang="'.$language.'"');
2358
}
2359
 
2360
 
2361
// STANDARD WEB PAGE PARTS.
2362
 
2363
/**
2364
 * Send the HTTP headers that Moodle requires.
2365
 *
2366
 * There is a backwards compatibility hack for legacy code
2367
 * that needs to add custom IE compatibility directive.
2368
 *
2369
 * Example:
2370
 * <code>
2371
 * if (!isset($CFG->additionalhtmlhead)) {
2372
 *     $CFG->additionalhtmlhead = '';
2373
 * }
2374
 * $CFG->additionalhtmlhead .= '<meta http-equiv="X-UA-Compatible" content="IE=8" />';
2375
 * header('X-UA-Compatible: IE=8');
2376
 * echo $OUTPUT->header();
2377
 * </code>
2378
 *
2379
 * Please note the $CFG->additionalhtmlhead alone might not work,
2380
 * you should send the IE compatibility header() too.
2381
 *
2382
 * @param string $contenttype
2383
 * @param bool $cacheable Can this page be cached on back?
2384
 * @return void, sends HTTP headers
2385
 */
2386
function send_headers($contenttype, $cacheable = true) {
2387
    global $CFG;
2388
 
2389
    @header('Content-Type: ' . $contenttype);
2390
    @header('Content-Script-Type: text/javascript');
2391
    @header('Content-Style-Type: text/css');
2392
 
2393
    if (empty($CFG->additionalhtmlhead) or stripos($CFG->additionalhtmlhead, 'X-UA-Compatible') === false) {
2394
        @header('X-UA-Compatible: IE=edge');
2395
    }
2396
 
2397
    if ($cacheable) {
2398
        // Allow caching on "back" (but not on normal clicks).
2399
        @header('Cache-Control: private, pre-check=0, post-check=0, max-age=0, no-transform');
2400
        @header('Pragma: no-cache');
2401
        @header('Expires: ');
2402
    } else {
2403
        // Do everything we can to always prevent clients and proxies caching.
2404
        @header('Cache-Control: no-store, no-cache, must-revalidate');
2405
        @header('Cache-Control: post-check=0, pre-check=0, no-transform', false);
2406
        @header('Pragma: no-cache');
2407
        @header('Expires: Mon, 20 Aug 1969 09:23:00 GMT');
2408
        @header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
2409
    }
2410
    @header('Accept-Ranges: none');
2411
 
2412
    // The Moodle app must be allowed to embed content always.
2413
    if (empty($CFG->allowframembedding) && !core_useragent::is_moodle_app()) {
2414
        @header('X-Frame-Options: sameorigin');
2415
    }
2416
 
2417
    // If referrer policy is set, add a referrer header.
2418
    if (!empty($CFG->referrerpolicy) && ($CFG->referrerpolicy !== 'default')) {
2419
        @header('Referrer-Policy: ' . $CFG->referrerpolicy);
2420
    }
2421
}
2422
 
2423
/**
2424
 * Return the right arrow with text ('next'), and optionally embedded in a link.
2425
 *
2426
 * @param string $text HTML/plain text label (set to blank only for breadcrumb separator cases).
2427
 * @param string $url An optional link to use in a surrounding HTML anchor.
2428
 * @param bool $accesshide True if text should be hidden (for screen readers only).
2429
 * @param string $addclass Additional class names for the link, or the arrow character.
2430
 * @return string HTML string.
2431
 */
2432
function link_arrow_right($text, $url='', $accesshide=false, $addclass='', $addparams = []) {
2433
    global $OUTPUT; // TODO: move to output renderer.
2434
    $arrowclass = 'arrow ';
2435
    if (!$url) {
2436
        $arrowclass .= $addclass;
2437
    }
2438
    $arrow = '<span class="'.$arrowclass.'" aria-hidden="true">'.$OUTPUT->rarrow().'</span>';
2439
    $htmltext = '';
2440
    if ($text) {
2441
        $htmltext = '<span class="arrow_text">'.$text.'</span>&nbsp;';
2442
        if ($accesshide) {
2443
            $htmltext = get_accesshide($htmltext);
2444
        }
2445
    }
2446
    if ($url) {
2447
        $class = 'arrow_link';
2448
        if ($addclass) {
2449
            $class .= ' '.$addclass;
2450
        }
2451
 
2452
        $linkparams = [
2453
            'class' => $class,
2454
            'href' => $url,
2455
            'title' => preg_replace('/<.*?>/', '', $text),
2456
        ];
2457
 
2458
        $linkparams += $addparams;
2459
 
2460
        return html_writer::link($url, $htmltext . $arrow, $linkparams);
2461
    }
2462
    return $htmltext.$arrow;
2463
}
2464
 
2465
/**
2466
 * Return the left arrow with text ('previous'), and optionally embedded in a link.
2467
 *
2468
 * @param string $text HTML/plain text label (set to blank only for breadcrumb separator cases).
2469
 * @param string $url An optional link to use in a surrounding HTML anchor.
2470
 * @param bool $accesshide True if text should be hidden (for screen readers only).
2471
 * @param string $addclass Additional class names for the link, or the arrow character.
2472
 * @return string HTML string.
2473
 */
2474
function link_arrow_left($text, $url='', $accesshide=false, $addclass='', $addparams = []) {
2475
    global $OUTPUT; // TODO: move to utput renderer.
2476
    $arrowclass = 'arrow ';
2477
    if (! $url) {
2478
        $arrowclass .= $addclass;
2479
    }
2480
    $arrow = '<span class="'.$arrowclass.'" aria-hidden="true">'.$OUTPUT->larrow().'</span>';
2481
    $htmltext = '';
2482
    if ($text) {
2483
        $htmltext = '&nbsp;<span class="arrow_text">'.$text.'</span>';
2484
        if ($accesshide) {
2485
            $htmltext = get_accesshide($htmltext);
2486
        }
2487
    }
2488
    if ($url) {
2489
        $class = 'arrow_link';
2490
        if ($addclass) {
2491
            $class .= ' '.$addclass;
2492
        }
2493
 
2494
        $linkparams = [
2495
            'class' => $class,
2496
            'href' => $url,
2497
            'title' => preg_replace('/<.*?>/', '', $text),
2498
        ];
2499
 
2500
        $linkparams += $addparams;
2501
 
2502
        return html_writer::link($url, $arrow . $htmltext, $linkparams);
2503
    }
2504
    return $arrow.$htmltext;
2505
}
2506
 
2507
/**
2508
 * Return a HTML element with the class "accesshide", for accessibility.
2509
 *
2510
 * Please use cautiously - where possible, text should be visible!
2511
 *
2512
 * @param string $text Plain text.
2513
 * @param string $elem Lowercase element name, default "span".
2514
 * @param string $class Additional classes for the element.
2515
 * @param string $attrs Additional attributes string in the form, "name='value' name2='value2'"
2516
 * @return string HTML string.
2517
 */
2518
function get_accesshide($text, $elem='span', $class='', $attrs='') {
2519
    return "<$elem class=\"accesshide $class\" $attrs>$text</$elem>";
2520
}
2521
 
2522
/**
2523
 * Return the breadcrumb trail navigation separator.
2524
 *
2525
 * @return string HTML string.
2526
 */
2527
function get_separator() {
2528
    // Accessibility: the 'hidden' slash is preferred for screen readers.
2529
    return ' '.link_arrow_right($text='/', $url='', $accesshide=true, 'sep').' ';
2530
}
2531
 
2532
/**
2533
 * Print (or return) a collapsible region, that has a caption that can be clicked to expand or collapse the region.
2534
 *
2535
 * If JavaScript is off, then the region will always be expanded.
2536
 *
2537
 * @param string $contents the contents of the box.
2538
 * @param string $classes class names added to the div that is output.
2539
 * @param string $id id added to the div that is output. Must not be blank.
2540
 * @param string $caption text displayed at the top. Clicking on this will cause the region to expand or contract.
2541
 * @param string $userpref the name of the user preference that stores the user's preferred default state.
2542
 *      (May be blank if you do not wish the state to be persisted.
2543
 * @param boolean $default Initial collapsed state to use if the user_preference it not set.
2544
 * @param boolean $return if true, return the HTML as a string, rather than printing it.
2545
 * @return string|void If $return is false, returns nothing, otherwise returns a string of HTML.
2546
 */
2547
function print_collapsible_region($contents, $classes, $id, $caption, $userpref = '', $default = false, $return = false) {
2548
    $output  = print_collapsible_region_start($classes, $id, $caption, $userpref, $default, true);
2549
    $output .= $contents;
2550
    $output .= print_collapsible_region_end(true);
2551
 
2552
    if ($return) {
2553
        return $output;
2554
    } else {
2555
        echo $output;
2556
    }
2557
}
2558
 
2559
/**
2560
 * Print (or return) the start of a collapsible region
2561
 *
2562
 * The collapsibleregion has a caption that can be clicked to expand or collapse the region. If JavaScript is off, then the region
2563
 * will always be expanded.
2564
 *
2565
 * @param string $classes class names added to the div that is output.
2566
 * @param string $id id added to the div that is output. Must not be blank.
2567
 * @param string $caption text displayed at the top. Clicking on this will cause the region to expand or contract.
2568
 * @param string $userpref the name of the user preference that stores the user's preferred default state.
2569
 *      (May be blank if you do not wish the state to be persisted.
2570
 * @param boolean $default Initial collapsed state to use if the user_preference it not set.
2571
 * @param boolean $return if true, return the HTML as a string, rather than printing it.
2572
 * @param string $extracontent the extra content will show next to caption, eg.Help icon.
2573
 * @return string|void if $return is false, returns nothing, otherwise returns a string of HTML.
2574
 */
2575
function print_collapsible_region_start($classes, $id, $caption, $userpref = '', $default = false, $return = false,
2576
        $extracontent = null) {
2577
    global $PAGE;
2578
 
2579
    // Work out the initial state.
2580
    if (!empty($userpref) and is_string($userpref)) {
2581
        $collapsed = get_user_preferences($userpref, $default);
2582
    } else {
2583
        $collapsed = $default;
2584
        $userpref = false;
2585
    }
2586
 
2587
    if ($collapsed) {
2588
        $classes .= ' collapsed';
2589
    }
2590
 
2591
    $output = '';
2592
    $output .= '<div id="' . $id . '" class="collapsibleregion ' . $classes . '">';
2593
    $output .= '<div id="' . $id . '_sizer">';
2594
    $output .= '<div id="' . $id . '_caption" class="collapsibleregioncaption">';
2595
    $output .= $caption . ' </div>';
2596
    if ($extracontent) {
2597
        $output .= html_writer::div($extracontent, 'collapsibleregionextracontent');
2598
    }
2599
    $output .= '<div id="' . $id . '_inner" class="collapsibleregioninner">';
2600
    $PAGE->requires->js_init_call('M.util.init_collapsible_region', array($id, $userpref, get_string('clicktohideshow')));
2601
 
2602
    if ($return) {
2603
        return $output;
2604
    } else {
2605
        echo $output;
2606
    }
2607
}
2608
 
2609
/**
2610
 * Close a region started with print_collapsible_region_start.
2611
 *
2612
 * @param boolean $return if true, return the HTML as a string, rather than printing it.
2613
 * @return string|void if $return is false, returns nothing, otherwise returns a string of HTML.
2614
 */
2615
function print_collapsible_region_end($return = false) {
2616
    $output = '</div></div></div>';
2617
 
2618
    if ($return) {
2619
        return $output;
2620
    } else {
2621
        echo $output;
2622
    }
2623
}
2624
 
2625
/**
2626
 * Print a specified group's avatar.
2627
 *
2628
 * @param array|stdClass $group A single {@link group} object OR array of groups.
2629
 * @param int $courseid The course ID.
2630
 * @param boolean $large Default small picture, or large.
2631
 * @param boolean $return If false print picture, otherwise return the output as string
2632
 * @param boolean $link Enclose image in a link to view specified course?
2633
 * @param boolean $includetoken Whether to use a user token when displaying this group image.
2634
 *                True indicates to generate a token for current user, and integer value indicates to generate a token for the
2635
 *                user whose id is the value indicated.
2636
 *                If the group picture is included in an e-mail or some other location where the audience is a specific
2637
 *                user who will not be logged in when viewing, then we use a token to authenticate the user.
2638
 * @return string|void Depending on the setting of $return
2639
 */
2640
function print_group_picture($group, $courseid, $large = false, $return = false, $link = true, $includetoken = false) {
2641
    global $CFG;
2642
 
2643
    if (is_array($group)) {
2644
        $output = '';
2645
        foreach ($group as $g) {
2646
            $output .= print_group_picture($g, $courseid, $large, true, $link, $includetoken);
2647
        }
2648
        if ($return) {
2649
            return $output;
2650
        } else {
2651
            echo $output;
2652
            return;
2653
        }
2654
    }
2655
 
2656
    $pictureurl = get_group_picture_url($group, $courseid, $large, $includetoken);
2657
 
2658
    // If there is no picture, do nothing.
2659
    if (!isset($pictureurl)) {
2660
        return;
2661
    }
2662
 
2663
    $context = context_course::instance($courseid);
2664
 
2665
    $groupname = s($group->name);
2666
    $pictureimage = html_writer::img($pictureurl, $groupname, ['title' => $groupname]);
2667
 
2668
    $output = '';
2669
    if ($link or has_capability('moodle/site:accessallgroups', $context)) {
2670
        $linkurl = new moodle_url('/user/index.php', ['id' => $courseid, 'group' => $group->id]);
2671
        $output .= html_writer::link($linkurl, $pictureimage);
2672
    } else {
2673
        $output .= $pictureimage;
2674
    }
2675
 
2676
    if ($return) {
2677
        return $output;
2678
    } else {
2679
        echo $output;
2680
    }
2681
}
2682
 
2683
/**
2684
 * Return the url to the group picture.
2685
 *
2686
 * @param  stdClass $group A group object.
2687
 * @param  int $courseid The course ID for the group.
2688
 * @param  bool $large A large or small group picture? Default is small.
2689
 * @param  boolean $includetoken Whether to use a user token when displaying this group image.
2690
 *                 True indicates to generate a token for current user, and integer value indicates to generate a token for the
2691
 *                 user whose id is the value indicated.
2692
 *                 If the group picture is included in an e-mail or some other location where the audience is a specific
2693
 *                 user who will not be logged in when viewing, then we use a token to authenticate the user.
2694
 * @return ?moodle_url Returns the url for the group picture.
2695
 */
2696
function get_group_picture_url($group, $courseid, $large = false, $includetoken = false) {
2697
    global $CFG;
2698
 
2699
    $context = context_course::instance($courseid);
2700
 
2701
    // If there is no picture, do nothing.
2702
    if (!$group->picture) {
2703
        return;
2704
    }
2705
 
2706
    if ($large) {
2707
        $file = 'f1';
2708
    } else {
2709
        $file = 'f2';
2710
    }
2711
 
2712
    $grouppictureurl = moodle_url::make_pluginfile_url(
2713
            $context->id, 'group', 'icon', $group->id, '/', $file, false, $includetoken);
2714
    $grouppictureurl->param('rev', $group->picture);
2715
    return $grouppictureurl;
2716
}
2717
 
2718
 
2719
/**
2720
 * Display a recent activity note
2721
 *
2722
 * @staticvar string $strftimerecent
2723
 * @param int $time A timestamp int.
2724
 * @param stdClass $user A user object from the database.
2725
 * @param string $text Text for display for the note
2726
 * @param string $link The link to wrap around the text
2727
 * @param bool $return If set to true the HTML is returned rather than echo'd
2728
 * @param string $viewfullnames
2729
 * @return ?string If $retrun was true returns HTML for a recent activity notice.
2730
 */
2731
function print_recent_activity_note($time, $user, $text, $link, $return=false, $viewfullnames=null) {
2732
    static $strftimerecent = null;
2733
    $output = '';
2734
 
2735
    if (is_null($viewfullnames)) {
2736
        $context = context_system::instance();
2737
        $viewfullnames = has_capability('moodle/site:viewfullnames', $context);
2738
    }
2739
 
2740
    if (is_null($strftimerecent)) {
2741
        $strftimerecent = get_string('strftimerecent');
2742
    }
2743
 
2744
    $output .= '<div class="head">';
2745
    $output .= '<div class="date">'.userdate($time, $strftimerecent).'</div>';
2746
    $output .= '<div class="name">'.fullname($user, $viewfullnames).'</div>';
2747
    $output .= '</div>';
2748
    $output .= '<div class="info"><a href="'.$link.'">'.format_string($text, true).'</a></div>';
2749
 
2750
    if ($return) {
2751
        return $output;
2752
    } else {
2753
        echo $output;
2754
    }
2755
}
2756
 
2757
/**
2758
 * Returns a popup menu with course activity modules
2759
 *
2760
 * Given a course this function returns a small popup menu with all the course activity modules in it, as a navigation menu
2761
 * outputs a simple list structure in XHTML.
2762
 * The data is taken from the serialised array stored in the course record.
2763
 *
2764
 * @param stdClass $course A course object.
2765
 * @param array $sections
2766
 * @param course_modinfo $modinfo
2767
 * @param string $strsection
2768
 * @param string $strjumpto
2769
 * @param int $width
2770
 * @param string $cmid
2771
 * @return string The HTML block
2772
 */
2773
function navmenulist($course, $sections, $modinfo, $strsection, $strjumpto, $width=50, $cmid=0) {
2774
 
2775
    global $CFG, $OUTPUT;
2776
 
2777
    $section = -1;
2778
    $menu = array();
2779
    $doneheading = false;
2780
 
2781
    $courseformatoptions = course_get_format($course)->get_format_options();
2782
    $coursecontext = context_course::instance($course->id);
2783
 
2784
    $menu[] = '<ul class="navmenulist"><li class="jumpto section"><span>'.$strjumpto.'</span><ul>';
2785
    foreach ($modinfo->cms as $mod) {
2786
        if (!$mod->has_view()) {
2787
            // Don't show modules which you can't link to!
2788
            continue;
2789
        }
2790
 
2791
        // For course formats using 'numsections' do not show extra sections.
2792
        if (isset($courseformatoptions['numsections']) && $mod->sectionnum > $courseformatoptions['numsections']) {
2793
            break;
2794
        }
2795
 
2796
        if (!$mod->uservisible) { // Do not icnlude empty sections at all.
2797
            continue;
2798
        }
2799
 
2800
        if ($mod->sectionnum >= 0 and $section != $mod->sectionnum) {
2801
            $thissection = $sections[$mod->sectionnum];
2802
 
2803
            if ($thissection->visible or
2804
                    (isset($courseformatoptions['hiddensections']) and !$courseformatoptions['hiddensections']) or
2805
                    has_capability('moodle/course:viewhiddensections', $coursecontext)) {
2806
                $thissection->summary = strip_tags(format_string($thissection->summary, true));
2807
                if (!$doneheading) {
2808
                    $menu[] = '</ul></li>';
2809
                }
2810
                if ($course->format == 'weeks' or empty($thissection->summary)) {
2811
                    $item = $strsection ." ". $mod->sectionnum;
2812
                } else {
2813
                    if (core_text::strlen($thissection->summary) < ($width-3)) {
2814
                        $item = $thissection->summary;
2815
                    } else {
2816
                        $item = core_text::substr($thissection->summary, 0, $width).'...';
2817
                    }
2818
                }
2819
                $menu[] = '<li class="section"><span>'.$item.'</span>';
2820
                $menu[] = '<ul>';
2821
                $doneheading = true;
2822
 
2823
                $section = $mod->sectionnum;
2824
            } else {
2825
                // No activities from this hidden section shown.
2826
                continue;
2827
            }
2828
        }
2829
 
2830
        $url = $mod->modname .'/view.php?id='. $mod->id;
2831
        $mod->name = strip_tags(format_string($mod->name ,true));
2832
        if (core_text::strlen($mod->name) > ($width+5)) {
2833
            $mod->name = core_text::substr($mod->name, 0, $width).'...';
2834
        }
2835
        if (!$mod->visible) {
2836
            $mod->name = '('.$mod->name.')';
2837
        }
2838
        $class = 'activity '.$mod->modname;
2839
        $class .= ($cmid == $mod->id) ? ' selected' : '';
2840
        $menu[] = '<li class="'.$class.'">'.
2841
                  $OUTPUT->image_icon('monologo', '', $mod->modname).
2842
                  '<a href="'.$CFG->wwwroot.'/mod/'.$url.'">'.$mod->name.'</a></li>';
2843
    }
2844
 
2845
    if ($doneheading) {
2846
        $menu[] = '</ul></li>';
2847
    }
2848
    $menu[] = '</ul></li></ul>';
2849
 
2850
    return implode("\n", $menu);
2851
}
2852
 
2853
/**
2854
 * Prints a grade menu (as part of an existing form) with help showing all possible numerical grades and scales.
2855
 *
2856
 * @todo Finish documenting this function
2857
 * @todo Deprecate: this is only used in a few contrib modules
2858
 *
2859
 * @param int $courseid The course ID
2860
 * @param string $name
2861
 * @param string $current
2862
 * @param boolean $includenograde Include those with no grades
2863
 * @param boolean $return If set to true returns rather than echo's
2864
 * @return string|bool|null Depending on value of $return
2865
 */
2866
function print_grade_menu($courseid, $name, $current, $includenograde=true, $return=false) {
2867
    global $OUTPUT;
2868
 
2869
    $output = '';
2870
    $strscale = get_string('scale');
2871
    $strscales = get_string('scales');
2872
 
2873
    $scales = get_scales_menu($courseid);
2874
    foreach ($scales as $i => $scalename) {
2875
        $grades[-$i] = $strscale .': '. $scalename;
2876
    }
2877
    if ($includenograde) {
2878
        $grades[0] = get_string('nograde');
2879
    }
2880
    for ($i=100; $i>=1; $i--) {
2881
        $grades[$i] = $i;
2882
    }
2883
    $output .= html_writer::select($grades, $name, $current, false);
2884
 
2885
    $linkobject = '<span class="helplink">' . $OUTPUT->pix_icon('help', $strscales) . '</span>';
2886
    $link = new moodle_url('/course/scales.php', array('id' => $courseid, 'list' => 1));
2887
    $action = new popup_action('click', $link, 'ratingscales', array('height' => 400, 'width' => 500));
2888
    $output .= $OUTPUT->action_link($link, $linkobject, $action, array('title' => $strscales));
2889
 
2890
    if ($return) {
2891
        return $output;
2892
    } else {
2893
        echo $output;
2894
    }
2895
}
2896
 
2897
/**
2898
 * Print an error to STDOUT and exit with a non-zero code. For commandline scripts.
2899
 *
2900
 * Default errorcode is 1.
2901
 *
2902
 * Very useful for perl-like error-handling:
2903
 * do_somethting() or mdie("Something went wrong");
2904
 *
2905
 * @param string  $msg       Error message
2906
 * @param integer $errorcode Error code to emit
2907
 */
2908
function mdie($msg='', $errorcode=1) {
2909
    trigger_error($msg);
2910
    exit($errorcode);
2911
}
2912
 
2913
/**
2914
 * Print a message and exit.
2915
 *
2916
 * @param string $message The message to print in the notice
2917
 * @param moodle_url|string $link The link to use for the continue button
2918
 * @param object $course A course object. Unused.
2919
 * @return void This function simply exits
2920
 */
2921
function notice ($message, $link='', $course=null) {
2922
    global $PAGE, $OUTPUT;
2923
 
2924
    $message = clean_text($message);   // In case nasties are in here.
2925
 
2926
    if (CLI_SCRIPT) {
2927
        echo("!!$message!!\n");
2928
        exit(1); // No success.
2929
    }
2930
 
2931
    if (!$PAGE->headerprinted) {
2932
        // Header not yet printed.
2933
        $PAGE->set_title(get_string('notice'));
2934
        echo $OUTPUT->header();
2935
    } else {
2936
        echo $OUTPUT->container_end_all(false);
2937
    }
2938
 
2939
    echo $OUTPUT->box($message, 'generalbox', 'notice');
2940
    echo $OUTPUT->continue_button($link);
2941
 
2942
    echo $OUTPUT->footer();
2943
    exit(1); // General error code.
2944
}
2945
 
2946
/**
2947
 * Redirects the user to another page, after printing a notice.
2948
 *
2949
 * This function calls the OUTPUT redirect method, echo's the output and then dies to ensure nothing else happens.
2950
 *
2951
 * <strong>Good practice:</strong> You should call this method before starting page
2952
 * output by using any of the OUTPUT methods.
2953
 *
2954
 * @param moodle_url|string $url A moodle_url to redirect to. Strings are not to be trusted!
2955
 * @param string $message The message to display to the user
2956
 * @param int $delay The delay before redirecting
2957
 * @param string $messagetype The type of notification to show the message in. See constants on \core\output\notification.
2958
 * @throws moodle_exception
2959
 */
2960
function redirect($url, $message='', $delay=null, $messagetype = \core\output\notification::NOTIFY_INFO) {
2961
    global $OUTPUT, $PAGE, $CFG;
2962
 
2963
    if (CLI_SCRIPT or AJAX_SCRIPT) {
2964
        // This is wrong - developers should not use redirect in these scripts but it should not be very likely.
2965
        throw new moodle_exception('redirecterrordetected', 'error');
2966
    }
2967
 
2968
    if ($delay === null) {
2969
        $delay = -1;
2970
    }
2971
 
2972
    // Prevent debug errors - make sure context is properly initialised.
2973
    if ($PAGE) {
2974
        $PAGE->set_context(null);
2975
        $PAGE->set_pagelayout('redirect');  // No header and footer needed.
2976
        $PAGE->set_title(get_string('pageshouldredirect', 'moodle'));
2977
    }
2978
 
2979
    if ($url instanceof moodle_url) {
2980
        $url = $url->out(false);
2981
    }
2982
 
2983
    $debugdisableredirect = false;
2984
    do {
2985
        if (defined('DEBUGGING_PRINTED')) {
2986
            // Some debugging already printed, no need to look more.
2987
            $debugdisableredirect = true;
2988
            break;
2989
        }
2990
 
2991
        if (core_useragent::is_msword()) {
2992
            // Clicking a URL from MS Word sends a request to the server without cookies. If that
2993
            // causes a redirect Word will open a browser pointing the new URL. If not, the URL that
2994
            // was clicked is opened. Because the request from Word is without cookies, it almost
2995
            // always results in a redirect to the login page, even if the user is logged in in their
2996
            // browser. This is not what we want, so prevent the redirect for requests from Word.
2997
            $debugdisableredirect = true;
2998
            break;
2999
        }
3000
 
3001
        if (empty($CFG->debugdisplay) or empty($CFG->debug)) {
3002
            // No errors should be displayed.
3003
            break;
3004
        }
3005
 
3006
        if (!function_exists('error_get_last') or !$lasterror = error_get_last()) {
3007
            break;
3008
        }
3009
 
3010
        if (!($lasterror['type'] & $CFG->debug)) {
3011
            // Last error not interesting.
3012
            break;
3013
        }
3014
 
3015
        // Watch out here, @hidden() errors are returned from error_get_last() too.
3016
        if (headers_sent()) {
3017
            // We already started printing something - that means errors likely printed.
3018
            $debugdisableredirect = true;
3019
            break;
3020
        }
3021
 
3022
        if (ob_get_level() and ob_get_contents()) {
3023
            // There is something waiting to be printed, hopefully it is the errors,
3024
            // but it might be some error hidden by @ too - such as the timezone mess from setup.php.
3025
            $debugdisableredirect = true;
3026
            break;
3027
        }
3028
    } while (false);
3029
 
3030
    // Technically, HTTP/1.1 requires Location: header to contain the absolute path.
3031
    // (In practice browsers accept relative paths - but still, might as well do it properly.)
3032
    // This code turns relative into absolute.
3033
    if (!preg_match('|^[a-z]+:|i', $url)) {
3034
        // Get host name http://www.wherever.com.
3035
        $hostpart = preg_replace('|^(.*?[^:/])/.*$|', '$1', $CFG->wwwroot);
3036
        if (preg_match('|^/|', $url)) {
3037
            // URLs beginning with / are relative to web server root so we just add them in.
3038
            $url = $hostpart.$url;
3039
        } else {
3040
            // URLs not beginning with / are relative to path of current script, so add that on.
3041
            $url = $hostpart.preg_replace('|\?.*$|', '', me()).'/../'.$url;
3042
        }
3043
        // Replace all ..s.
3044
        while (true) {
3045
            $newurl = preg_replace('|/(?!\.\.)[^/]*/\.\./|', '/', $url);
3046
            if ($newurl == $url) {
3047
                break;
3048
            }
3049
            $url = $newurl;
3050
        }
3051
    }
3052
 
3053
    // Sanitise url - we can not rely on moodle_url or our URL cleaning
3054
    // because they do not support all valid external URLs.
3055
    $url = preg_replace('/[\x00-\x1F\x7F]/', '', $url);
3056
    $url = str_replace('"', '%22', $url);
3057
    $encodedurl = preg_replace("/\&(?![a-zA-Z0-9#]{1,8};)/", "&amp;", $url);
3058
    $encodedurl = preg_replace('/^.*href="([^"]*)".*$/', "\\1", clean_text('<a href="'.$encodedurl.'" />', FORMAT_HTML));
3059
    $url = str_replace('&amp;', '&', $encodedurl);
3060
 
3061
    if (!empty($message)) {
3062
        if (!$debugdisableredirect && !headers_sent()) {
3063
            // A message has been provided, and the headers have not yet been sent.
3064
            // Display the message as a notification on the subsequent page.
3065
            \core\notification::add($message, $messagetype);
3066
            $message = null;
3067
            $delay = 0;
3068
        } else {
3069
            if ($delay === -1 || !is_numeric($delay)) {
3070
                $delay = 3;
3071
            }
3072
            $message = clean_text($message);
3073
        }
3074
    } else {
3075
        $message = get_string('pageshouldredirect');
3076
        $delay = 0;
3077
    }
3078
 
3079
    // Make sure the session is closed properly, this prevents problems in IIS
3080
    // and also some potential PHP shutdown issues.
3081
    \core\session\manager::write_close();
3082
 
3083
    if ($delay == 0 && !$debugdisableredirect && !headers_sent()) {
3084
 
3085
        // This helps when debugging redirect issues like loops and it is not clear
3086
        // which layer in the stack sent the redirect header. If debugging is on
3087
        // then the file and line is also shown.
3088
        $redirectby = 'Moodle';
3089
        if (debugging('', DEBUG_DEVELOPER)) {
3090
            $origin = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
3091
            $redirectby .= ' /' . str_replace($CFG->dirroot . '/', '', $origin['file']) . ':' . $origin['line'];
3092
        }
3093
        @header("X-Redirect-By: $redirectby");
3094
 
3095
        // 302 might not work for POST requests, 303 is ignored by obsolete clients.
3096
        @header($_SERVER['SERVER_PROTOCOL'] . ' 303 See Other');
3097
        @header('Location: '.$url);
3098
        echo bootstrap_renderer::plain_redirect_message($encodedurl);
3099
        exit;
3100
    }
3101
 
3102
    // Include a redirect message, even with a HTTP redirect, because that is recommended practice.
3103
    if ($PAGE) {
3104
        $CFG->docroot = false; // To prevent the link to moodle docs from being displayed on redirect page.
3105
        echo $OUTPUT->redirect_message($encodedurl, $message, $delay, $debugdisableredirect, $messagetype);
3106
        exit;
3107
    } else {
3108
        echo bootstrap_renderer::early_redirect_message($encodedurl, $message, $delay);
3109
        exit;
3110
    }
3111
}
3112
 
3113
/**
3114
 * Given an email address, this function will return an obfuscated version of it.
3115
 *
3116
 * @param string $email The email address to obfuscate
3117
 * @return string The obfuscated email address
3118
 */
3119
function obfuscate_email($email) {
3120
    $i = 0;
3121
    $length = strlen($email);
3122
    $obfuscated = '';
3123
    while ($i < $length) {
3124
        if (rand(0, 2) && $email[$i]!='@') { // MDL-20619 some browsers have problems unobfuscating @.
3125
            $obfuscated.='%'.dechex(ord($email[$i]));
3126
        } else {
3127
            $obfuscated.=$email[$i];
3128
        }
3129
        $i++;
3130
    }
3131
    return $obfuscated;
3132
}
3133
 
3134
/**
3135
 * This function takes some text and replaces about half of the characters
3136
 * with HTML entity equivalents.   Return string is obviously longer.
3137
 *
3138
 * @param string $plaintext The text to be obfuscated
3139
 * @return string The obfuscated text
3140
 */
3141
function obfuscate_text($plaintext) {
3142
    $i=0;
3143
    $length = core_text::strlen($plaintext);
3144
    $obfuscated='';
3145
    $prevobfuscated = false;
3146
    while ($i < $length) {
3147
        $char = core_text::substr($plaintext, $i, 1);
3148
        $ord = core_text::utf8ord($char);
3149
        $numerical = ($ord >= ord('0')) && ($ord <= ord('9'));
3150
        if ($prevobfuscated and $numerical ) {
3151
            $obfuscated.='&#'.$ord.';';
3152
        } else if (rand(0, 2)) {
3153
            $obfuscated.='&#'.$ord.';';
3154
            $prevobfuscated = true;
3155
        } else {
3156
            $obfuscated.=$char;
3157
            $prevobfuscated = false;
3158
        }
3159
        $i++;
3160
    }
3161
    return $obfuscated;
3162
}
3163
 
3164
/**
3165
 * This function uses the {@link obfuscate_email()} and {@link obfuscate_text()}
3166
 * to generate a fully obfuscated email link, ready to use.
3167
 *
3168
 * @param string $email The email address to display
3169
 * @param string $label The text to displayed as hyperlink to $email
3170
 * @param boolean $dimmed If true then use css class 'dimmed' for hyperlink
3171
 * @param string $subject The subject of the email in the mailto link
3172
 * @param string $body The content of the email in the mailto link
3173
 * @return string The obfuscated mailto link
3174
 */
3175
function obfuscate_mailto($email, $label='', $dimmed=false, $subject = '', $body = '') {
3176
 
3177
    if (empty($label)) {
3178
        $label = $email;
3179
    }
3180
 
3181
    $label = obfuscate_text($label);
3182
    $email = obfuscate_email($email);
3183
    $mailto = obfuscate_text('mailto');
3184
    $url = new moodle_url("mailto:$email");
3185
    $attrs = array();
3186
 
3187
    if (!empty($subject)) {
3188
        $url->param('subject', format_string($subject));
3189
    }
3190
    if (!empty($body)) {
3191
        $url->param('body', format_string($body));
3192
    }
3193
 
3194
    // Use the obfuscated mailto.
3195
    $url = preg_replace('/^mailto/', $mailto, $url->out());
3196
 
3197
    if ($dimmed) {
3198
        $attrs['title'] = get_string('emaildisable');
3199
        $attrs['class'] = 'dimmed';
3200
    }
3201
 
3202
    return html_writer::link($url, $label, $attrs);
3203
}
3204
 
3205
/**
3206
 * This function is used to rebuild the <nolink> tag because some formats (PLAIN and WIKI)
3207
 * will transform it to html entities
3208
 *
3209
 * @param string $text Text to search for nolink tag in
3210
 * @return string
3211
 */
3212
function rebuildnolinktag($text) {
3213
 
3214
    $text = preg_replace('/&lt;(\/*nolink)&gt;/i', '<$1>', $text);
3215
 
3216
    return $text;
3217
}
3218
 
3219
/**
3220
 * Prints a maintenance message from $CFG->maintenance_message or default if empty.
3221
 */
3222
function print_maintenance_message() {
3223
    global $CFG, $SITE, $PAGE, $OUTPUT;
3224
 
3225
    header($_SERVER['SERVER_PROTOCOL'] . ' 503 Moodle under maintenance');
3226
    header('Status: 503 Moodle under maintenance');
3227
    header('Retry-After: 300');
3228
 
3229
    $PAGE->set_pagetype('maintenance-message');
3230
    $PAGE->set_pagelayout('maintenance');
3231
    $PAGE->set_heading($SITE->fullname);
3232
    echo $OUTPUT->header();
3233
    echo $OUTPUT->heading(get_string('sitemaintenance', 'admin'));
3234
    if (isset($CFG->maintenance_message) and !html_is_blank($CFG->maintenance_message)) {
3235
        echo $OUTPUT->box_start('maintenance_message generalbox boxwidthwide boxaligncenter');
3236
        echo $CFG->maintenance_message;
3237
        echo $OUTPUT->box_end();
3238
    }
3239
    echo $OUTPUT->footer();
3240
    die;
3241
}
3242
 
3243
/**
3244
 * Returns a string containing a nested list, suitable for formatting into tabs with CSS.
3245
 *
3246
 * It is not recommended to use this function in Moodle 2.5 but it is left for backward
3247
 * compartibility.
3248
 *
3249
 * Example how to print a single line tabs:
3250
 * $rows = array(
3251
 *    new tabobject(...),
3252
 *    new tabobject(...)
3253
 * );
3254
 * echo $OUTPUT->tabtree($rows, $selectedid);
3255
 *
3256
 * Multiple row tabs may not look good on some devices but if you want to use them
3257
 * you can specify ->subtree for the active tabobject.
3258
 *
3259
 * @param array $tabrows An array of rows where each row is an array of tab objects
3260
 * @param string $selected  The id of the selected tab (whatever row it's on)
3261
 * @param array  $inactive  An array of ids of inactive tabs that are not selectable.
3262
 * @param array  $activated An array of ids of other tabs that are currently activated
3263
 * @param bool $return If true output is returned rather then echo'd
3264
 * @return string HTML output if $return was set to true.
3265
 */
3266
function print_tabs($tabrows, $selected = null, $inactive = null, $activated = null, $return = false) {
3267
    global $OUTPUT;
3268
 
3269
    $tabrows = array_reverse($tabrows);
3270
    $subtree = array();
3271
    foreach ($tabrows as $row) {
3272
        $tree = array();
3273
 
3274
        foreach ($row as $tab) {
3275
            $tab->inactive = is_array($inactive) && in_array((string)$tab->id, $inactive);
3276
            $tab->activated = is_array($activated) && in_array((string)$tab->id, $activated);
3277
            $tab->selected = (string)$tab->id == $selected;
3278
 
3279
            if ($tab->activated || $tab->selected) {
3280
                $tab->subtree = $subtree;
3281
            }
3282
            $tree[] = $tab;
3283
        }
3284
        $subtree = $tree;
3285
    }
3286
    $output = $OUTPUT->tabtree($subtree);
3287
    if ($return) {
3288
        return $output;
3289
    } else {
3290
        print $output;
3291
        return !empty($output);
3292
    }
3293
}
3294
 
3295
/**
3296
 * Alter debugging level for the current request,
3297
 * the change is not saved in database.
3298
 *
3299
 * @param int $level one of the DEBUG_* constants
3300
 * @param bool $debugdisplay
3301
 */
3302
function set_debugging($level, $debugdisplay = null) {
3303
    global $CFG;
3304
 
3305
    $CFG->debug = (int)$level;
3306
    $CFG->debugdeveloper = (($CFG->debug & DEBUG_DEVELOPER) === DEBUG_DEVELOPER);
3307
 
3308
    if ($debugdisplay !== null) {
3309
        $CFG->debugdisplay = (bool)$debugdisplay;
3310
    }
3311
}
3312
 
3313
/**
3314
 * Standard Debugging Function
3315
 *
3316
 * Returns true if the current site debugging settings are equal or above specified level.
3317
 * If passed a parameter it will emit a debugging notice similar to trigger_error(). The
3318
 * routing of notices is controlled by $CFG->debugdisplay
3319
 * eg use like this:
3320
 *
3321
 * 1)  debugging('a normal debug notice');
3322
 * 2)  debugging('something really picky', DEBUG_ALL);
3323
 * 3)  debugging('annoying debug message only for developers', DEBUG_DEVELOPER);
3324
 * 4)  if (debugging()) { perform extra debugging operations (do not use print or echo) }
3325
 *
3326
 * In code blocks controlled by debugging() (such as example 4)
3327
 * any output should be routed via debugging() itself, or the lower-level
3328
 * trigger_error() or error_log(). Using echo or print will break XHTML
3329
 * JS and HTTP headers.
3330
 *
3331
 * It is also possible to define NO_DEBUG_DISPLAY which redirects the message to error_log.
3332
 *
3333
 * @param string $message a message to print
3334
 * @param int $level the level at which this debugging statement should show
3335
 * @param array $backtrace use different backtrace
3336
 * @return bool
3337
 */
3338
function debugging($message = '', $level = DEBUG_NORMAL, $backtrace = null) {
3339
    global $CFG, $USER;
3340
 
3341
    $forcedebug = false;
3342
    if (!empty($CFG->debugusers) && $USER) {
3343
        $debugusers = explode(',', $CFG->debugusers);
3344
        $forcedebug = in_array($USER->id, $debugusers);
3345
    }
3346
 
3347
    if (!$forcedebug and (empty($CFG->debug) || ($CFG->debug != -1 and $CFG->debug < $level))) {
3348
        return false;
3349
    }
3350
 
3351
    if (!isset($CFG->debugdisplay)) {
3352
        $CFG->debugdisplay = ini_get_bool('display_errors');
3353
    }
3354
 
3355
    if ($message) {
3356
        if (!$backtrace) {
3357
            $backtrace = debug_backtrace();
3358
        }
3359
        $from = format_backtrace($backtrace, CLI_SCRIPT || NO_DEBUG_DISPLAY);
3360
        if (PHPUNIT_TEST) {
3361
            if (phpunit_util::debugging_triggered($message, $level, $from)) {
3362
                // We are inside test, the debug message was logged.
3363
                return true;
3364
            }
3365
        }
3366
 
3367
        if (NO_DEBUG_DISPLAY) {
3368
            // Script does not want any errors or debugging in output,
3369
            // we send the info to error log instead.
3370
            error_log('Debugging: ' . $message . ' in '. PHP_EOL . $from);
3371
        } else if ($forcedebug or $CFG->debugdisplay) {
3372
            if (!defined('DEBUGGING_PRINTED')) {
3373
                define('DEBUGGING_PRINTED', 1); // Indicates we have printed something.
3374
            }
3375
 
3376
            if (CLI_SCRIPT) {
3377
                echo "++ $message ++\n$from";
3378
            } else {
3379
                if (property_exists($CFG, 'debug_developer_debugging_as_error')) {
3380
                    $showaserror = $CFG->debug_developer_debugging_as_error;
3381
                } else {
3382
                    $showaserror = (bool) get_whoops();
3383
                }
3384
 
3385
                if ($showaserror) {
3386
                    trigger_error($message, E_USER_NOTICE);
3387
                } else {
3388
                    echo '<div class="notifytiny debuggingmessage" data-rel="debugging">', $message, $from, '</div>';
3389
                }
3390
            }
3391
        } else {
3392
            trigger_error($message . $from, E_USER_NOTICE);
3393
        }
3394
    }
3395
    return true;
3396
}
3397
 
3398
/**
3399
 * Outputs a HTML comment to the browser.
3400
 *
3401
 * This is used for those hard-to-debug pages that use bits from many different files in very confusing ways (e.g. blocks).
3402
 *
3403
 * <code>print_location_comment(__FILE__, __LINE__);</code>
3404
 *
3405
 * @param string $file
3406
 * @param integer $line
3407
 * @param boolean $return Whether to return or print the comment
3408
 * @return string|void Void unless true given as third parameter
3409
 */
3410
function print_location_comment($file, $line, $return = false) {
3411
    if ($return) {
3412
        return "<!-- $file at line $line -->\n";
3413
    } else {
3414
        echo "<!-- $file at line $line -->\n";
3415
    }
3416
}
3417
 
3418
 
3419
/**
3420
 * Returns true if the user is using a right-to-left language.
3421
 *
3422
 * @return boolean true if the current language is right-to-left (Hebrew, Arabic etc)
3423
 */
3424
function right_to_left() {
3425
    return (get_string('thisdirection', 'langconfig') === 'rtl');
3426
}
3427
 
3428
 
3429
/**
3430
 * Returns swapped left<=> right if in RTL environment.
3431
 *
3432
 * Part of RTL Moodles support.
3433
 *
3434
 * @param string $align align to check
3435
 * @return string
3436
 */
3437
function fix_align_rtl($align) {
3438
    if (!right_to_left()) {
3439
        return $align;
3440
    }
3441
    if ($align == 'left') {
3442
        return 'right';
3443
    }
3444
    if ($align == 'right') {
3445
        return 'left';
3446
    }
3447
    return $align;
3448
}
3449
 
3450
 
3451
/**
3452
 * Returns true if the page is displayed in a popup window.
3453
 *
3454
 * Gets the information from the URL parameter inpopup.
3455
 *
3456
 * @todo Use a central function to create the popup calls all over Moodle and
3457
 * In the moment only works with resources and probably questions.
3458
 *
3459
 * @return boolean
3460
 */
3461
function is_in_popup() {
3462
    $inpopup = optional_param('inpopup', '', PARAM_BOOL);
3463
 
3464
    return ($inpopup);
3465
}
3466
 
3467
/**
3468
 * Progress trace class.
3469
 *
3470
 * Use this class from long operations where you want to output occasional information about
3471
 * what is going on, but don't know if, or in what format, the output should be.
3472
 *
3473
 * @copyright 2009 Tim Hunt
3474
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3475
 * @package core
3476
 */
3477
abstract class progress_trace {
3478
    /**
3479
     * Output an progress message in whatever format.
3480
     *
3481
     * @param string $message the message to output.
3482
     * @param integer $depth indent depth for this message.
3483
     */
3484
    abstract public function output($message, $depth = 0);
3485
 
3486
    /**
3487
     * Called when the processing is finished.
3488
     */
3489
    public function finished() {
3490
    }
3491
}
3492
 
3493
/**
3494
 * This subclass of progress_trace does not ouput anything.
3495
 *
3496
 * @copyright 2009 Tim Hunt
3497
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3498
 * @package core
3499
 */
3500
class null_progress_trace extends progress_trace {
3501
    /**
3502
     * Does Nothing
3503
     *
3504
     * @param string $message
3505
     * @param int $depth
3506
     * @return void Does Nothing
3507
     */
3508
    public function output($message, $depth = 0) {
3509
    }
3510
}
3511
 
3512
/**
3513
 * This subclass of progress_trace outputs to plain text.
3514
 *
3515
 * @copyright 2009 Tim Hunt
3516
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3517
 * @package core
3518
 */
3519
class text_progress_trace extends progress_trace {
3520
    /**
3521
     * Output the trace message.
3522
     *
3523
     * @param string $message
3524
     * @param int $depth
3525
     * @return void Output is echo'd
3526
     */
3527
    public function output($message, $depth = 0) {
3528
        mtrace(str_repeat('  ', $depth) . $message);
3529
    }
3530
}
3531
 
3532
/**
3533
 * This subclass of progress_trace outputs as HTML.
3534
 *
3535
 * @copyright 2009 Tim Hunt
3536
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3537
 * @package core
3538
 */
3539
class html_progress_trace extends progress_trace {
3540
    /**
3541
     * Output the trace message.
3542
     *
3543
     * @param string $message
3544
     * @param int $depth
3545
     * @return void Output is echo'd
3546
     */
3547
    public function output($message, $depth = 0) {
3548
        echo '<p>', str_repeat('&#160;&#160;', $depth), htmlspecialchars($message, ENT_COMPAT), "</p>\n";
3549
        flush();
3550
    }
3551
}
3552
 
3553
/**
3554
 * HTML List Progress Tree
3555
 *
3556
 * @copyright 2009 Tim Hunt
3557
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3558
 * @package core
3559
 */
3560
class html_list_progress_trace extends progress_trace {
3561
    /** @var int */
3562
    protected $currentdepth = -1;
3563
 
3564
    /**
3565
     * Echo out the list
3566
     *
3567
     * @param string $message The message to display
3568
     * @param int $depth
3569
     * @return void Output is echoed
3570
     */
3571
    public function output($message, $depth = 0) {
3572
        $samedepth = true;
3573
        while ($this->currentdepth > $depth) {
3574
            echo "</li>\n</ul>\n";
3575
            $this->currentdepth -= 1;
3576
            if ($this->currentdepth == $depth) {
3577
                echo '<li>';
3578
            }
3579
            $samedepth = false;
3580
        }
3581
        while ($this->currentdepth < $depth) {
3582
            echo "<ul>\n<li>";
3583
            $this->currentdepth += 1;
3584
            $samedepth = false;
3585
        }
3586
        if ($samedepth) {
3587
            echo "</li>\n<li>";
3588
        }
3589
        echo htmlspecialchars($message, ENT_COMPAT);
3590
        flush();
3591
    }
3592
 
3593
    /**
3594
     * Called when the processing is finished.
3595
     */
3596
    public function finished() {
3597
        while ($this->currentdepth >= 0) {
3598
            echo "</li>\n</ul>\n";
3599
            $this->currentdepth -= 1;
3600
        }
3601
    }
3602
}
3603
 
3604
/**
3605
 * This subclass of progress_trace outputs to error log.
3606
 *
3607
 * @copyright Petr Skoda {@link http://skodak.org}
3608
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3609
 * @package core
3610
 */
3611
class error_log_progress_trace extends progress_trace {
3612
    /** @var string log prefix */
3613
    protected $prefix;
3614
 
3615
    /**
3616
     * Constructor.
3617
     * @param string $prefix optional log prefix
3618
     */
3619
    public function __construct($prefix = '') {
3620
        $this->prefix = $prefix;
3621
    }
3622
 
3623
    /**
3624
     * Output the trace message.
3625
     *
3626
     * @param string $message
3627
     * @param int $depth
3628
     * @return void Output is sent to error log.
3629
     */
3630
    public function output($message, $depth = 0) {
3631
        error_log($this->prefix . str_repeat('  ', $depth) . $message);
3632
    }
3633
}
3634
 
3635
/**
3636
 * Special type of trace that can be used for catching of output of other traces.
3637
 *
3638
 * @copyright Petr Skoda {@link http://skodak.org}
3639
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3640
 * @package core
3641
 */
3642
class progress_trace_buffer extends progress_trace {
3643
    /** @var progress_trace */
3644
    protected $trace;
3645
    /** @var bool do we pass output out */
3646
    protected $passthrough;
3647
    /** @var string output buffer */
3648
    protected $buffer;
3649
 
3650
    /**
3651
     * Constructor.
3652
     *
3653
     * @param progress_trace $trace
3654
     * @param bool $passthrough true means output and buffer, false means just buffer and no output
3655
     */
3656
    public function __construct(progress_trace $trace, $passthrough = true) {
3657
        $this->trace       = $trace;
3658
        $this->passthrough = $passthrough;
3659
        $this->buffer      = '';
3660
    }
3661
 
3662
    /**
3663
     * Output the trace message.
3664
     *
3665
     * @param string $message the message to output.
3666
     * @param int $depth indent depth for this message.
3667
     * @return void output stored in buffer
3668
     */
3669
    public function output($message, $depth = 0) {
3670
        ob_start();
3671
        $this->trace->output($message, $depth);
3672
        $this->buffer .= ob_get_contents();
3673
        if ($this->passthrough) {
3674
            ob_end_flush();
3675
        } else {
3676
            ob_end_clean();
3677
        }
3678
    }
3679
 
3680
    /**
3681
     * Called when the processing is finished.
3682
     */
3683
    public function finished() {
3684
        ob_start();
3685
        $this->trace->finished();
3686
        $this->buffer .= ob_get_contents();
3687
        if ($this->passthrough) {
3688
            ob_end_flush();
3689
        } else {
3690
            ob_end_clean();
3691
        }
3692
    }
3693
 
3694
    /**
3695
     * Reset internal text buffer.
3696
     */
3697
    public function reset_buffer() {
3698
        $this->buffer = '';
3699
    }
3700
 
3701
    /**
3702
     * Return internal text buffer.
3703
     * @return string buffered plain text
3704
     */
3705
    public function get_buffer() {
3706
        return $this->buffer;
3707
    }
3708
}
3709
 
3710
/**
3711
 * Special type of trace that can be used for redirecting to multiple other traces.
3712
 *
3713
 * @copyright Petr Skoda {@link http://skodak.org}
3714
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3715
 * @package core
3716
 */
3717
class combined_progress_trace extends progress_trace {
3718
 
3719
    /**
3720
     * An array of traces.
3721
     * @var array
3722
     */
3723
    protected $traces;
3724
 
3725
    /**
3726
     * Constructs a new instance.
3727
     *
3728
     * @param array $traces multiple traces
3729
     */
3730
    public function __construct(array $traces) {
3731
        $this->traces = $traces;
3732
    }
3733
 
3734
    /**
3735
     * Output an progress message in whatever format.
3736
     *
3737
     * @param string $message the message to output.
3738
     * @param integer $depth indent depth for this message.
3739
     */
3740
    public function output($message, $depth = 0) {
3741
        foreach ($this->traces as $trace) {
3742
            $trace->output($message, $depth);
3743
        }
3744
    }
3745
 
3746
    /**
3747
     * Called when the processing is finished.
3748
     */
3749
    public function finished() {
3750
        foreach ($this->traces as $trace) {
3751
            $trace->finished();
3752
        }
3753
    }
3754
}
3755
 
3756
/**
3757
 * Returns a localized sentence in the current language summarizing the current password policy
3758
 *
3759
 * @todo this should be handled by a function/method in the language pack library once we have a support for it
3760
 * @uses $CFG
3761
 * @return string
3762
 */
3763
function print_password_policy() {
3764
    global $CFG;
3765
 
3766
    $message = '';
3767
    if (!empty($CFG->passwordpolicy)) {
3768
        $messages = array();
3769
        if (!empty($CFG->minpasswordlength)) {
3770
            $messages[] = get_string('informminpasswordlength', 'auth', $CFG->minpasswordlength);
3771
        }
3772
        if (!empty($CFG->minpassworddigits)) {
3773
            $messages[] = get_string('informminpassworddigits', 'auth', $CFG->minpassworddigits);
3774
        }
3775
        if (!empty($CFG->minpasswordlower)) {
3776
            $messages[] = get_string('informminpasswordlower', 'auth', $CFG->minpasswordlower);
3777
        }
3778
        if (!empty($CFG->minpasswordupper)) {
3779
            $messages[] = get_string('informminpasswordupper', 'auth', $CFG->minpasswordupper);
3780
        }
3781
        if (!empty($CFG->minpasswordnonalphanum)) {
3782
            $messages[] = get_string('informminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum);
3783
        }
3784
 
3785
        // Fire any additional password policy functions from plugins.
3786
        // Callbacks must return an array of message strings.
3787
        $pluginsfunction = get_plugins_with_function('print_password_policy');
3788
        foreach ($pluginsfunction as $plugintype => $plugins) {
3789
            foreach ($plugins as $pluginfunction) {
3790
                $messages = array_merge($messages, $pluginfunction());
3791
            }
3792
        }
3793
 
3794
        $messages = join(', ', $messages); // This is ugly but we do not have anything better yet...
3795
        // Check if messages is empty before outputting any text.
3796
        if ($messages != '') {
3797
            $message = get_string('informpasswordpolicy', 'auth', $messages);
3798
        }
3799
    }
3800
    return $message;
3801
}
3802
 
3803
/**
3804
 * Get the value of a help string fully prepared for display in the current language.
3805
 *
3806
 * @param string $identifier The identifier of the string to search for.
3807
 * @param string $component The module the string is associated with.
3808
 * @param boolean $ajax Whether this help is called from an AJAX script.
3809
 *                This is used to influence text formatting and determines
3810
 *                which format to output the doclink in.
3811
 * @param string|object|array $a An object, string or number that can be used
3812
 *      within translation strings
3813
 * @return stdClass An object containing:
3814
 * - heading: Any heading that there may be for this help string.
3815
 * - text: The wiki-formatted help string.
3816
 * - doclink: An object containing a link, the linktext, and any additional
3817
 *            CSS classes to apply to that link. Only present if $ajax = false.
3818
 * - completedoclink: A text representation of the doclink. Only present if $ajax = true.
3819
 */
3820
function get_formatted_help_string($identifier, $component, $ajax = false, $a = null) {
3821
    global $CFG, $OUTPUT;
3822
    $sm = get_string_manager();
3823
 
3824
    // Do not rebuild caches here!
3825
    // Devs need to learn to purge all caches after any change or disable $CFG->langstringcache.
3826
 
3827
    $data = new stdClass();
3828
 
3829
    if ($sm->string_exists($identifier, $component)) {
3830
        $data->heading = format_string(get_string($identifier, $component));
3831
    } else {
3832
        // Gracefully fall back to an empty string.
3833
        $data->heading = '';
3834
    }
3835
 
3836
    if ($sm->string_exists($identifier . '_help', $component)) {
3837
        $options = new stdClass();
3838
        $options->trusted = false;
3839
        $options->noclean = false;
3840
        $options->filter = false;
3841
        $options->para = true;
3842
        $options->newlines = false;
3843
        $options->overflowdiv = !$ajax;
3844
 
3845
        // Should be simple wiki only MDL-21695.
3846
        $data->text = format_text(get_string($identifier.'_help', $component, $a), FORMAT_MARKDOWN, $options);
3847
 
3848
        $helplink = $identifier . '_link';
3849
        if ($sm->string_exists($helplink, $component)) {  // Link to further info in Moodle docs.
3850
            $link = get_string($helplink, $component);
3851
            $linktext = get_string('morehelp');
3852
 
3853
            $data->doclink = new stdClass();
3854
            $url = new moodle_url(get_docs_url($link));
3855
            if ($ajax) {
3856
                $data->doclink->link = $url->out();
3857
                $data->doclink->linktext = $linktext;
3858
                $data->doclink->class = ($CFG->doctonewwindow) ? 'helplinkpopup' : '';
3859
            } else {
3860
                $data->completedoclink = html_writer::tag('div', $OUTPUT->doc_link($link, $linktext),
3861
                    array('class' => 'helpdoclink'));
3862
            }
3863
        }
3864
    } else {
3865
        $data->text = html_writer::tag('p',
3866
            html_writer::tag('strong', 'TODO') . ": missing help string [{$identifier}_help, {$component}]");
3867
    }
3868
    return $data;
3869
}