Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | 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
defined('MOODLE_INTERNAL') || die();
34
 
35
// Constants.
36
 
37
// Define text formatting types ... eventually we can add Wiki, BBcode etc.
38
 
39
/**
40
 * Does all sorts of transformations and filtering.
41
 */
42
define('FORMAT_MOODLE',   '0');
43
 
44
/**
45
 * Plain HTML (with some tags stripped).
46
 */
47
define('FORMAT_HTML',     '1');
48
 
49
/**
50
 * Plain text (even tags are printed in full).
51
 */
52
define('FORMAT_PLAIN',    '2');
53
 
54
/**
55
 * Wiki-formatted text.
56
 * Deprecated: left here just to note that '3' is not used (at the moment)
57
 * and to catch any latent wiki-like text (which generates an error)
58
 * @deprecated since 2005!
59
 */
60
define('FORMAT_WIKI',     '3');
61
 
62
/**
63
 * Markdown-formatted text http://daringfireball.net/projects/markdown/
64
 */
65
define('FORMAT_MARKDOWN', '4');
66
 
67
/**
68
 * A moodle_url comparison using this flag will return true if the base URLs match, params are ignored.
69
 */
70
define('URL_MATCH_BASE', 0);
71
 
72
/**
73
 * A moodle_url comparison using this flag will return true if the base URLs match and the params of url1 are part of url2.
74
 */
75
define('URL_MATCH_PARAMS', 1);
76
 
77
/**
78
 * A moodle_url comparison using this flag will return true if the two URLs are identical, except for the order of the params.
79
 */
80
define('URL_MATCH_EXACT', 2);
81
 
82
// Functions.
83
 
84
/**
85
 * Add quotes to HTML characters.
86
 *
87
 * Returns $var with HTML characters (like "<", ">", etc.) properly quoted.
88
 * Related function {@link p()} simply prints the output of this function.
89
 *
90
 * @param string $var the string potentially containing HTML characters
91
 * @return string
92
 */
93
function s($var) {
94
    if ($var === false) {
95
        return '0';
96
    }
97
 
98
    if ($var === null || $var === '') {
99
        return '';
100
    }
101
 
102
    return preg_replace(
103
        '/&amp;#(\d+|x[0-9a-f]+);/i', '&#$1;',
104
        htmlspecialchars($var, ENT_QUOTES | ENT_HTML401 | ENT_SUBSTITUTE)
105
    );
106
}
107
 
108
/**
109
 * Add quotes to HTML characters.
110
 *
111
 * Prints $var with HTML characters (like "<", ">", etc.) properly quoted.
112
 * This function simply calls & displays {@link s()}.
113
 * @see s()
114
 *
115
 * @param string $var the string potentially containing HTML characters
116
 */
117
function p($var) {
118
    echo s($var);
119
}
120
 
121
/**
122
 * Does proper javascript quoting.
123
 *
124
 * Do not use addslashes anymore, because it does not work when magic_quotes_sybase is enabled.
125
 *
126
 * @param mixed $var String, Array, or Object to add slashes to
127
 * @return mixed quoted result
128
 */
129
function addslashes_js($var) {
130
    if (is_string($var)) {
131
        $var = str_replace('\\', '\\\\', $var);
132
        $var = str_replace(array('\'', '"', "\n", "\r", "\0"), array('\\\'', '\\"', '\\n', '\\r', '\\0'), $var);
133
        $var = str_replace('</', '<\/', $var);   // XHTML compliance.
134
    } else if (is_array($var)) {
135
        $var = array_map('addslashes_js', $var);
136
    } else if (is_object($var)) {
137
        $a = get_object_vars($var);
138
        foreach ($a as $key => $value) {
139
            $a[$key] = addslashes_js($value);
140
        }
141
        $var = (object)$a;
142
    }
143
    return $var;
144
}
145
 
146
/**
147
 * Remove query string from url.
148
 *
149
 * Takes in a URL and returns it without the querystring portion.
150
 *
151
 * @param string $url the url which may have a query string attached.
152
 * @return string The remaining URL.
153
 */
154
function strip_querystring($url) {
155
    if ($url === null || $url === '') {
156
        return '';
157
    }
158
 
159
    if ($commapos = strpos($url, '?')) {
160
        return substr($url, 0, $commapos);
161
    } else {
162
        return $url;
163
    }
164
}
165
 
166
/**
167
 * Returns the name of the current script, WITH the querystring portion.
168
 *
169
 * This function is necessary because PHP_SELF and REQUEST_URI and SCRIPT_NAME
170
 * return different things depending on a lot of things like your OS, Web
171
 * server, and the way PHP is compiled (ie. as a CGI, module, ISAPI, etc.)
172
 * <b>NOTE:</b> This function returns false if the global variables needed are not set.
173
 *
174
 * @return mixed String or false if the global variables needed are not set.
175
 */
176
function me() {
177
    global $ME;
178
    return $ME;
179
}
180
 
181
/**
182
 * Guesses the full URL of the current script.
183
 *
184
 * This function is using $PAGE->url, but may fall back to $FULLME which
185
 * is constructed from  PHP_SELF and REQUEST_URI or SCRIPT_NAME
186
 *
187
 * @return mixed full page URL string or false if unknown
188
 */
189
function qualified_me() {
190
    global $FULLME, $PAGE, $CFG;
191
 
192
    if (isset($PAGE) and $PAGE->has_set_url()) {
193
        // This is the only recommended way to find out current page.
194
        return $PAGE->url->out(false);
195
 
196
    } else {
197
        if ($FULLME === null) {
198
            // CLI script most probably.
199
            return false;
200
        }
201
        if (!empty($CFG->sslproxy)) {
202
            // Return only https links when using SSL proxy.
203
            return preg_replace('/^http:/', 'https:', $FULLME, 1);
204
        } else {
205
            return $FULLME;
206
        }
207
    }
208
}
209
 
210
/**
211
 * Determines whether or not the Moodle site is being served over HTTPS.
212
 *
213
 * This is done simply by checking the value of $CFG->wwwroot, which seems
214
 * to be the only reliable method.
215
 *
216
 * @return boolean True if site is served over HTTPS, false otherwise.
217
 */
218
function is_https() {
219
    global $CFG;
220
 
221
    return (strpos($CFG->wwwroot, 'https://') === 0);
222
}
223
 
224
/**
225
 * Returns the cleaned local URL of the HTTP_REFERER less the URL query string parameters if required.
226
 *
227
 * @param bool $stripquery if true, also removes the query part of the url.
228
 * @return string The resulting referer or empty string.
229
 */
230
function get_local_referer($stripquery = true) {
231
    if (isset($_SERVER['HTTP_REFERER'])) {
232
        $referer = clean_param($_SERVER['HTTP_REFERER'], PARAM_LOCALURL);
233
        if ($stripquery) {
234
            return strip_querystring($referer);
235
        } else {
236
            return $referer;
237
        }
238
    } else {
239
        return '';
240
    }
241
}
242
 
243
/**
244
 * Determine if there is data waiting to be processed from a form
245
 *
246
 * Used on most forms in Moodle to check for data
247
 * Returns the data as an object, if it's found.
248
 * This object can be used in foreach loops without
249
 * casting because it's cast to (array) automatically
250
 *
251
 * Checks that submitted POST data exists and returns it as object.
252
 *
253
 * @return mixed false or object
254
 */
255
function data_submitted() {
256
 
257
    if (empty($_POST)) {
258
        return false;
259
    } else {
260
        return (object)fix_utf8($_POST);
261
    }
262
}
263
 
264
/**
265
 * Given some normal text this function will break up any
266
 * long words to a given size by inserting the given character
267
 *
268
 * It's multibyte savvy and doesn't change anything inside html tags.
269
 *
270
 * @param string $string the string to be modified
271
 * @param int $maxsize maximum length of the string to be returned
272
 * @param string $cutchar the string used to represent word breaks
273
 * @return string
274
 */
275
function break_up_long_words($string, $maxsize=20, $cutchar=' ') {
276
 
277
    // First of all, save all the tags inside the text to skip them.
278
    $tags = array();
279
    filter_save_tags($string, $tags);
280
 
281
    // Process the string adding the cut when necessary.
282
    $output = '';
283
    $length = core_text::strlen($string);
284
    $wordlength = 0;
285
 
286
    for ($i=0; $i<$length; $i++) {
287
        $char = core_text::substr($string, $i, 1);
288
        if ($char == ' ' or $char == "\t" or $char == "\n" or $char == "\r" or $char == "<" or $char == ">") {
289
            $wordlength = 0;
290
        } else {
291
            $wordlength++;
292
            if ($wordlength > $maxsize) {
293
                $output .= $cutchar;
294
                $wordlength = 0;
295
            }
296
        }
297
        $output .= $char;
298
    }
299
 
300
    // Finally load the tags back again.
301
    if (!empty($tags)) {
302
        $output = str_replace(array_keys($tags), $tags, $output);
303
    }
304
 
305
    return $output;
306
}
307
 
308
/**
309
 * Try and close the current window using JavaScript, either immediately, or after a delay.
310
 *
311
 * Echo's out the resulting XHTML & javascript
312
 *
313
 * @param integer $delay a delay in seconds before closing the window. Default 0.
314
 * @param boolean $reloadopener if true, we will see if this window was a pop-up, and try
315
 *      to reload the parent window before this one closes.
316
 */
317
function close_window($delay = 0, $reloadopener = false) {
318
    global $PAGE, $OUTPUT;
319
 
320
    if (!$PAGE->headerprinted) {
321
        $PAGE->set_title(get_string('closewindow'));
322
        echo $OUTPUT->header();
323
    } else {
324
        $OUTPUT->container_end_all(false);
325
    }
326
 
327
    if ($reloadopener) {
328
        // Trigger the reload immediately, even if the reload is after a delay.
329
        $PAGE->requires->js_function_call('window.opener.location.reload', array(true));
330
    }
331
    $OUTPUT->notification(get_string('windowclosing'), 'notifysuccess');
332
 
333
    $PAGE->requires->js_function_call('close_window', array(new stdClass()), false, $delay);
334
 
335
    echo $OUTPUT->footer();
336
    exit;
337
}
338
 
339
/**
340
 * Returns a string containing a link to the user documentation for the current page.
341
 *
342
 * Also contains an icon by default. Shown to teachers and admin only.
343
 *
344
 * @param string $text The text to be displayed for the link
345
 * @return string The link to user documentation for this current page
346
 */
347
function page_doc_link($text='') {
348
    global $OUTPUT, $PAGE;
349
    $path = page_get_doc_link_path($PAGE);
350
    if (!$path) {
351
        return '';
352
    }
353
    return $OUTPUT->doc_link($path, $text);
354
}
355
 
356
/**
357
 * Returns the path to use when constructing a link to the docs.
358
 *
359
 * @since Moodle 2.5.1 2.6
360
 * @param moodle_page $page
361
 * @return string
362
 */
363
function page_get_doc_link_path(moodle_page $page) {
364
    global $CFG;
365
 
366
    if (empty($CFG->docroot) || during_initial_install()) {
367
        return '';
368
    }
369
    if (!has_capability('moodle/site:doclinks', $page->context)) {
370
        return '';
371
    }
372
 
373
    $path = $page->docspath;
374
    if (!$path) {
375
        return '';
376
    }
377
    return $path;
378
}
379
 
380
 
381
/**
382
 * Validates an email to make sure it makes sense.
383
 *
384
 * @param string $address The email address to validate.
385
 * @return boolean
386
 */
387
function validate_email($address) {
388
    global $CFG;
389
 
390
    if ($address === null || $address === false || $address === '') {
391
        return false;
392
    }
393
 
394
    require_once("{$CFG->libdir}/phpmailer/moodle_phpmailer.php");
395
 
396
    return moodle_phpmailer::validateAddress($address ?? '') && !preg_match('/[<>]/', $address);
397
}
398
 
399
/**
400
 * Extracts file argument either from file parameter or PATH_INFO
401
 *
402
 * Note: $scriptname parameter is not needed anymore
403
 *
404
 * @return string file path (only safe characters)
405
 */
406
function get_file_argument() {
407
    global $SCRIPT;
408
 
409
    $relativepath = false;
410
    $hasforcedslashargs = false;
411
 
412
    if (isset($_SERVER['REQUEST_URI']) && !empty($_SERVER['REQUEST_URI'])) {
413
        // Checks whether $_SERVER['REQUEST_URI'] contains '/pluginfile.php/'
414
        // instead of '/pluginfile.php?', when serving a file from e.g. mod_imscp or mod_scorm.
415
        if ((strpos($_SERVER['REQUEST_URI'], '/pluginfile.php/') !== false)
416
                && isset($_SERVER['PATH_INFO']) && !empty($_SERVER['PATH_INFO'])) {
417
            // Exclude edge cases like '/pluginfile.php/?file='.
418
            $args = explode('/', ltrim($_SERVER['PATH_INFO'], '/'));
419
            $hasforcedslashargs = (count($args) > 2); // Always at least: context, component and filearea.
420
        }
421
    }
422
    if (!$hasforcedslashargs) {
423
        $relativepath = optional_param('file', false, PARAM_PATH);
424
    }
425
 
426
    if ($relativepath !== false and $relativepath !== '') {
427
        return $relativepath;
428
    }
429
    $relativepath = false;
430
 
431
    // Then try extract file from the slasharguments.
432
    if (stripos($_SERVER['SERVER_SOFTWARE'], 'iis') !== false) {
433
        // NOTE: IIS tends to convert all file paths to single byte DOS encoding,
434
        //       we can not use other methods because they break unicode chars,
435
        //       the only ways are to use URL rewriting
436
        //       OR
437
        //       to properly set the 'FastCGIUtf8ServerVariables' registry key.
438
        if (isset($_SERVER['PATH_INFO']) and $_SERVER['PATH_INFO'] !== '') {
439
            // Check that PATH_INFO works == must not contain the script name.
440
            if (strpos($_SERVER['PATH_INFO'], $SCRIPT) === false) {
441
                $relativepath = clean_param(urldecode($_SERVER['PATH_INFO']), PARAM_PATH);
442
            }
443
        }
444
    } else {
445
        // All other apache-like servers depend on PATH_INFO.
446
        if (isset($_SERVER['PATH_INFO'])) {
447
            if (isset($_SERVER['SCRIPT_NAME']) and strpos($_SERVER['PATH_INFO'], $_SERVER['SCRIPT_NAME']) === 0) {
448
                $relativepath = substr($_SERVER['PATH_INFO'], strlen($_SERVER['SCRIPT_NAME']));
449
            } else {
450
                $relativepath = $_SERVER['PATH_INFO'];
451
            }
452
            $relativepath = clean_param($relativepath, PARAM_PATH);
453
        }
454
    }
455
 
456
    return $relativepath;
457
}
458
 
459
/**
460
 * Just returns an array of text formats suitable for a popup menu
461
 *
462
 * @return array
463
 */
464
function format_text_menu() {
465
    return array (FORMAT_MOODLE => get_string('formattext'),
466
                  FORMAT_HTML => get_string('formathtml'),
467
                  FORMAT_PLAIN => get_string('formatplain'),
468
                  FORMAT_MARKDOWN => get_string('formatmarkdown'));
469
}
470
 
471
/**
472
 * Given text in a variety of format codings, this function returns the text as safe HTML.
473
 *
474
 * This function should mainly be used for long strings like posts,
475
 * answers, glossary items etc. For short strings {@link format_string()}.
476
 *
477
 * <pre>
478
 * Options:
479
 *      trusted     :   If true the string won't be cleaned. Default false required noclean=true.
480
 *      noclean     :   If true the string won't be cleaned, unless $CFG->forceclean is set. Default false required trusted=true.
481
 *      filter      :   If true the string will be run through applicable filters as well. Default true.
482
 *      para        :   If true then the returned string will be wrapped in div tags. Default true.
483
 *      newlines    :   If true then lines newline breaks will be converted to HTML newline breaks. Default true.
484
 *      context     :   The context that will be used for filtering.
485
 *      overflowdiv :   If set to true the formatted text will be encased in a div
486
 *                      with the class no-overflow before being returned. Default false.
487
 *      allowid     :   If true then id attributes will not be removed, even when
488
 *                      using htmlpurifier. Default false.
489
 *      blanktarget :   If true all <a> tags will have target="_blank" added unless target is explicitly specified.
490
 * </pre>
491
 *
492
 * @param string $text The text to be formatted. This is raw text originally from user input.
493
 * @param int $format Identifier of the text format to be used
494
 *            [FORMAT_MOODLE, FORMAT_HTML, FORMAT_PLAIN, FORMAT_MARKDOWN]
495
 * @param stdClass|array $options text formatting options
496
 * @param int $courseiddonotuse deprecated course id, use context option instead
497
 * @return string
498
 */
499
function format_text($text, $format = FORMAT_MOODLE, $options = null, $courseiddonotuse = null) {
500
    global $CFG;
501
 
502
    // Manually include the formatting class for now until after the release after 4.5 LTS.
503
    require_once("{$CFG->libdir}/classes/formatting.php");
504
 
505
    if ($format === FORMAT_WIKI) {
506
        // This format was deprecated in Moodle 1.5.
507
        throw new \coding_exception(
508
            'Wiki-like formatting is not supported.'
509
        );
510
    }
511
 
512
    if ($options instanceof \core\context) {
513
        // A common mistake has been to call this function with a context object.
514
        // This has never been expected, or nor supported.
515
        debugging(
516
            'The options argument should not be a context object directly. ' .
517
                ' Please pass an array with a context key instead.',
518
            DEBUG_DEVELOPER,
519
        );
520
        $params['context'] = $options;
521
        $options = [];
522
    }
523
 
524
    if ($options) {
525
        $options = (array) $options;
526
    }
527
 
528
    if (empty($CFG->version) || $CFG->version < 2013051400 || during_initial_install()) {
529
        // Do not filter anything during installation or before upgrade completes.
530
        $params['context'] = null;
531
    } else if ($options && isset($options['context'])) { // First by explicit passed context option.
532
        if (is_numeric($options['context'])) {
533
            // A contextid was passed.
534
            $params['context'] = \core\context::instance_by_id($options['context']);
535
        } else if ($options['context'] instanceof \core\context) {
536
            $params['context'] = $options['context'];
537
        } else {
538
            debugging(
539
                'Unknown context passed to format_text(). Content will not be filtered.',
540
                DEBUG_DEVELOPER,
541
            );
542
        }
543
 
544
        // Unset the context from $options to prevent it overriding the configured value.
545
        unset($options['context']);
546
    } else if ($courseiddonotuse) {
547
        // Legacy courseid.
548
        $params['context'] = \core\context\course::instance($courseiddonotuse);
549
        debugging(
550
            "Passing a courseid to format_text() is deprecated, please pass a context instead.",
551
            DEBUG_DEVELOPER,
552
        );
553
    }
554
 
555
    $params['text'] =  $text;
556
 
557
    if ($options) {
558
        // The smiley option was deprecated in Moodle 2.0.
559
        if (array_key_exists('smiley', $options)) {
560
            unset($options['smiley']);
561
            debugging(
562
                'The smiley option is deprecated and no longer used.',
563
                DEBUG_DEVELOPER,
564
            );
565
        }
566
 
567
        // The nocache option was deprecated in Moodle 2.3 in MDL-34347.
568
        if (array_key_exists('nocache', $options)) {
569
            unset($options['nocache']);
570
            debugging(
571
                'The nocache option is deprecated and no longer used.',
572
                DEBUG_DEVELOPER,
573
            );
574
        }
575
 
576
        $validoptions = [
577
            'text',
578
            'format',
579
            'context',
580
            'trusted',
581
            'clean',
582
            'filter',
583
            'para',
584
            'newlines',
585
            'overflowdiv',
586
            'blanktarget',
587
            'allowid',
588
            'noclean',
589
        ];
590
 
591
        $invalidoptions = array_diff(array_keys($options), $validoptions);
592
        if ($invalidoptions) {
593
            debugging(sprintf(
594
                'The following options are not valid: %s',
595
                implode(', ', $invalidoptions),
596
            ), DEBUG_DEVELOPER);
597
            foreach ($invalidoptions as $option) {
598
                unset($options[$option]);
599
            }
600
        }
601
 
602
        foreach ($options as $option => $value) {
603
            $params[$option] = $value;
604
        }
605
 
606
        // The noclean option has been renamed to clean.
607
        if (array_key_exists('noclean', $params)) {
608
            $params['clean'] = !$params['noclean'];
609
            unset($params['noclean']);
610
        }
611
    }
612
 
613
    if ($format !== null) {
614
        $params['format'] = $format;
615
    }
616
 
617
    return \core\di::get(\core\formatting::class)->format_text(...$params);
618
}
619
 
620
/**
621
 * Resets some data related to filters, called during upgrade or when general filter settings change.
622
 *
623
 * @param bool $phpunitreset true means called from our PHPUnit integration test reset
624
 * @return void
625
 */
626
function reset_text_filters_cache($phpunitreset = false) {
627
    global $CFG, $DB;
628
 
629
    if ($phpunitreset) {
630
        // HTMLPurifier does not change, DB is already reset to defaults,
631
        // nothing to do here, the dataroot was cleared too.
632
        return;
633
    }
634
 
635
    // The purge_all_caches() deals with cachedir and localcachedir purging,
636
    // the individual filter caches are invalidated as necessary elsewhere.
637
 
638
    // Update $CFG->filterall cache flag.
639
    if (empty($CFG->stringfilters)) {
640
        set_config('filterall', 0);
641
        return;
642
    }
643
    $installedfilters = core_component::get_plugin_list('filter');
644
    $filters = explode(',', $CFG->stringfilters);
645
    foreach ($filters as $filter) {
646
        if (isset($installedfilters[$filter])) {
647
            set_config('filterall', 1);
648
            return;
649
        }
650
    }
651
    set_config('filterall', 0);
652
}
653
 
654
/**
655
 * Given a simple string, this function returns the string
656
 * processed by enabled string filters if $CFG->filterall is enabled
657
 *
658
 * This function should be used to print short strings (non html) that
659
 * need filter processing e.g. activity titles, post subjects,
660
 * glossary concepts.
661
 *
662
 * @staticvar bool $strcache
663
 * @param string $string The string to be filtered. Should be plain text, expect
664
 * possibly for multilang tags.
665
 * @param ?bool $striplinks To strip any link in the result text. Moodle 1.8 default changed from false to true! MDL-8713
666
 * @param array $options options array/object or courseid
667
 * @return string
668
 */
669
function format_string($string, $striplinks = true, $options = null) {
670
    global $CFG;
671
 
672
    // Manually include the formatting class for now until after the release after 4.5 LTS.
673
    require_once("{$CFG->libdir}/classes/formatting.php");
674
 
675
    $params = [
676
        'string' => $string,
677
        'striplinks' => (bool) $striplinks,
678
    ];
679
 
680
    // This method only expects either:
681
    // - an array of options;
682
    // - a stdClass of options to be cast to an array; or
683
    // - an integer courseid.
684
    if ($options instanceof \core\context) {
685
        // A common mistake has been to call this function with a context object.
686
        // This has never been expected, or nor supported.
687
        debugging(
688
            'The options argument should not be a context object directly. ' .
689
                ' Please pass an array with a context key instead.',
690
            DEBUG_DEVELOPER,
691
        );
692
        $params['context'] = $options;
693
        $options = [];
694
    } else if (is_numeric($options)) {
695
        // Legacy courseid usage.
696
        $params['context'] = \core\context\course::instance($options);
697
        $options = [];
698
    } else if (is_array($options) || is_a($options, \stdClass::class)) {
699
        $options = (array) $options;
700
        if (isset($options['context'])) {
701
            if (is_numeric($options['context'])) {
702
                // A contextid was passed usage.
703
                $params['context'] = \core\context::instance_by_id($options['context']);
704
            } else if ($options['context'] instanceof \core\context) {
705
                $params['context'] = $options['context'];
706
            } else {
707
                debugging(
708
                    'An invalid value for context was provided.',
709
                    DEBUG_DEVELOPER,
710
                );
711
            }
712
        }
713
    } else if ($options !== null) {
714
        // Something else was passed, so we'll just use an empty array.
715
        debugging(sprintf(
716
            'The options argument should be an Array, or stdclass. %s passed.',
717
            gettype($options),
718
        ), DEBUG_DEVELOPER);
719
 
720
        // Attempt to cast to array since we always used to, but throw in some debugging.
721
        $options = array_filter(
722
            (array) $options,
723
            fn ($key) => !is_numeric($key),
724
            ARRAY_FILTER_USE_KEY,
725
        );
726
    }
727
 
728
    if (isset($options['filter'])) {
729
        $params['filter'] = (bool) $options['filter'];
730
    } else {
731
        $params['filter'] = true;
732
    }
733
 
734
    if (isset($options['escape'])) {
735
        $params['escape'] = (bool) $options['escape'];
736
    } else {
737
        $params['escape'] = true;
738
    }
739
 
740
    $validoptions = [
741
        'string',
742
        'striplinks',
743
        'context',
744
        'filter',
745
        'escape',
746
    ];
747
 
748
    if ($options) {
749
        $invalidoptions = array_diff(array_keys($options), $validoptions);
750
        if ($invalidoptions) {
751
            debugging(sprintf(
752
                'The following options are not valid: %s',
753
                implode(', ', $invalidoptions),
754
            ), DEBUG_DEVELOPER);
755
        }
756
    }
757
 
758
    return \core\di::get(\core\formatting::class)->format_string(
759
        ...$params,
760
    );
761
}
762
 
763
/**
764
 * Given a string, performs a negative lookahead looking for any ampersand character
765
 * that is not followed by a proper HTML entity. If any is found, it is replaced
766
 * by &amp;. The string is then returned.
767
 *
768
 * @param string $string
769
 * @return string
770
 */
771
function replace_ampersands_not_followed_by_entity($string) {
772
    return preg_replace("/\&(?![a-zA-Z0-9#]{1,8};)/", "&amp;", $string ?? '');
773
}
774
 
775
/**
776
 * Given a string, replaces all <a>.*</a> by .* and returns the string.
777
 *
778
 * @param string $string
779
 * @return string
780
 */
781
function strip_links($string) {
782
    return preg_replace('/(<a\s[^>]+?>)(.+?)(<\/a>)/is', '$2', $string);
783
}
784
 
785
/**
786
 * This expression turns links into something nice in a text format. (Russell Jungwirth)
787
 *
788
 * @param string $string
789
 * @return string
790
 */
791
function wikify_links($string) {
792
    return preg_replace('~(<a [^<]*href=["|\']?([^ "\']*)["|\']?[^>]*>([^<]*)</a>)~i', '$3 [ $2 ]', $string);
793
}
794
 
795
/**
796
 * Given text in a variety of format codings, this function returns the text as plain text suitable for plain email.
797
 *
798
 * @param string $text The text to be formatted. This is raw text originally from user input.
799
 * @param int $format Identifier of the text format to be used
800
 *            [FORMAT_MOODLE, FORMAT_HTML, FORMAT_PLAIN, FORMAT_WIKI, FORMAT_MARKDOWN]
801
 * @return string
802
 */
803
function format_text_email($text, $format) {
804
 
805
    switch ($format) {
806
 
807
        case FORMAT_PLAIN:
808
            return $text;
809
            break;
810
 
811
        case FORMAT_WIKI:
812
            // There should not be any of these any more!
813
            $text = wikify_links($text);
814
            return core_text::entities_to_utf8(strip_tags($text), true);
815
            break;
816
 
817
        case FORMAT_HTML:
818
            return html_to_text($text);
819
            break;
820
 
821
        case FORMAT_MOODLE:
822
        case FORMAT_MARKDOWN:
823
        default:
824
            $text = wikify_links($text);
825
            return core_text::entities_to_utf8(strip_tags($text), true);
826
            break;
827
    }
828
}
829
 
830
/**
831
 * Formats activity intro text
832
 *
833
 * @param string $module name of module
834
 * @param object $activity instance of activity
835
 * @param int $cmid course module id
836
 * @param bool $filter filter resulting html text
837
 * @return string
838
 */
839
function format_module_intro($module, $activity, $cmid, $filter=true) {
840
    global $CFG;
841
    require_once("$CFG->libdir/filelib.php");
842
    $context = context_module::instance($cmid);
843
    $options = array('noclean' => true, 'para' => false, 'filter' => $filter, 'context' => $context, 'overflowdiv' => true);
844
    $intro = file_rewrite_pluginfile_urls($activity->intro, 'pluginfile.php', $context->id, 'mod_'.$module, 'intro', null);
845
    return trim(format_text($intro, $activity->introformat, $options, null));
846
}
847
 
848
/**
849
 * Removes the usage of Moodle files from a text.
850
 *
851
 * In some rare cases we need to re-use a text that already has embedded links
852
 * to some files hosted within Moodle. But the new area in which we will push
853
 * this content does not support files... therefore we need to remove those files.
854
 *
855
 * @param string $source The text
856
 * @return string The stripped text
857
 */
858
function strip_pluginfile_content($source) {
859
    $baseurl = '@@PLUGINFILE@@';
860
    // Looking for something like < .* "@@pluginfile@@.*" .* >
861
    $pattern = '$<[^<>]+["\']' . $baseurl . '[^"\']*["\'][^<>]*>$';
862
    $stripped = preg_replace($pattern, '', $source);
863
    // Use purify html to rebalence potentially mismatched tags and generally cleanup.
864
    return purify_html($stripped);
865
}
866
 
867
/**
868
 * Legacy function, used for cleaning of old forum and glossary text only.
869
 *
870
 * @param string $text text that may contain legacy TRUSTTEXT marker
871
 * @return string text without legacy TRUSTTEXT marker
872
 */
873
function trusttext_strip($text) {
874
    if (!is_string($text)) {
875
        // This avoids the potential for an endless loop below.
876
        throw new coding_exception('trusttext_strip parameter must be a string');
877
    }
878
    while (true) { // Removing nested TRUSTTEXT.
879
        $orig = $text;
880
        $text = str_replace('#####TRUSTTEXT#####', '', $text);
881
        if (strcmp($orig, $text) === 0) {
882
            return $text;
883
        }
884
    }
885
}
886
 
887
/**
888
 * Must be called before editing of all texts with trust flag. Removes all XSS nasties from texts stored in database if needed.
889
 *
890
 * @param stdClass $object data object with xxx, xxxformat and xxxtrust fields
891
 * @param string $field name of text field
892
 * @param context $context active context
893
 * @return stdClass updated $object
894
 */
895
function trusttext_pre_edit($object, $field, $context) {
896
    $trustfield  = $field.'trust';
897
    $formatfield = $field.'format';
898
 
899
    if ($object->$formatfield == FORMAT_MARKDOWN) {
900
        // We do not have a way to sanitise Markdown texts,
901
        // luckily editors for this format should not have XSS problems.
902
        return $object;
903
    }
904
 
905
    if (!$object->$trustfield or !trusttext_trusted($context)) {
906
        $object->$field = clean_text($object->$field, $object->$formatfield);
907
    }
908
 
909
    return $object;
910
}
911
 
912
/**
913
 * Is current user trusted to enter no dangerous XSS in this context?
914
 *
915
 * Please note the user must be in fact trusted everywhere on this server!!
916
 *
917
 * @param context $context
918
 * @return bool true if user trusted
919
 */
920
function trusttext_trusted($context) {
921
    return (trusttext_active() and has_capability('moodle/site:trustcontent', $context));
922
}
923
 
924
/**
925
 * Is trusttext feature active?
926
 *
927
 * @return bool
928
 */
929
function trusttext_active() {
930
    global $CFG;
931
 
932
    return !empty($CFG->enabletrusttext);
933
}
934
 
935
/**
936
 * Cleans raw text removing nasties.
937
 *
938
 * Given raw text (eg typed in by a user) this function cleans it up and removes any nasty tags that could mess up
939
 * Moodle pages through XSS attacks.
940
 *
941
 * The result must be used as a HTML text fragment, this function can not cleanup random
942
 * parts of html tags such as url or src attributes.
943
 *
944
 * NOTE: the format parameter was deprecated because we can safely clean only HTML.
945
 *
946
 * @param string $text The text to be cleaned
947
 * @param int|string $format deprecated parameter, should always contain FORMAT_HTML or FORMAT_MOODLE
948
 * @param array $options Array of options; currently only option supported is 'allowid' (if true,
949
 *   does not remove id attributes when cleaning)
950
 * @return string The cleaned up text
951
 */
952
function clean_text($text, $format = FORMAT_HTML, $options = array()) {
953
    $text = (string)$text;
954
 
955
    if ($format != FORMAT_HTML and $format != FORMAT_HTML) {
956
        // TODO: we need to standardise cleanup of text when loading it into editor first.
957
        // debugging('clean_text() is designed to work only with html');.
958
    }
959
 
960
    if ($format == FORMAT_PLAIN) {
961
        return $text;
962
    }
963
 
964
    if (is_purify_html_necessary($text)) {
965
        $text = purify_html($text, $options);
966
    }
967
 
968
    // Originally we tried to neutralise some script events here, it was a wrong approach because
969
    // it was trivial to work around that (for example using style based XSS exploits).
970
    // We must not give false sense of security here - all developers MUST understand how to use
971
    // rawurlencode(), htmlentities(), htmlspecialchars(), p(), s(), moodle_url, html_writer and friends!!!
972
 
973
    return $text;
974
}
975
 
976
/**
977
 * Is it necessary to use HTMLPurifier?
978
 *
979
 * @private
980
 * @param string $text
981
 * @return bool false means html is safe and valid, true means use HTMLPurifier
982
 */
983
function is_purify_html_necessary($text) {
984
    if ($text === '') {
985
        return false;
986
    }
987
 
988
    if ($text === (string)((int)$text)) {
989
        return false;
990
    }
991
 
992
    if (strpos($text, '&') !== false or preg_match('|<[^pesb/]|', $text)) {
993
        // We need to normalise entities or other tags except p, em, strong and br present.
994
        return true;
995
    }
996
 
997
    $altered = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8', true);
998
    if ($altered === $text) {
999
        // No < > or other special chars means this must be safe.
1000
        return false;
1001
    }
1002
 
1003
    // Let's try to convert back some safe html tags.
1004
    $altered = preg_replace('|&lt;p&gt;(.*?)&lt;/p&gt;|m', '<p>$1</p>', $altered);
1005
    if ($altered === $text) {
1006
        return false;
1007
    }
1008
    $altered = preg_replace('|&lt;em&gt;([^<>]+?)&lt;/em&gt;|m', '<em>$1</em>', $altered);
1009
    if ($altered === $text) {
1010
        return false;
1011
    }
1012
    $altered = preg_replace('|&lt;strong&gt;([^<>]+?)&lt;/strong&gt;|m', '<strong>$1</strong>', $altered);
1013
    if ($altered === $text) {
1014
        return false;
1015
    }
1016
    $altered = str_replace('&lt;br /&gt;', '<br />', $altered);
1017
    if ($altered === $text) {
1018
        return false;
1019
    }
1020
 
1021
    return true;
1022
}
1023
 
1024
/**
1025
 * KSES replacement cleaning function - uses HTML Purifier.
1026
 *
1027
 * @param string $text The (X)HTML string to purify
1028
 * @param array $options Array of options; currently only option supported is 'allowid' (if set,
1029
 *   does not remove id attributes when cleaning)
1030
 * @return string
1031
 */
1032
function purify_html($text, $options = array()) {
1033
    global $CFG;
1034
 
1035
    $text = (string)$text;
1036
 
1037
    static $purifiers = array();
1038
    static $caches = array();
1039
 
1040
    // Purifier code can change only during major version upgrade.
1041
    $version = empty($CFG->version) ? 0 : $CFG->version;
1042
    $cachedir = "$CFG->localcachedir/htmlpurifier/$version";
1043
    if (!file_exists($cachedir)) {
1044
        // Purging of caches may remove the cache dir at any time,
1045
        // luckily file_exists() results should be cached for all existing directories.
1046
        $purifiers = array();
1047
        $caches = array();
1048
        gc_collect_cycles();
1049
 
1050
        make_localcache_directory('htmlpurifier', false);
1051
        check_dir_exists($cachedir);
1052
    }
1053
 
1054
    $allowid = empty($options['allowid']) ? 0 : 1;
1055
    $allowobjectembed = empty($CFG->allowobjectembed) ? 0 : 1;
1056
 
1057
    $type = 'type_'.$allowid.'_'.$allowobjectembed;
1058
 
1059
    if (!array_key_exists($type, $caches)) {
1060
        $caches[$type] = cache::make('core', 'htmlpurifier', array('type' => $type));
1061
    }
1062
    $cache = $caches[$type];
1063
 
1064
    // Add revision number and all options to the text key so that it is compatible with local cluster node caches.
1065
    $key = "|$version|$allowobjectembed|$allowid|$text";
1066
    $filteredtext = $cache->get($key);
1067
 
1068
    if ($filteredtext === true) {
1069
        // The filtering did not change the text last time, no need to filter anything again.
1070
        return $text;
1071
    } else if ($filteredtext !== false) {
1072
        return $filteredtext;
1073
    }
1074
 
1075
    if (empty($purifiers[$type])) {
1076
        require_once $CFG->libdir.'/htmlpurifier/HTMLPurifier.safe-includes.php';
1077
        require_once $CFG->libdir.'/htmlpurifier/locallib.php';
1078
        $config = HTMLPurifier_Config::createDefault();
1079
 
1080
        $config->set('HTML.DefinitionID', 'moodlehtml');
1081
        $config->set('HTML.DefinitionRev', 7);
1441 ariadna 1082
        $config->set('CSS.Proprietary', true);
1 efrain 1083
        $config->set('Cache.SerializerPath', $cachedir);
1084
        $config->set('Cache.SerializerPermissions', $CFG->directorypermissions);
1085
        $config->set('Core.NormalizeNewlines', false);
1086
        $config->set('Core.ConvertDocumentToFragment', true);
1087
        $config->set('Core.Encoding', 'UTF-8');
1088
        $config->set('HTML.Doctype', 'XHTML 1.0 Transitional');
1089
        $config->set('URI.AllowedSchemes', array(
1090
            'http' => true,
1091
            'https' => true,
1092
            'ftp' => true,
1093
            'irc' => true,
1094
            'nntp' => true,
1095
            'news' => true,
1096
            'rtsp' => true,
1097
            'rtmp' => true,
1098
            'teamspeak' => true,
1099
            'gopher' => true,
1100
            'mms' => true,
1101
            'mailto' => true
1102
        ));
1103
        $config->set('Attr.AllowedFrameTargets', array('_blank'));
1104
 
1105
        if ($allowobjectembed) {
1106
            $config->set('HTML.SafeObject', true);
1107
            $config->set('Output.FlashCompat', true);
1108
            $config->set('HTML.SafeEmbed', true);
1109
        }
1110
 
1111
        if ($allowid) {
1112
            $config->set('Attr.EnableID', true);
1113
        }
1114
 
1115
        if ($def = $config->maybeGetRawHTMLDefinition()) {
1116
            $def->addElement('nolink', 'Inline', 'Flow', array());                      // Skip our filters inside.
1117
            $def->addElement('tex', 'Inline', 'Inline', array());                       // Tex syntax, equivalent to $$xx$$.
1118
            $def->addElement('algebra', 'Inline', 'Inline', array());                   // Algebra syntax, equivalent to @@xx@@.
1119
            $def->addElement('lang', 'Block', 'Flow', array(), array('lang'=>'CDATA')); // Original multilang style - only our hacked lang attribute.
1120
            $def->addAttribute('span', 'xxxlang', 'CDATA');                             // Current very problematic multilang.
1121
            // Enable the bidirectional isolate element and its span equivalent.
1122
            $def->addElement('bdi', 'Inline', 'Flow', 'Common');
1123
            $def->addAttribute('span', 'dir', 'Enum#ltr,rtl,auto');
1124
 
1125
            // Media elements.
1126
            // https://html.spec.whatwg.org/#the-video-element
1127
            $def->addElement('video', 'Inline', 'Optional: #PCDATA | Flow | source | track', 'Common', [
1128
                'src' => 'URI',
1129
                'crossorigin' => 'Enum#anonymous,use-credentials',
1130
                'poster' => 'URI',
1131
                'preload' => 'Enum#auto,metadata,none',
1132
                'autoplay' => 'Bool',
1133
                'playsinline' => 'Bool',
1134
                'loop' => 'Bool',
1135
                'muted' => 'Bool',
1136
                'controls' => 'Bool',
1137
                'width' => 'Length',
1138
                'height' => 'Length',
1139
            ]);
1140
            // https://html.spec.whatwg.org/#the-audio-element
1141
            $def->addElement('audio', 'Inline', 'Optional: #PCDATA | Flow | source | track', 'Common', [
1142
                'src' => 'URI',
1143
                'crossorigin' => 'Enum#anonymous,use-credentials',
1144
                'preload' => 'Enum#auto,metadata,none',
1145
                'autoplay' => 'Bool',
1146
                'loop' => 'Bool',
1147
                'muted' => 'Bool',
1148
                'controls' => 'Bool'
1149
            ]);
1150
            // https://html.spec.whatwg.org/#the-source-element
1151
            $def->addElement('source', false, 'Empty', null, [
1152
                'src' => 'URI',
1153
                'type' => 'Text'
1154
            ]);
1155
            // https://html.spec.whatwg.org/#the-track-element
1156
            $def->addElement('track', false, 'Empty', null, [
1157
                'src' => 'URI',
1158
                'kind' => 'Enum#subtitles,captions,descriptions,chapters,metadata',
1159
                'srclang' => 'Text',
1160
                'label' => 'Text',
1161
                'default' => 'Bool',
1162
            ]);
1163
 
1164
            // Use the built-in Ruby module to add annotation support.
1165
            $def->manager->addModule(new HTMLPurifier_HTMLModule_Ruby());
1166
        }
1167
 
1168
        $purifier = new HTMLPurifier($config);
1169
        $purifiers[$type] = $purifier;
1170
    } else {
1171
        $purifier = $purifiers[$type];
1172
    }
1173
 
1174
    $multilang = (strpos($text, 'class="multilang"') !== false);
1175
 
1176
    $filteredtext = $text;
1177
    if ($multilang) {
1178
        $filteredtextregex = '/<span(\s+lang="([a-zA-Z0-9_-]+)"|\s+class="multilang"){2}\s*>/';
1179
        $filteredtext = preg_replace($filteredtextregex, '<span xxxlang="${2}">', $filteredtext);
1180
    }
1181
    $filteredtext = (string)$purifier->purify($filteredtext);
1182
    if ($multilang) {
1183
        $filteredtext = preg_replace('/<span xxxlang="([a-zA-Z0-9_-]+)">/', '<span lang="${1}" class="multilang">', $filteredtext);
1184
    }
1185
 
1186
    if ($text === $filteredtext) {
1187
        // No need to store the filtered text, next time we will just return unfiltered text
1188
        // because it was not changed by purifying.
1189
        $cache->set($key, true);
1190
    } else {
1191
        $cache->set($key, $filteredtext);
1192
    }
1193
 
1194
    return $filteredtext;
1195
}
1196
 
1197
/**
1198
 * Given plain text, makes it into HTML as nicely as possible.
1199
 *
1200
 * May contain HTML tags already.
1201
 *
1202
 * Do not abuse this function. It is intended as lower level formatting feature used
1203
 * by {@link format_text()} to convert FORMAT_MOODLE to HTML. You are supposed
1204
 * to call format_text() in most of cases.
1205
 *
1206
 * @param string $text The string to convert.
1207
 * @param boolean $smileyignored Was used to determine if smiley characters should convert to smiley images, ignored now
1208
 * @param boolean $para If true then the returned string will be wrapped in div tags
1209
 * @param boolean $newlines If true then lines newline breaks will be converted to HTML newline breaks.
1210
 * @return string
1211
 */
1212
function text_to_html($text, $smileyignored = null, $para = true, $newlines = true) {
1213
    // Remove any whitespace that may be between HTML tags.
1214
    $text = preg_replace("~>([[:space:]]+)<~i", "><", $text);
1215
 
1216
    // Remove any returns that precede or follow HTML tags.
1217
    $text = preg_replace("~([\n\r])<~i", " <", $text);
1218
    $text = preg_replace("~>([\n\r])~i", "> ", $text);
1219
 
1220
    // Make returns into HTML newlines.
1221
    if ($newlines) {
1222
        $text = nl2br($text);
1223
    }
1224
 
1225
    // Wrap the whole thing in a div if required.
1226
    if ($para) {
1227
        // In 1.9 this was changed from a p => div.
1228
        return '<div class="text_to_html">'.$text.'</div>';
1229
    } else {
1230
        return $text;
1231
    }
1232
}
1233
 
1234
/**
1235
 * Given Markdown formatted text, make it into XHTML using external function
1236
 *
1237
 * @param string $text The markdown formatted text to be converted.
1238
 * @return string Converted text
1239
 */
1240
function markdown_to_html($text) {
1241
    if ($text === '' or $text === null) {
1242
        return $text;
1243
    }
1244
 
1245
    return \Michelf\MarkdownExtra::defaultTransform($text);
1246
}
1247
 
1248
/**
1249
 * Given HTML text, make it into plain text using external function
1250
 *
1251
 * @param string $html The text to be converted.
1252
 * @param integer $width Width to wrap the text at. (optional, default 75 which
1253
 *      is a good value for email. 0 means do not limit line length.)
1254
 * @param boolean $dolinks By default, any links in the HTML are collected, and
1255
 *      printed as a list at the end of the HTML. If you don't want that, set this
1256
 *      argument to false.
1257
 * @return string plain text equivalent of the HTML.
1258
 */
1259
function html_to_text($html, $width = 75, $dolinks = true) {
1260
    global $CFG;
1261
 
1262
    require_once($CFG->libdir .'/html2text/lib.php');
1263
 
1264
    $options = array(
1265
        'width'     => $width,
1266
        'do_links'  => 'table',
1267
    );
1268
 
1269
    if (empty($dolinks)) {
1270
        $options['do_links'] = 'none';
1271
    }
1272
    $h2t = new core_html2text($html, $options);
1273
    $result = $h2t->getText();
1274
 
1275
    return $result;
1276
}
1277
 
1278
/**
1279
 * Converts texts or strings to plain text.
1280
 *
1281
 * - When used to convert user input introduced in an editor the text format needs to be passed in $contentformat like we usually
1282
 *   do in format_text.
1283
 * - When this function is used for strings that are usually passed through format_string before displaying them
1284
 *   we need to set $contentformat to false. This will execute html_to_text as these strings can contain multilang tags if
1285
 *   multilang filter is applied to headings.
1286
 *
1287
 * @param string $content The text as entered by the user
1288
 * @param int|false $contentformat False for strings or the text format: FORMAT_MOODLE/FORMAT_HTML/FORMAT_PLAIN/FORMAT_MARKDOWN
1289
 * @return string Plain text.
1290
 */
1291
function content_to_text($content, $contentformat) {
1292
 
1293
    switch ($contentformat) {
1294
        case FORMAT_PLAIN:
1295
            // Nothing here.
1296
            break;
1297
        case FORMAT_MARKDOWN:
1298
            $content = markdown_to_html($content);
1299
            $content = html_to_text($content, 75, false);
1300
            break;
1301
        default:
1302
            // FORMAT_HTML, FORMAT_MOODLE and $contentformat = false, the later one are strings usually formatted through
1303
            // format_string, we need to convert them from html because they can contain HTML (multilang filter).
1304
            $content = html_to_text($content, 75, false);
1305
    }
1306
 
1307
    return trim($content, "\r\n ");
1308
}
1309
 
1310
/**
1311
 * Factory method for extracting draft file links from arbitrary text using regular expressions. Only text
1312
 * is required; other file fields may be passed to filter.
1313
 *
1314
 * @param string $text Some html content.
1315
 * @param bool $forcehttps force https urls.
1316
 * @param int $contextid This parameter and the next three identify the file area to save to.
1317
 * @param string $component The component name.
1318
 * @param string $filearea The filearea.
1319
 * @param int $itemid The item id for the filearea.
1320
 * @param string $filename The specific filename of the file.
1321
 * @return array
1322
 */
1323
function extract_draft_file_urls_from_text($text, $forcehttps = false, $contextid = null, $component = null,
1324
                                           $filearea = null, $itemid = null, $filename = null) {
1325
    global $CFG;
1326
 
1327
    $wwwroot = $CFG->wwwroot;
1328
    if ($forcehttps) {
1329
        $wwwroot = str_replace('http://', 'https://', $wwwroot);
1330
    }
1331
    $urlstring = '/' . preg_quote($wwwroot, '/');
1332
 
1333
    $urlbase = preg_quote('draftfile.php');
1334
    $urlstring .= "\/(?<urlbase>{$urlbase})";
1335
 
1336
    if (is_null($contextid)) {
1337
        $contextid = '[0-9]+';
1338
    }
1339
    $urlstring .= "\/(?<contextid>{$contextid})";
1340
 
1341
    if (is_null($component)) {
1342
        $component = '[a-z_]+';
1343
    }
1344
    $urlstring .= "\/(?<component>{$component})";
1345
 
1346
    if (is_null($filearea)) {
1347
        $filearea = '[a-z_]+';
1348
    }
1349
    $urlstring .= "\/(?<filearea>{$filearea})";
1350
 
1351
    if (is_null($itemid)) {
1352
        $itemid = '[0-9]+';
1353
    }
1354
    $urlstring .= "\/(?<itemid>{$itemid})";
1355
 
1356
    // Filename matching magic based on file_rewrite_urls_to_pluginfile().
1357
    if (is_null($filename)) {
1358
        $filename = '[^\'\",&<>|`\s:\\\\]+';
1359
    }
1360
    $urlstring .= "\/(?<filename>{$filename})/";
1361
 
1362
    // Regular expression which matches URLs and returns their components.
1363
    preg_match_all($urlstring, $text, $urls, PREG_SET_ORDER);
1364
    return $urls;
1365
}
1366
 
1367
/**
1368
 * This function will highlight search words in a given string
1369
 *
1370
 * It cares about HTML and will not ruin links.  It's best to use
1371
 * this function after performing any conversions to HTML.
1372
 *
1373
 * @param string $needle The search string. Syntax like "word1 +word2 -word3" is dealt with correctly.
1374
 * @param string $haystack The string (HTML) within which to highlight the search terms.
1375
 * @param boolean $matchcase whether to do case-sensitive. Default case-insensitive.
1376
 * @param string $prefix the string to put before each search term found.
1377
 * @param string $suffix the string to put after each search term found.
1378
 * @return string The highlighted HTML.
1379
 */
1380
function highlight($needle, $haystack, $matchcase = false,
1381
        $prefix = '<span class="highlight">', $suffix = '</span>') {
1382
 
1383
    // Quick bail-out in trivial cases.
1384
    if (empty($needle) or empty($haystack)) {
1385
        return $haystack;
1386
    }
1387
 
1388
    // Break up the search term into words, discard any -words and build a regexp.
1389
    $words = preg_split('/ +/', trim($needle));
1390
    foreach ($words as $index => $word) {
1391
        if (strpos($word, '-') === 0) {
1392
            unset($words[$index]);
1393
        } else if (strpos($word, '+') === 0) {
1394
            $words[$index] = '\b' . preg_quote(ltrim($word, '+'), '/') . '\b'; // Match only as a complete word.
1395
        } else {
1396
            $words[$index] = preg_quote($word, '/');
1397
        }
1398
    }
1399
    $regexp = '/(' . implode('|', $words) . ')/u'; // Char u is to do UTF-8 matching.
1400
    if (!$matchcase) {
1401
        $regexp .= 'i';
1402
    }
1403
 
1404
    // Another chance to bail-out if $search was only -words.
1405
    if (empty($words)) {
1406
        return $haystack;
1407
    }
1408
 
1409
    // Split the string into HTML tags and real content.
1410
    $chunks = preg_split('/((?:<[^>]*>)+)/', $haystack, -1, PREG_SPLIT_DELIM_CAPTURE);
1411
 
1412
    // We have an array of alternating blocks of text, then HTML tags, then text, ...
1413
    // Loop through replacing search terms in the text, and leaving the HTML unchanged.
1414
    $ishtmlchunk = false;
1415
    $result = '';
1416
    foreach ($chunks as $chunk) {
1417
        if ($ishtmlchunk) {
1418
            $result .= $chunk;
1419
        } else {
1420
            $result .= preg_replace($regexp, $prefix . '$1' . $suffix, $chunk);
1421
        }
1422
        $ishtmlchunk = !$ishtmlchunk;
1423
    }
1424
 
1425
    return $result;
1426
}
1427
 
1428
/**
1429
 * This function will highlight instances of $needle in $haystack
1430
 *
1431
 * It's faster that the above function {@link highlight()} and doesn't care about
1432
 * HTML or anything.
1433
 *
1434
 * @param string $needle The string to search for
1435
 * @param string $haystack The string to search for $needle in
1436
 * @return string The highlighted HTML
1437
 */
1438
function highlightfast($needle, $haystack) {
1439
 
1440
    if (empty($needle) or empty($haystack)) {
1441
        return $haystack;
1442
    }
1443
 
1444
    $parts = explode(core_text::strtolower($needle), core_text::strtolower($haystack));
1445
 
1446
    if (count($parts) === 1) {
1447
        return $haystack;
1448
    }
1449
 
1450
    $pos = 0;
1451
 
1452
    foreach ($parts as $key => $part) {
1453
        $parts[$key] = substr($haystack, $pos, strlen($part));
1454
        $pos += strlen($part);
1455
 
1456
        $parts[$key] .= '<span class="highlight">'.substr($haystack, $pos, strlen($needle)).'</span>';
1457
        $pos += strlen($needle);
1458
    }
1459
 
1460
    return str_replace('<span class="highlight"></span>', '', join('', $parts));
1461
}
1462
 
1463
/**
1464
 * Converts a language code to hyphen-separated format in accordance to the
1465
 * {@link https://datatracker.ietf.org/doc/html/rfc5646#section-2.1 BCP47 syntax}.
1466
 *
1467
 * For additional information, check out
1468
 * {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang MDN web docs - lang}.
1469
 *
1470
 * @param string $langcode The language code to convert.
1471
 * @return string
1472
 */
1473
function get_html_lang_attribute_value(string $langcode): string {
1474
    $langcode = clean_param($langcode, PARAM_LANG);
1475
    if ($langcode === '') {
1476
        return 'en';
1477
    }
1478
 
1479
    // Grab language ISO code from lang config. If it differs from English, then it's been specified and we can return it.
1480
    $langiso = (string) (new lang_string('iso6391', 'core_langconfig', null, $langcode));
1481
    if ($langiso !== 'en') {
1482
        return $langiso;
1483
    }
1484
 
1485
    // Where we cannot determine the value from lang config, use the first two characters from the lang code.
1486
    return substr($langcode, 0, 2);
1487
}
1488
 
1489
/**
1490
 * Return a string containing 'lang', xml:lang and optionally 'dir' HTML attributes.
1491
 *
1492
 * Internationalisation, for print_header and backup/restorelib.
1493
 *
1494
 * @param bool $dir Default false
1495
 * @return string Attributes
1496
 */
1497
function get_html_lang($dir = false) {
1498
    global $CFG;
1499
 
1500
    $currentlang = current_language();
1501
    if (isset($CFG->lang) && $currentlang !== $CFG->lang && !get_string_manager()->translation_exists($currentlang)) {
1502
        // Use the default site language when the current language is not available.
1503
        $currentlang = $CFG->lang;
1504
        // Fix the current language.
1505
        fix_current_language($currentlang);
1506
    }
1507
 
1508
    $direction = '';
1509
    if ($dir) {
1510
        if (right_to_left()) {
1511
            $direction = ' dir="rtl"';
1512
        } else {
1513
            $direction = ' dir="ltr"';
1514
        }
1515
    }
1516
 
1517
    // Accessibility: added the 'lang' attribute to $direction, used in theme <html> tag.
1518
    $language = get_html_lang_attribute_value($currentlang);
1519
    @header('Content-Language: '.$language);
1520
    return ($direction.' lang="'.$language.'" xml:lang="'.$language.'"');
1521
}
1522
 
1523
 
1524
// STANDARD WEB PAGE PARTS.
1525
 
1526
/**
1527
 * Send the HTTP headers that Moodle requires.
1528
 *
1529
 * There is a backwards compatibility hack for legacy code
1530
 * that needs to add custom IE compatibility directive.
1531
 *
1532
 * Example:
1533
 * <code>
1534
 * if (!isset($CFG->additionalhtmlhead)) {
1535
 *     $CFG->additionalhtmlhead = '';
1536
 * }
1537
 * $CFG->additionalhtmlhead .= '<meta http-equiv="X-UA-Compatible" content="IE=8" />';
1538
 * header('X-UA-Compatible: IE=8');
1539
 * echo $OUTPUT->header();
1540
 * </code>
1541
 *
1542
 * Please note the $CFG->additionalhtmlhead alone might not work,
1543
 * you should send the IE compatibility header() too.
1544
 *
1545
 * @param string $contenttype
1546
 * @param bool $cacheable Can this page be cached on back?
1547
 * @return void, sends HTTP headers
1548
 */
1549
function send_headers($contenttype, $cacheable = true) {
1550
    global $CFG;
1551
 
1552
    @header('Content-Type: ' . $contenttype);
1553
    @header('Content-Script-Type: text/javascript');
1554
    @header('Content-Style-Type: text/css');
1555
 
1556
    if (empty($CFG->additionalhtmlhead) or stripos($CFG->additionalhtmlhead, 'X-UA-Compatible') === false) {
1557
        @header('X-UA-Compatible: IE=edge');
1558
    }
1559
 
1560
    if ($cacheable) {
1561
        // Allow caching on "back" (but not on normal clicks).
1562
        @header('Cache-Control: private, pre-check=0, post-check=0, max-age=0, no-transform');
1563
        @header('Pragma: no-cache');
1564
        @header('Expires: ');
1565
    } else {
1566
        // Do everything we can to always prevent clients and proxies caching.
1441 ariadna 1567
        @header('Cache-Control: no-store, no-cache, must-revalidate, no-transform');
1 efrain 1568
        @header('Pragma: no-cache');
1569
        @header('Expires: Mon, 20 Aug 1969 09:23:00 GMT');
1570
        @header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
1571
    }
1572
    @header('Accept-Ranges: none');
1573
 
1574
    // The Moodle app must be allowed to embed content always.
1575
    if (empty($CFG->allowframembedding) && !core_useragent::is_moodle_app()) {
1576
        @header('X-Frame-Options: sameorigin');
1577
    }
1578
 
1579
    // If referrer policy is set, add a referrer header.
1580
    if (!empty($CFG->referrerpolicy) && ($CFG->referrerpolicy !== 'default')) {
1581
        @header('Referrer-Policy: ' . $CFG->referrerpolicy);
1582
    }
1583
}
1584
 
1585
/**
1586
 * Return the right arrow with text ('next'), and optionally embedded in a link.
1587
 *
1588
 * @param string $text HTML/plain text label (set to blank only for breadcrumb separator cases).
1589
 * @param string $url An optional link to use in a surrounding HTML anchor.
1590
 * @param bool $accesshide True if text should be hidden (for screen readers only).
1591
 * @param string $addclass Additional class names for the link, or the arrow character.
1592
 * @return string HTML string.
1593
 */
1594
function link_arrow_right($text, $url='', $accesshide=false, $addclass='', $addparams = []) {
1595
    global $OUTPUT; // TODO: move to output renderer.
1596
    $arrowclass = 'arrow ';
1597
    if (!$url) {
1598
        $arrowclass .= $addclass;
1599
    }
1600
    $arrow = '<span class="'.$arrowclass.'" aria-hidden="true">'.$OUTPUT->rarrow().'</span>';
1601
    $htmltext = '';
1602
    if ($text) {
1603
        $htmltext = '<span class="arrow_text">'.$text.'</span>&nbsp;';
1604
        if ($accesshide) {
1605
            $htmltext = get_accesshide($htmltext);
1606
        }
1607
    }
1608
    if ($url) {
1609
        $class = 'arrow_link';
1610
        if ($addclass) {
1611
            $class .= ' '.$addclass;
1612
        }
1613
 
1614
        $linkparams = [
1615
            'class' => $class,
1616
            'href' => $url,
1617
            'title' => preg_replace('/<.*?>/', '', $text),
1618
        ];
1619
 
1620
        $linkparams += $addparams;
1621
 
1622
        return html_writer::link($url, $htmltext . $arrow, $linkparams);
1623
    }
1624
    return $htmltext.$arrow;
1625
}
1626
 
1627
/**
1628
 * Return the left arrow with text ('previous'), and optionally embedded in a link.
1629
 *
1630
 * @param string $text HTML/plain text label (set to blank only for breadcrumb separator cases).
1631
 * @param string $url An optional link to use in a surrounding HTML anchor.
1632
 * @param bool $accesshide True if text should be hidden (for screen readers only).
1633
 * @param string $addclass Additional class names for the link, or the arrow character.
1634
 * @return string HTML string.
1635
 */
1636
function link_arrow_left($text, $url='', $accesshide=false, $addclass='', $addparams = []) {
1637
    global $OUTPUT; // TODO: move to utput renderer.
1638
    $arrowclass = 'arrow ';
1639
    if (! $url) {
1640
        $arrowclass .= $addclass;
1641
    }
1642
    $arrow = '<span class="'.$arrowclass.'" aria-hidden="true">'.$OUTPUT->larrow().'</span>';
1643
    $htmltext = '';
1644
    if ($text) {
1645
        $htmltext = '&nbsp;<span class="arrow_text">'.$text.'</span>';
1646
        if ($accesshide) {
1647
            $htmltext = get_accesshide($htmltext);
1648
        }
1649
    }
1650
    if ($url) {
1651
        $class = 'arrow_link';
1652
        if ($addclass) {
1653
            $class .= ' '.$addclass;
1654
        }
1655
 
1656
        $linkparams = [
1657
            'class' => $class,
1658
            'href' => $url,
1659
            'title' => preg_replace('/<.*?>/', '', $text),
1660
        ];
1661
 
1662
        $linkparams += $addparams;
1663
 
1664
        return html_writer::link($url, $arrow . $htmltext, $linkparams);
1665
    }
1666
    return $arrow.$htmltext;
1667
}
1668
 
1669
/**
1670
 * Return a HTML element with the class "accesshide", for accessibility.
1671
 *
1672
 * Please use cautiously - where possible, text should be visible!
1673
 *
1674
 * @param string $text Plain text.
1675
 * @param string $elem Lowercase element name, default "span".
1676
 * @param string $class Additional classes for the element.
1677
 * @param string $attrs Additional attributes string in the form, "name='value' name2='value2'"
1678
 * @return string HTML string.
1679
 */
1680
function get_accesshide($text, $elem='span', $class='', $attrs='') {
1681
    return "<$elem class=\"accesshide $class\" $attrs>$text</$elem>";
1682
}
1683
 
1684
/**
1685
 * Return the breadcrumb trail navigation separator.
1686
 *
1687
 * @return string HTML string.
1688
 */
1689
function get_separator() {
1690
    // Accessibility: the 'hidden' slash is preferred for screen readers.
1691
    return ' '.link_arrow_right($text='/', $url='', $accesshide=true, 'sep').' ';
1692
}
1693
 
1694
/**
1695
 * Print (or return) a collapsible region, that has a caption that can be clicked to expand or collapse the region.
1696
 *
1697
 * If JavaScript is off, then the region will always be expanded.
1698
 *
1699
 * @param string $contents the contents of the box.
1700
 * @param string $classes class names added to the div that is output.
1701
 * @param string $id id added to the div that is output. Must not be blank.
1702
 * @param string $caption text displayed at the top. Clicking on this will cause the region to expand or contract.
1703
 * @param string $userpref the name of the user preference that stores the user's preferred default state.
1704
 *      (May be blank if you do not wish the state to be persisted.
1705
 * @param boolean $default Initial collapsed state to use if the user_preference it not set.
1706
 * @param boolean $return if true, return the HTML as a string, rather than printing it.
1707
 * @return string|void If $return is false, returns nothing, otherwise returns a string of HTML.
1708
 */
1709
function print_collapsible_region($contents, $classes, $id, $caption, $userpref = '', $default = false, $return = false) {
1710
    $output  = print_collapsible_region_start($classes, $id, $caption, $userpref, $default, true);
1711
    $output .= $contents;
1712
    $output .= print_collapsible_region_end(true);
1713
 
1714
    if ($return) {
1715
        return $output;
1716
    } else {
1717
        echo $output;
1718
    }
1719
}
1720
 
1721
/**
1722
 * Print (or return) the start of a collapsible region
1723
 *
1724
 * The collapsibleregion has a caption that can be clicked to expand or collapse the region. If JavaScript is off, then the region
1725
 * will always be expanded.
1726
 *
1727
 * @param string $classes class names added to the div that is output.
1728
 * @param string $id id added to the div that is output. Must not be blank.
1729
 * @param string $caption text displayed at the top. Clicking on this will cause the region to expand or contract.
1730
 * @param string $userpref the name of the user preference that stores the user's preferred default state.
1731
 *      (May be blank if you do not wish the state to be persisted.
1732
 * @param boolean $default Initial collapsed state to use if the user_preference it not set.
1733
 * @param boolean $return if true, return the HTML as a string, rather than printing it.
1734
 * @param string $extracontent the extra content will show next to caption, eg.Help icon.
1735
 * @return string|void if $return is false, returns nothing, otherwise returns a string of HTML.
1736
 */
1737
function print_collapsible_region_start($classes, $id, $caption, $userpref = '', $default = false, $return = false,
1738
        $extracontent = null) {
1739
    global $PAGE;
1740
 
1741
    // Work out the initial state.
1742
    if (!empty($userpref) and is_string($userpref)) {
1743
        $collapsed = get_user_preferences($userpref, $default);
1744
    } else {
1745
        $collapsed = $default;
1746
        $userpref = false;
1747
    }
1748
 
1749
    if ($collapsed) {
1750
        $classes .= ' collapsed';
1751
    }
1752
 
1753
    $output = '';
1754
    $output .= '<div id="' . $id . '" class="collapsibleregion ' . $classes . '">';
1755
    $output .= '<div id="' . $id . '_sizer">';
1756
    $output .= '<div id="' . $id . '_caption" class="collapsibleregioncaption">';
1757
    $output .= $caption . ' </div>';
1758
    if ($extracontent) {
1759
        $output .= html_writer::div($extracontent, 'collapsibleregionextracontent');
1760
    }
1761
    $output .= '<div id="' . $id . '_inner" class="collapsibleregioninner">';
1762
    $PAGE->requires->js_init_call('M.util.init_collapsible_region', array($id, $userpref, get_string('clicktohideshow')));
1763
 
1764
    if ($return) {
1765
        return $output;
1766
    } else {
1767
        echo $output;
1768
    }
1769
}
1770
 
1771
/**
1772
 * Close a region started with print_collapsible_region_start.
1773
 *
1774
 * @param boolean $return if true, return the HTML as a string, rather than printing it.
1775
 * @return string|void if $return is false, returns nothing, otherwise returns a string of HTML.
1776
 */
1777
function print_collapsible_region_end($return = false) {
1778
    $output = '</div></div></div>';
1779
 
1780
    if ($return) {
1781
        return $output;
1782
    } else {
1783
        echo $output;
1784
    }
1785
}
1786
 
1787
/**
1788
 * Print a specified group's avatar.
1789
 *
1790
 * @param array|stdClass $group A single {@link group} object OR array of groups.
1791
 * @param int $courseid The course ID.
1792
 * @param boolean $large Default small picture, or large.
1793
 * @param boolean $return If false print picture, otherwise return the output as string
1794
 * @param boolean $link Enclose image in a link to view specified course?
1795
 * @param boolean $includetoken Whether to use a user token when displaying this group image.
1796
 *                True indicates to generate a token for current user, and integer value indicates to generate a token for the
1797
 *                user whose id is the value indicated.
1798
 *                If the group picture is included in an e-mail or some other location where the audience is a specific
1799
 *                user who will not be logged in when viewing, then we use a token to authenticate the user.
1800
 * @return string|void Depending on the setting of $return
1801
 */
1802
function print_group_picture($group, $courseid, $large = false, $return = false, $link = true, $includetoken = false) {
1803
    global $CFG;
1804
 
1805
    if (is_array($group)) {
1806
        $output = '';
1807
        foreach ($group as $g) {
1808
            $output .= print_group_picture($g, $courseid, $large, true, $link, $includetoken);
1809
        }
1810
        if ($return) {
1811
            return $output;
1812
        } else {
1813
            echo $output;
1814
            return;
1815
        }
1816
    }
1817
 
1818
    $pictureurl = get_group_picture_url($group, $courseid, $large, $includetoken);
1819
 
1820
    // If there is no picture, do nothing.
1821
    if (!isset($pictureurl)) {
1822
        return;
1823
    }
1824
 
1825
    $context = context_course::instance($courseid);
1826
 
1827
    $groupname = s($group->name);
1828
    $pictureimage = html_writer::img($pictureurl, $groupname, ['title' => $groupname]);
1829
 
1830
    $output = '';
1831
    if ($link or has_capability('moodle/site:accessallgroups', $context)) {
1832
        $linkurl = new moodle_url('/user/index.php', ['id' => $courseid, 'group' => $group->id]);
1833
        $output .= html_writer::link($linkurl, $pictureimage);
1834
    } else {
1835
        $output .= $pictureimage;
1836
    }
1837
 
1838
    if ($return) {
1839
        return $output;
1840
    } else {
1841
        echo $output;
1842
    }
1843
}
1844
 
1845
/**
1846
 * Return the url to the group picture.
1847
 *
1848
 * @param  stdClass $group A group object.
1849
 * @param  int $courseid The course ID for the group.
1850
 * @param  bool $large A large or small group picture? Default is small.
1851
 * @param  boolean $includetoken Whether to use a user token when displaying this group image.
1852
 *                 True indicates to generate a token for current user, and integer value indicates to generate a token for the
1853
 *                 user whose id is the value indicated.
1854
 *                 If the group picture is included in an e-mail or some other location where the audience is a specific
1855
 *                 user who will not be logged in when viewing, then we use a token to authenticate the user.
1856
 * @return ?moodle_url Returns the url for the group picture.
1857
 */
1858
function get_group_picture_url($group, $courseid, $large = false, $includetoken = false) {
1859
    global $CFG;
1860
 
1861
    $context = context_course::instance($courseid);
1862
 
1863
    // If there is no picture, do nothing.
1864
    if (!$group->picture) {
1865
        return;
1866
    }
1867
 
1868
    if ($large) {
1869
        $file = 'f1';
1870
    } else {
1871
        $file = 'f2';
1872
    }
1873
 
1874
    $grouppictureurl = moodle_url::make_pluginfile_url(
1875
            $context->id, 'group', 'icon', $group->id, '/', $file, false, $includetoken);
1876
    $grouppictureurl->param('rev', $group->picture);
1877
    return $grouppictureurl;
1878
}
1879
 
1880
 
1881
/**
1882
 * Display a recent activity note
1883
 *
1884
 * @staticvar string $strftimerecent
1885
 * @param int $time A timestamp int.
1886
 * @param stdClass $user A user object from the database.
1887
 * @param string $text Text for display for the note
1888
 * @param string $link The link to wrap around the text
1889
 * @param bool $return If set to true the HTML is returned rather than echo'd
1890
 * @param string $viewfullnames
1891
 * @return ?string If $retrun was true returns HTML for a recent activity notice.
1892
 */
1893
function print_recent_activity_note($time, $user, $text, $link, $return=false, $viewfullnames=null) {
1894
    static $strftimerecent = null;
1895
    $output = '';
1896
 
1897
    if (is_null($viewfullnames)) {
1898
        $context = context_system::instance();
1899
        $viewfullnames = has_capability('moodle/site:viewfullnames', $context);
1900
    }
1901
 
1902
    if (is_null($strftimerecent)) {
1903
        $strftimerecent = get_string('strftimerecent');
1904
    }
1905
 
1906
    $output .= '<div class="head">';
1907
    $output .= '<div class="date">'.userdate($time, $strftimerecent).'</div>';
1908
    $output .= '<div class="name">'.fullname($user, $viewfullnames).'</div>';
1909
    $output .= '</div>';
1910
    $output .= '<div class="info"><a href="'.$link.'">'.format_string($text, true).'</a></div>';
1911
 
1912
    if ($return) {
1913
        return $output;
1914
    } else {
1915
        echo $output;
1916
    }
1917
}
1918
 
1919
/**
1920
 * Returns a popup menu with course activity modules
1921
 *
1922
 * Given a course this function returns a small popup menu with all the course activity modules in it, as a navigation menu
1923
 * outputs a simple list structure in XHTML.
1924
 * The data is taken from the serialised array stored in the course record.
1925
 *
1926
 * @param stdClass $course A course object.
1927
 * @param array $sections
1928
 * @param course_modinfo $modinfo
1929
 * @param string $strsection
1930
 * @param string $strjumpto
1931
 * @param int $width
1932
 * @param string $cmid
1933
 * @return string The HTML block
1934
 */
1935
function navmenulist($course, $sections, $modinfo, $strsection, $strjumpto, $width=50, $cmid=0) {
1936
 
1937
    global $CFG, $OUTPUT;
1938
 
1939
    $section = -1;
1940
    $menu = array();
1941
    $doneheading = false;
1942
 
1943
    $courseformatoptions = course_get_format($course)->get_format_options();
1944
    $coursecontext = context_course::instance($course->id);
1945
 
1946
    $menu[] = '<ul class="navmenulist"><li class="jumpto section"><span>'.$strjumpto.'</span><ul>';
1947
    foreach ($modinfo->cms as $mod) {
1948
        if (!$mod->has_view()) {
1949
            // Don't show modules which you can't link to!
1950
            continue;
1951
        }
1952
 
1953
        // For course formats using 'numsections' do not show extra sections.
1954
        if (isset($courseformatoptions['numsections']) && $mod->sectionnum > $courseformatoptions['numsections']) {
1955
            break;
1956
        }
1957
 
1958
        if (!$mod->uservisible) { // Do not icnlude empty sections at all.
1959
            continue;
1960
        }
1961
 
1962
        if ($mod->sectionnum >= 0 and $section != $mod->sectionnum) {
1963
            $thissection = $sections[$mod->sectionnum];
1964
 
1965
            if ($thissection->visible or
1966
                    (isset($courseformatoptions['hiddensections']) and !$courseformatoptions['hiddensections']) or
1967
                    has_capability('moodle/course:viewhiddensections', $coursecontext)) {
1968
                $thissection->summary = strip_tags(format_string($thissection->summary, true));
1969
                if (!$doneheading) {
1970
                    $menu[] = '</ul></li>';
1971
                }
1972
                if ($course->format == 'weeks' or empty($thissection->summary)) {
1973
                    $item = $strsection ." ". $mod->sectionnum;
1974
                } else {
1975
                    if (core_text::strlen($thissection->summary) < ($width-3)) {
1976
                        $item = $thissection->summary;
1977
                    } else {
1978
                        $item = core_text::substr($thissection->summary, 0, $width).'...';
1979
                    }
1980
                }
1981
                $menu[] = '<li class="section"><span>'.$item.'</span>';
1982
                $menu[] = '<ul>';
1983
                $doneheading = true;
1984
 
1985
                $section = $mod->sectionnum;
1986
            } else {
1987
                // No activities from this hidden section shown.
1988
                continue;
1989
            }
1990
        }
1991
 
1992
        $url = $mod->modname .'/view.php?id='. $mod->id;
1993
        $mod->name = strip_tags(format_string($mod->name ,true));
1994
        if (core_text::strlen($mod->name) > ($width+5)) {
1995
            $mod->name = core_text::substr($mod->name, 0, $width).'...';
1996
        }
1997
        if (!$mod->visible) {
1998
            $mod->name = '('.$mod->name.')';
1999
        }
2000
        $class = 'activity '.$mod->modname;
2001
        $class .= ($cmid == $mod->id) ? ' selected' : '';
2002
        $menu[] = '<li class="'.$class.'">'.
2003
                  $OUTPUT->image_icon('monologo', '', $mod->modname).
2004
                  '<a href="'.$CFG->wwwroot.'/mod/'.$url.'">'.$mod->name.'</a></li>';
2005
    }
2006
 
2007
    if ($doneheading) {
2008
        $menu[] = '</ul></li>';
2009
    }
2010
    $menu[] = '</ul></li></ul>';
2011
 
2012
    return implode("\n", $menu);
2013
}
2014
 
2015
/**
2016
 * Print an error to STDOUT and exit with a non-zero code. For commandline scripts.
2017
 *
2018
 * Default errorcode is 1.
2019
 *
2020
 * Very useful for perl-like error-handling:
2021
 * do_somethting() or mdie("Something went wrong");
2022
 *
2023
 * @param string  $msg       Error message
2024
 * @param integer $errorcode Error code to emit
2025
 */
2026
function mdie($msg='', $errorcode=1) {
2027
    trigger_error($msg);
2028
    exit($errorcode);
2029
}
2030
 
2031
/**
2032
 * Print a message and exit.
2033
 *
2034
 * @param string $message The message to print in the notice
2035
 * @param moodle_url|string $link The link to use for the continue button
2036
 * @param object $course A course object. Unused.
2037
 * @return void This function simply exits
2038
 */
2039
function notice ($message, $link='', $course=null) {
2040
    global $PAGE, $OUTPUT;
2041
 
2042
    $message = clean_text($message);   // In case nasties are in here.
2043
 
2044
    if (CLI_SCRIPT) {
2045
        echo("!!$message!!\n");
2046
        exit(1); // No success.
2047
    }
2048
 
2049
    if (!$PAGE->headerprinted) {
2050
        // Header not yet printed.
2051
        $PAGE->set_title(get_string('notice'));
2052
        echo $OUTPUT->header();
2053
    } else {
2054
        echo $OUTPUT->container_end_all(false);
2055
    }
2056
 
2057
    echo $OUTPUT->box($message, 'generalbox', 'notice');
2058
    echo $OUTPUT->continue_button($link);
2059
 
2060
    echo $OUTPUT->footer();
2061
    exit(1); // General error code.
2062
}
2063
 
2064
/**
2065
 * Redirects the user to another page, after printing a notice.
2066
 *
2067
 * This function calls the OUTPUT redirect method, echo's the output and then dies to ensure nothing else happens.
2068
 *
2069
 * <strong>Good practice:</strong> You should call this method before starting page
2070
 * output by using any of the OUTPUT methods.
2071
 *
2072
 * @param moodle_url|string $url A moodle_url to redirect to. Strings are not to be trusted!
2073
 * @param string $message The message to display to the user
2074
 * @param int $delay The delay before redirecting
2075
 * @param string $messagetype The type of notification to show the message in. See constants on \core\output\notification.
2076
 * @throws moodle_exception
2077
 */
1441 ariadna 2078
function redirect($url, $message='', $delay=null, $messagetype = \core\output\notification::NOTIFY_INFO): Never {
1 efrain 2079
    global $OUTPUT, $PAGE, $CFG;
2080
 
2081
    if (CLI_SCRIPT or AJAX_SCRIPT) {
2082
        // This is wrong - developers should not use redirect in these scripts but it should not be very likely.
2083
        throw new moodle_exception('redirecterrordetected', 'error');
2084
    }
2085
 
2086
    if ($delay === null) {
2087
        $delay = -1;
2088
    }
2089
 
2090
    // Prevent debug errors - make sure context is properly initialised.
2091
    if ($PAGE) {
2092
        $PAGE->set_context(null);
2093
        $PAGE->set_pagelayout('redirect');  // No header and footer needed.
2094
        $PAGE->set_title(get_string('pageshouldredirect', 'moodle'));
2095
    }
2096
 
2097
    if ($url instanceof moodle_url) {
2098
        $url = $url->out(false);
2099
    }
2100
 
2101
    $debugdisableredirect = false;
2102
    do {
2103
        if (defined('DEBUGGING_PRINTED')) {
2104
            // Some debugging already printed, no need to look more.
2105
            $debugdisableredirect = true;
2106
            break;
2107
        }
2108
 
2109
        if (core_useragent::is_msword()) {
2110
            // Clicking a URL from MS Word sends a request to the server without cookies. If that
2111
            // causes a redirect Word will open a browser pointing the new URL. If not, the URL that
2112
            // was clicked is opened. Because the request from Word is without cookies, it almost
2113
            // always results in a redirect to the login page, even if the user is logged in in their
2114
            // browser. This is not what we want, so prevent the redirect for requests from Word.
2115
            $debugdisableredirect = true;
2116
            break;
2117
        }
2118
 
2119
        if (empty($CFG->debugdisplay) or empty($CFG->debug)) {
2120
            // No errors should be displayed.
2121
            break;
2122
        }
2123
 
2124
        if (!function_exists('error_get_last') or !$lasterror = error_get_last()) {
2125
            break;
2126
        }
2127
 
2128
        if (!($lasterror['type'] & $CFG->debug)) {
2129
            // Last error not interesting.
2130
            break;
2131
        }
2132
 
2133
        // Watch out here, @hidden() errors are returned from error_get_last() too.
2134
        if (headers_sent()) {
2135
            // We already started printing something - that means errors likely printed.
2136
            $debugdisableredirect = true;
2137
            break;
2138
        }
2139
 
2140
        if (ob_get_level() and ob_get_contents()) {
2141
            // There is something waiting to be printed, hopefully it is the errors,
2142
            // but it might be some error hidden by @ too - such as the timezone mess from setup.php.
2143
            $debugdisableredirect = true;
2144
            break;
2145
        }
2146
    } while (false);
2147
 
2148
    // Technically, HTTP/1.1 requires Location: header to contain the absolute path.
2149
    // (In practice browsers accept relative paths - but still, might as well do it properly.)
2150
    // This code turns relative into absolute.
2151
    if (!preg_match('|^[a-z]+:|i', $url)) {
2152
        // Get host name http://www.wherever.com.
2153
        $hostpart = preg_replace('|^(.*?[^:/])/.*$|', '$1', $CFG->wwwroot);
2154
        if (preg_match('|^/|', $url)) {
2155
            // URLs beginning with / are relative to web server root so we just add them in.
2156
            $url = $hostpart.$url;
2157
        } else {
2158
            // URLs not beginning with / are relative to path of current script, so add that on.
2159
            $url = $hostpart.preg_replace('|\?.*$|', '', me()).'/../'.$url;
2160
        }
2161
        // Replace all ..s.
2162
        while (true) {
2163
            $newurl = preg_replace('|/(?!\.\.)[^/]*/\.\./|', '/', $url);
2164
            if ($newurl == $url) {
2165
                break;
2166
            }
2167
            $url = $newurl;
2168
        }
2169
    }
2170
 
2171
    // Sanitise url - we can not rely on moodle_url or our URL cleaning
2172
    // because they do not support all valid external URLs.
2173
    $url = preg_replace('/[\x00-\x1F\x7F]/', '', $url);
2174
    $url = str_replace('"', '%22', $url);
2175
    $encodedurl = preg_replace("/\&(?![a-zA-Z0-9#]{1,8};)/", "&amp;", $url);
2176
    $encodedurl = preg_replace('/^.*href="([^"]*)".*$/', "\\1", clean_text('<a href="'.$encodedurl.'" />', FORMAT_HTML));
2177
    $url = str_replace('&amp;', '&', $encodedurl);
2178
 
2179
    if (!empty($message)) {
2180
        if (!$debugdisableredirect && !headers_sent()) {
2181
            // A message has been provided, and the headers have not yet been sent.
2182
            // Display the message as a notification on the subsequent page.
2183
            \core\notification::add($message, $messagetype);
2184
            $message = null;
2185
            $delay = 0;
2186
        } else {
2187
            if ($delay === -1 || !is_numeric($delay)) {
2188
                $delay = 3;
2189
            }
2190
            $message = clean_text($message);
2191
        }
2192
    } else {
2193
        $message = get_string('pageshouldredirect');
2194
        $delay = 0;
2195
    }
2196
 
2197
    // Make sure the session is closed properly, this prevents problems in IIS
2198
    // and also some potential PHP shutdown issues.
2199
    \core\session\manager::write_close();
2200
 
2201
    if ($delay == 0 && !$debugdisableredirect && !headers_sent()) {
2202
 
2203
        // This helps when debugging redirect issues like loops and it is not clear
2204
        // which layer in the stack sent the redirect header. If debugging is on
2205
        // then the file and line is also shown.
2206
        $redirectby = 'Moodle';
2207
        if (debugging('', DEBUG_DEVELOPER)) {
2208
            $origin = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
2209
            $redirectby .= ' /' . str_replace($CFG->dirroot . '/', '', $origin['file']) . ':' . $origin['line'];
2210
        }
2211
        @header("X-Redirect-By: $redirectby");
2212
 
2213
        // 302 might not work for POST requests, 303 is ignored by obsolete clients.
2214
        @header($_SERVER['SERVER_PROTOCOL'] . ' 303 See Other');
2215
        @header('Location: '.$url);
2216
        echo bootstrap_renderer::plain_redirect_message($encodedurl);
2217
        exit;
2218
    }
2219
 
2220
    // Include a redirect message, even with a HTTP redirect, because that is recommended practice.
2221
    if ($PAGE) {
2222
        $CFG->docroot = false; // To prevent the link to moodle docs from being displayed on redirect page.
2223
        echo $OUTPUT->redirect_message($encodedurl, $message, $delay, $debugdisableredirect, $messagetype);
2224
        exit;
2225
    } else {
2226
        echo bootstrap_renderer::early_redirect_message($encodedurl, $message, $delay);
2227
        exit;
2228
    }
2229
}
2230
 
2231
/**
2232
 * Given an email address, this function will return an obfuscated version of it.
2233
 *
2234
 * @param string $email The email address to obfuscate
2235
 * @return string The obfuscated email address
2236
 */
2237
function obfuscate_email($email) {
2238
    $i = 0;
2239
    $length = strlen($email);
2240
    $obfuscated = '';
2241
    while ($i < $length) {
2242
        if (rand(0, 2) && $email[$i]!='@') { // MDL-20619 some browsers have problems unobfuscating @.
2243
            $obfuscated.='%'.dechex(ord($email[$i]));
2244
        } else {
2245
            $obfuscated.=$email[$i];
2246
        }
2247
        $i++;
2248
    }
2249
    return $obfuscated;
2250
}
2251
 
2252
/**
2253
 * This function takes some text and replaces about half of the characters
2254
 * with HTML entity equivalents.   Return string is obviously longer.
2255
 *
2256
 * @param string $plaintext The text to be obfuscated
2257
 * @return string The obfuscated text
2258
 */
2259
function obfuscate_text($plaintext) {
2260
    $i=0;
2261
    $length = core_text::strlen($plaintext);
2262
    $obfuscated='';
2263
    $prevobfuscated = false;
2264
    while ($i < $length) {
2265
        $char = core_text::substr($plaintext, $i, 1);
2266
        $ord = core_text::utf8ord($char);
2267
        $numerical = ($ord >= ord('0')) && ($ord <= ord('9'));
2268
        if ($prevobfuscated and $numerical ) {
2269
            $obfuscated.='&#'.$ord.';';
2270
        } else if (rand(0, 2)) {
2271
            $obfuscated.='&#'.$ord.';';
2272
            $prevobfuscated = true;
2273
        } else {
2274
            $obfuscated.=$char;
2275
            $prevobfuscated = false;
2276
        }
2277
        $i++;
2278
    }
2279
    return $obfuscated;
2280
}
2281
 
2282
/**
2283
 * This function uses the {@link obfuscate_email()} and {@link obfuscate_text()}
2284
 * to generate a fully obfuscated email link, ready to use.
2285
 *
2286
 * @param string $email The email address to display
2287
 * @param string $label The text to displayed as hyperlink to $email
2288
 * @param boolean $dimmed If true then use css class 'dimmed' for hyperlink
2289
 * @param string $subject The subject of the email in the mailto link
2290
 * @param string $body The content of the email in the mailto link
2291
 * @return string The obfuscated mailto link
2292
 */
2293
function obfuscate_mailto($email, $label='', $dimmed=false, $subject = '', $body = '') {
2294
 
2295
    if (empty($label)) {
2296
        $label = $email;
2297
    }
2298
 
2299
    $label = obfuscate_text($label);
2300
    $email = obfuscate_email($email);
2301
    $mailto = obfuscate_text('mailto');
2302
    $url = new moodle_url("mailto:$email");
2303
    $attrs = array();
2304
 
2305
    if (!empty($subject)) {
2306
        $url->param('subject', format_string($subject));
2307
    }
2308
    if (!empty($body)) {
2309
        $url->param('body', format_string($body));
2310
    }
2311
 
2312
    // Use the obfuscated mailto.
2313
    $url = preg_replace('/^mailto/', $mailto, $url->out());
2314
 
2315
    if ($dimmed) {
2316
        $attrs['title'] = get_string('emaildisable');
2317
        $attrs['class'] = 'dimmed';
2318
    }
2319
 
2320
    return html_writer::link($url, $label, $attrs);
2321
}
2322
 
2323
/**
2324
 * This function is used to rebuild the <nolink> tag because some formats (PLAIN and WIKI)
2325
 * will transform it to html entities
2326
 *
2327
 * @param string $text Text to search for nolink tag in
2328
 * @return string
2329
 */
2330
function rebuildnolinktag($text) {
2331
 
2332
    $text = preg_replace('/&lt;(\/*nolink)&gt;/i', '<$1>', $text);
2333
 
2334
    return $text;
2335
}
2336
 
2337
/**
2338
 * Prints a maintenance message from $CFG->maintenance_message or default if empty.
2339
 */
2340
function print_maintenance_message() {
2341
    global $CFG, $SITE, $PAGE, $OUTPUT;
2342
 
2343
    header($_SERVER['SERVER_PROTOCOL'] . ' 503 Moodle under maintenance');
2344
    header('Status: 503 Moodle under maintenance');
2345
    header('Retry-After: 300');
2346
 
2347
    $PAGE->set_pagetype('maintenance-message');
2348
    $PAGE->set_pagelayout('maintenance');
2349
    $PAGE->set_heading($SITE->fullname);
2350
    echo $OUTPUT->header();
2351
    echo $OUTPUT->heading(get_string('sitemaintenance', 'admin'));
2352
    if (isset($CFG->maintenance_message) and !html_is_blank($CFG->maintenance_message)) {
2353
        echo $OUTPUT->box_start('maintenance_message generalbox boxwidthwide boxaligncenter');
2354
        echo $CFG->maintenance_message;
2355
        echo $OUTPUT->box_end();
2356
    }
2357
    echo $OUTPUT->footer();
2358
    die;
2359
}
2360
 
2361
/**
2362
 * Returns a string containing a nested list, suitable for formatting into tabs with CSS.
2363
 *
2364
 * It is not recommended to use this function in Moodle 2.5 but it is left for backward
2365
 * compartibility.
2366
 *
2367
 * Example how to print a single line tabs:
2368
 * $rows = array(
2369
 *    new tabobject(...),
2370
 *    new tabobject(...)
2371
 * );
2372
 * echo $OUTPUT->tabtree($rows, $selectedid);
2373
 *
2374
 * Multiple row tabs may not look good on some devices but if you want to use them
2375
 * you can specify ->subtree for the active tabobject.
2376
 *
2377
 * @param array $tabrows An array of rows where each row is an array of tab objects
2378
 * @param string $selected  The id of the selected tab (whatever row it's on)
2379
 * @param array  $inactive  An array of ids of inactive tabs that are not selectable.
2380
 * @param array  $activated An array of ids of other tabs that are currently activated
2381
 * @param bool $return If true output is returned rather then echo'd
2382
 * @return string HTML output if $return was set to true.
2383
 */
2384
function print_tabs($tabrows, $selected = null, $inactive = null, $activated = null, $return = false) {
2385
    global $OUTPUT;
2386
 
2387
    $tabrows = array_reverse($tabrows);
2388
    $subtree = array();
2389
    foreach ($tabrows as $row) {
2390
        $tree = array();
2391
 
2392
        foreach ($row as $tab) {
2393
            $tab->inactive = is_array($inactive) && in_array((string)$tab->id, $inactive);
2394
            $tab->activated = is_array($activated) && in_array((string)$tab->id, $activated);
2395
            $tab->selected = (string)$tab->id == $selected;
2396
 
2397
            if ($tab->activated || $tab->selected) {
2398
                $tab->subtree = $subtree;
2399
            }
2400
            $tree[] = $tab;
2401
        }
2402
        $subtree = $tree;
2403
    }
2404
    $output = $OUTPUT->tabtree($subtree);
2405
    if ($return) {
2406
        return $output;
2407
    } else {
2408
        print $output;
2409
        return !empty($output);
2410
    }
2411
}
2412
 
2413
/**
2414
 * Alter debugging level for the current request,
2415
 * the change is not saved in database.
2416
 *
2417
 * @param int $level one of the DEBUG_* constants
2418
 * @param bool $debugdisplay
2419
 */
2420
function set_debugging($level, $debugdisplay = null) {
2421
    global $CFG;
2422
 
2423
    $CFG->debug = (int)$level;
2424
    $CFG->debugdeveloper = (($CFG->debug & DEBUG_DEVELOPER) === DEBUG_DEVELOPER);
2425
 
2426
    if ($debugdisplay !== null) {
2427
        $CFG->debugdisplay = (bool)$debugdisplay;
2428
    }
2429
}
2430
 
2431
/**
2432
 * Standard Debugging Function
2433
 *
2434
 * Returns true if the current site debugging settings are equal or above specified level.
2435
 * If passed a parameter it will emit a debugging notice similar to trigger_error(). The
2436
 * routing of notices is controlled by $CFG->debugdisplay
2437
 * eg use like this:
2438
 *
2439
 * 1)  debugging('a normal debug notice');
2440
 * 2)  debugging('something really picky', DEBUG_ALL);
2441
 * 3)  debugging('annoying debug message only for developers', DEBUG_DEVELOPER);
2442
 * 4)  if (debugging()) { perform extra debugging operations (do not use print or echo) }
2443
 *
2444
 * In code blocks controlled by debugging() (such as example 4)
2445
 * any output should be routed via debugging() itself, or the lower-level
2446
 * trigger_error() or error_log(). Using echo or print will break XHTML
2447
 * JS and HTTP headers.
2448
 *
2449
 * It is also possible to define NO_DEBUG_DISPLAY which redirects the message to error_log.
2450
 *
2451
 * @param string $message a message to print
2452
 * @param int $level the level at which this debugging statement should show
2453
 * @param array $backtrace use different backtrace
2454
 * @return bool
2455
 */
2456
function debugging($message = '', $level = DEBUG_NORMAL, $backtrace = null) {
2457
    global $CFG, $USER;
2458
 
2459
    $forcedebug = false;
2460
    if (!empty($CFG->debugusers) && $USER) {
2461
        $debugusers = explode(',', $CFG->debugusers);
2462
        $forcedebug = in_array($USER->id, $debugusers);
2463
    }
2464
 
2465
    if (!$forcedebug and (empty($CFG->debug) || ($CFG->debug != -1 and $CFG->debug < $level))) {
2466
        return false;
2467
    }
2468
 
2469
    if (!isset($CFG->debugdisplay)) {
2470
        $CFG->debugdisplay = ini_get_bool('display_errors');
2471
    }
2472
 
2473
    if ($message) {
2474
        if (!$backtrace) {
2475
            $backtrace = debug_backtrace();
2476
        }
2477
        $from = format_backtrace($backtrace, CLI_SCRIPT || NO_DEBUG_DISPLAY);
2478
        if (PHPUNIT_TEST) {
2479
            if (phpunit_util::debugging_triggered($message, $level, $from)) {
2480
                // We are inside test, the debug message was logged.
2481
                return true;
2482
            }
2483
        }
2484
 
2485
        if (NO_DEBUG_DISPLAY) {
2486
            // Script does not want any errors or debugging in output,
2487
            // we send the info to error log instead.
2488
            error_log('Debugging: ' . $message . ' in '. PHP_EOL . $from);
2489
        } else if ($forcedebug or $CFG->debugdisplay) {
2490
            if (!defined('DEBUGGING_PRINTED')) {
2491
                define('DEBUGGING_PRINTED', 1); // Indicates we have printed something.
2492
            }
2493
 
2494
            if (CLI_SCRIPT) {
2495
                echo "++ $message ++\n$from";
2496
            } else {
2497
                if (property_exists($CFG, 'debug_developer_debugging_as_error')) {
2498
                    $showaserror = $CFG->debug_developer_debugging_as_error;
2499
                } else {
2500
                    $showaserror = (bool) get_whoops();
2501
                }
2502
 
2503
                if ($showaserror) {
2504
                    trigger_error($message, E_USER_NOTICE);
2505
                } else {
2506
                    echo '<div class="notifytiny debuggingmessage" data-rel="debugging">', $message, $from, '</div>';
2507
                }
2508
            }
2509
        } else {
2510
            trigger_error($message . $from, E_USER_NOTICE);
2511
        }
2512
    }
2513
    return true;
2514
}
2515
 
2516
/**
2517
 * Outputs a HTML comment to the browser.
2518
 *
2519
 * This is used for those hard-to-debug pages that use bits from many different files in very confusing ways (e.g. blocks).
2520
 *
2521
 * <code>print_location_comment(__FILE__, __LINE__);</code>
2522
 *
2523
 * @param string $file
2524
 * @param integer $line
2525
 * @param boolean $return Whether to return or print the comment
2526
 * @return string|void Void unless true given as third parameter
2527
 */
2528
function print_location_comment($file, $line, $return = false) {
2529
    if ($return) {
2530
        return "<!-- $file at line $line -->\n";
2531
    } else {
2532
        echo "<!-- $file at line $line -->\n";
2533
    }
2534
}
2535
 
2536
 
2537
/**
2538
 * Returns true if the user is using a right-to-left language.
2539
 *
2540
 * @return boolean true if the current language is right-to-left (Hebrew, Arabic etc)
2541
 */
2542
function right_to_left() {
2543
    return (get_string('thisdirection', 'langconfig') === 'rtl');
2544
}
2545
 
2546
 
2547
/**
2548
 * Returns swapped left<=> right if in RTL environment.
2549
 *
2550
 * Part of RTL Moodles support.
2551
 *
2552
 * @param string $align align to check
2553
 * @return string
2554
 */
2555
function fix_align_rtl($align) {
2556
    if (!right_to_left()) {
2557
        return $align;
2558
    }
2559
    if ($align == 'left') {
2560
        return 'right';
2561
    }
2562
    if ($align == 'right') {
2563
        return 'left';
2564
    }
2565
    return $align;
2566
}
2567
 
2568
 
2569
/**
2570
 * Returns true if the page is displayed in a popup window.
2571
 *
2572
 * Gets the information from the URL parameter inpopup.
2573
 *
2574
 * @todo Use a central function to create the popup calls all over Moodle and
2575
 * In the moment only works with resources and probably questions.
2576
 *
2577
 * @return boolean
2578
 */
2579
function is_in_popup() {
2580
    $inpopup = optional_param('inpopup', '', PARAM_BOOL);
2581
 
2582
    return ($inpopup);
2583
}
2584
 
2585
/**
2586
 * Returns a localized sentence in the current language summarizing the current password policy
2587
 *
2588
 * @todo this should be handled by a function/method in the language pack library once we have a support for it
2589
 * @uses $CFG
2590
 * @return string
2591
 */
2592
function print_password_policy() {
2593
    global $CFG;
2594
 
2595
    $message = '';
2596
    if (!empty($CFG->passwordpolicy)) {
2597
        $messages = array();
2598
        if (!empty($CFG->minpasswordlength)) {
2599
            $messages[] = get_string('informminpasswordlength', 'auth', $CFG->minpasswordlength);
2600
        }
2601
        if (!empty($CFG->minpassworddigits)) {
2602
            $messages[] = get_string('informminpassworddigits', 'auth', $CFG->minpassworddigits);
2603
        }
2604
        if (!empty($CFG->minpasswordlower)) {
2605
            $messages[] = get_string('informminpasswordlower', 'auth', $CFG->minpasswordlower);
2606
        }
2607
        if (!empty($CFG->minpasswordupper)) {
2608
            $messages[] = get_string('informminpasswordupper', 'auth', $CFG->minpasswordupper);
2609
        }
2610
        if (!empty($CFG->minpasswordnonalphanum)) {
2611
            $messages[] = get_string('informminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum);
2612
        }
2613
 
2614
        // Fire any additional password policy functions from plugins.
2615
        // Callbacks must return an array of message strings.
2616
        $pluginsfunction = get_plugins_with_function('print_password_policy');
2617
        foreach ($pluginsfunction as $plugintype => $plugins) {
2618
            foreach ($plugins as $pluginfunction) {
2619
                $messages = array_merge($messages, $pluginfunction());
2620
            }
2621
        }
2622
 
2623
        $messages = join(', ', $messages); // This is ugly but we do not have anything better yet...
2624
        // Check if messages is empty before outputting any text.
2625
        if ($messages != '') {
2626
            $message = get_string('informpasswordpolicy', 'auth', $messages);
2627
        }
2628
    }
2629
    return $message;
2630
}
2631
 
2632
/**
2633
 * Get the value of a help string fully prepared for display in the current language.
2634
 *
2635
 * @param string $identifier The identifier of the string to search for.
2636
 * @param string $component The module the string is associated with.
2637
 * @param boolean $ajax Whether this help is called from an AJAX script.
2638
 *                This is used to influence text formatting and determines
2639
 *                which format to output the doclink in.
2640
 * @param string|object|array $a An object, string or number that can be used
2641
 *      within translation strings
2642
 * @return stdClass An object containing:
2643
 * - heading: Any heading that there may be for this help string.
2644
 * - text: The wiki-formatted help string.
2645
 * - doclink: An object containing a link, the linktext, and any additional
2646
 *            CSS classes to apply to that link. Only present if $ajax = false.
2647
 * - completedoclink: A text representation of the doclink. Only present if $ajax = true.
2648
 */
2649
function get_formatted_help_string($identifier, $component, $ajax = false, $a = null) {
2650
    global $CFG, $OUTPUT;
2651
    $sm = get_string_manager();
2652
 
2653
    // Do not rebuild caches here!
2654
    // Devs need to learn to purge all caches after any change or disable $CFG->langstringcache.
2655
 
2656
    $data = new stdClass();
2657
 
2658
    if ($sm->string_exists($identifier, $component)) {
2659
        $data->heading = format_string(get_string($identifier, $component));
2660
    } else {
2661
        // Gracefully fall back to an empty string.
2662
        $data->heading = '';
2663
    }
2664
 
2665
    if ($sm->string_exists($identifier . '_help', $component)) {
2666
        $options = new stdClass();
2667
        $options->trusted = false;
2668
        $options->noclean = false;
2669
        $options->filter = false;
2670
        $options->para = true;
2671
        $options->newlines = false;
2672
        $options->overflowdiv = !$ajax;
2673
 
2674
        // Should be simple wiki only MDL-21695.
2675
        $data->text = format_text(get_string($identifier.'_help', $component, $a), FORMAT_MARKDOWN, $options);
2676
 
2677
        $helplink = $identifier . '_link';
2678
        if ($sm->string_exists($helplink, $component)) {  // Link to further info in Moodle docs.
1441 ariadna 2679
            // The link is stored in a language file but should not be translated, use value for English.
2680
            $link = $sm->get_string($helplink, $component, null, 'en');
2681
            // The text 'More help' should be in the current language.
1 efrain 2682
            $linktext = get_string('morehelp');
2683
 
2684
            $data->doclink = new stdClass();
2685
            $url = new moodle_url(get_docs_url($link));
2686
            if ($ajax) {
2687
                $data->doclink->link = $url->out();
2688
                $data->doclink->linktext = $linktext;
2689
                $data->doclink->class = ($CFG->doctonewwindow) ? 'helplinkpopup' : '';
2690
            } else {
2691
                $data->completedoclink = html_writer::tag('div', $OUTPUT->doc_link($link, $linktext),
2692
                    array('class' => 'helpdoclink'));
2693
            }
2694
        }
2695
    } else {
2696
        $data->text = html_writer::tag('p',
2697
            html_writer::tag('strong', 'TODO') . ": missing help string [{$identifier}_help, {$component}]");
2698
    }
2699
    return $data;
2700
}