Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 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
namespace core\output;
18
 
19
use breadcrumb_navigation_node;
20
use cm_info;
21
use core\hook\output\after_http_headers;
22
use core_block\output\block_contents;
23
use core_block\output\block_move_target;
24
use core_completion\cm_completion_details;
25
use core\context;
26
use core_tag\output\taglist;
27
use core_text;
28
use core_useragent;
29
use core\check\check as check_check;
30
use core\check\result as check_result;
31
use core\context\system as context_system;
32
use core\context\course as context_course;
33
use core\di;
34
use core\exception\coding_exception;
35
use core\hook\manager as hook_manager;
36
use core\hook\output\after_standard_main_region_html_generation;
37
use core\hook\output\before_footer_html_generation;
38
use core\hook\output\before_html_attributes;
39
use core\hook\output\before_http_headers;
40
use core\hook\output\before_standard_footer_html_generation;
41
use core\hook\output\before_standard_top_of_body_html_generation;
42
use core\output\actions\component_action;
43
use core\output\actions\popup_action;
44
use core\output\local\properties\badge;
45
use core\plugin_manager;
46
use moodleform;
47
use moodle_page;
48
use moodle_url;
49
use navigation_node;
50
use rating;
51
use rating_manager;
52
use stdClass;
53
use HTML_QuickForm_element;
54
 
55
/**
56
 * The standard implementation of the core_renderer interface.
57
 *
58
 * @copyright 2009 Tim Hunt
59
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
60
 * @since Moodle 2.0
61
 * @package core
62
 * @category output
63
 */
64
class core_renderer extends renderer_base {
65
    /**
66
     * Do NOT use, please use <?php echo $OUTPUT->main_content() ?>
67
     * in layout files instead.
68
     * @deprecated
69
     * @var string used in {@see core_renderer::header()}.
70
     */
71
    const MAIN_CONTENT_TOKEN = '[MAIN CONTENT GOES HERE]';
72
 
73
    /**
74
     * @var string Used to pass information from {@see core_renderer::doctype()} to
75
     * @see core_renderer::standard_head_html()
76
     */
77
    protected $contenttype;
78
 
79
    /**
80
     * @var string Used by {@see core_renderer::redirect_message()} method to communicate
81
     * with {@see core_renderer::header()}.
82
     */
83
    protected $metarefreshtag = '';
84
 
85
    /**
86
     * @var string Unique token for the closing HTML
87
     */
88
    protected $unique_end_html_token; // phpcs:ignore moodle.NamingConventions.ValidVariableName.MemberNameUnderscore
89
 
90
    /**
91
     * @var string Unique token for performance information
92
     */
93
    protected $unique_performance_info_token; // phpcs:ignore moodle.NamingConventions.ValidVariableName.MemberNameUnderscore
94
 
95
    /**
96
     * @var string Unique token for the main content.
97
     */
98
    protected $unique_main_content_token; // phpcs:ignore moodle.NamingConventions.ValidVariableName.MemberNameUnderscore
99
 
100
    /** @var custom_menu_item language The language menu if created */
101
    protected $language = null;
102
 
103
    /** @var string The current selector for an element being streamed into */
104
    protected $currentselector = '';
105
 
106
    /** @var string The current element tag which is being streamed into */
107
    protected $currentelement = '';
108
 
109
    /**
110
     * Constructor
111
     *
112
     * @param moodle_page $page the page we are doing output for.
113
     * @param string $target one of rendering target constants
114
     */
115
    public function __construct(moodle_page $page, $target) {
116
        $this->opencontainers = $page->opencontainers;
117
        $this->page = $page;
118
        $this->target = $target;
119
 
120
        $this->unique_end_html_token = '%%ENDHTML-' . sesskey() . '%%';
121
        $this->unique_performance_info_token = '%%PERFORMANCEINFO-' . sesskey() . '%%';
122
        $this->unique_main_content_token = '[MAIN CONTENT GOES HERE - ' . sesskey() . ']';
123
    }
124
 
125
    /**
126
     * Get the DOCTYPE declaration that should be used with this page. Designed to
127
     * be called in theme layout.php files.
128
     *
129
     * @return string the DOCTYPE declaration that should be used.
130
     */
131
    public function doctype() {
132
        if ($this->page->theme->doctype === 'html5') {
133
            $this->contenttype = 'text/html; charset=utf-8';
134
            return "<!DOCTYPE html>\n";
135
        } else if ($this->page->theme->doctype === 'xhtml5') {
136
            $this->contenttype = 'application/xhtml+xml; charset=utf-8';
137
            return "<!DOCTYPE html>\n";
138
        } else {
139
            // legacy xhtml 1.0
140
            $this->contenttype = 'text/html; charset=utf-8';
141
            return ('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' . "\n");
142
        }
143
    }
144
 
145
    /**
146
     * The attributes that should be added to the <html> tag. Designed to
147
     * be called in theme layout.php files.
148
     *
149
     * @return string HTML fragment.
150
     */
151
    public function htmlattributes() {
152
        $return = get_html_lang(true);
153
 
154
        $hook = new before_html_attributes($this);
155
 
156
        if ($this->page->theme->doctype !== 'html5') {
157
            $hook->add_attribute('xmlns', 'http://www.w3.org/1999/xhtml');
158
        }
159
 
160
        $hook->process_legacy_callbacks();
161
        di::get(hook_manager::class)->dispatch($hook);
162
 
163
        foreach ($hook->get_attributes() as $key => $val) {
164
            $val = s($val);
165
            $return .= " $key=\"$val\"";
166
        }
167
 
168
        return $return;
169
    }
170
 
171
    /**
172
     * The standard tags (meta tags, links to stylesheets and JavaScript, etc.)
173
     * that should be included in the <head> tag. Designed to be called in theme
174
     * layout.php files.
175
     *
176
     * @return string HTML fragment.
177
     */
178
    public function standard_head_html() {
179
        global $CFG, $SESSION, $SITE;
180
 
181
        // Before we output any content, we need to ensure that certain
182
        // page components are set up.
183
 
184
        // Blocks must be set up early as they may require javascript which
185
        // has to be included in the page header before output is created.
186
        foreach ($this->page->blocks->get_regions() as $region) {
187
            $this->page->blocks->ensure_content_created($region, $this);
188
        }
189
 
190
        // Give plugins an opportunity to add any head elements. The callback
191
        // must always return a string containing valid html head content.
192
 
193
        $hook = new \core\hook\output\before_standard_head_html_generation($this);
194
        $hook->process_legacy_callbacks();
195
        di::get(hook_manager::class)->dispatch($hook);
196
 
197
        // Allow a url_rewrite plugin to setup any dynamic head content.
198
        if (isset($CFG->urlrewriteclass) && !isset($CFG->upgraderunning)) {
199
            $class = $CFG->urlrewriteclass;
200
            $hook->add_html($class::html_head_setup());
201
        }
202
 
203
        $hook->add_html('<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />' . "\n");
204
        $hook->add_html('<meta name="keywords" content="moodle, ' . $this->page->title . '" />' . "\n");
205
        // This is only set by the {@see redirect()} method
206
        $hook->add_html($this->metarefreshtag);
207
 
208
        // Check if a periodic refresh delay has been set and make sure we arn't
209
        // already meta refreshing
210
        if ($this->metarefreshtag == '' && $this->page->periodicrefreshdelay !== null) {
211
            $hook->add_html(
212
                html_writer::empty_tag('meta', [
213
                    'http-equiv' => 'refresh',
214
                    'content' => $this->page->periodicrefreshdelay . ';url=' . $this->page->url->out(),
215
                ]),
216
            );
217
        }
218
 
219
        $output = $hook->get_output();
220
 
221
        // Set up help link popups for all links with the helptooltip class
222
        $this->page->requires->js_init_call('M.util.help_popups.setup');
223
 
224
        $focus = $this->page->focuscontrol;
225
        if (!empty($focus)) {
226
            if (preg_match("#forms\['([a-zA-Z0-9]+)'\].elements\['([a-zA-Z0-9]+)'\]#", $focus, $matches)) {
227
                // This is a horrifically bad way to handle focus but it is passed in
228
                // through messy formslib::moodleform
229
                $this->page->requires->js_function_call('old_onload_focus', [$matches[1], $matches[2]]);
230
            } else if (strpos($focus, '.') !== false) {
231
                // Old style of focus, bad way to do it
232
                debugging('This code is using the old style focus event, Please update this code to focus on an element id or the moodleform focus method.', DEBUG_DEVELOPER);
233
                $this->page->requires->js_function_call('old_onload_focus', explode('.', $focus, 2));
234
            } else {
235
                // Focus element with given id
236
                $this->page->requires->js_function_call('focuscontrol', [$focus]);
237
            }
238
        }
239
 
240
        // Get the theme stylesheet - this has to be always first CSS, this loads also styles.css from all plugins;
241
        // any other custom CSS can not be overridden via themes and is highly discouraged
242
        $urls = $this->page->theme->css_urls($this->page);
243
        foreach ($urls as $url) {
244
            $this->page->requires->css_theme($url);
245
        }
246
 
247
        // Get the theme javascript head and footer
248
        if ($jsurl = $this->page->theme->javascript_url(true)) {
249
            $this->page->requires->js($jsurl, true);
250
        }
251
        if ($jsurl = $this->page->theme->javascript_url(false)) {
252
            $this->page->requires->js($jsurl);
253
        }
254
 
255
        // Get any HTML from the page_requirements_manager.
256
        $output .= $this->page->requires->get_head_code($this->page, $this);
257
 
258
        // List alternate versions.
259
        foreach ($this->page->alternateversions as $type => $alt) {
260
            $output .= html_writer::empty_tag('link', [
261
                'rel' => 'alternate',
262
                'type' => $type,
263
                'title' => $alt->title,
264
                'href' => $alt->url,
265
            ]);
266
        }
267
 
268
        // Add noindex tag if relevant page and setting applied.
269
        $allowindexing = isset($CFG->allowindexing) ? $CFG->allowindexing : 0;
270
        $loginpages = ['login-index', 'login-signup'];
271
        if ($allowindexing == 2 || ($allowindexing == 0 && in_array($this->page->pagetype, $loginpages))) {
272
            if (!isset($CFG->additionalhtmlhead)) {
273
                $CFG->additionalhtmlhead = '';
274
            }
275
            if (stripos($CFG->additionalhtmlhead, '<meta name="robots" content="noindex" />') === false) {
276
                $CFG->additionalhtmlhead .= '<meta name="robots" content="noindex" />';
277
            }
278
        }
279
 
280
        if (!empty($CFG->additionalhtmlhead)) {
281
            $output .= "\n" . $CFG->additionalhtmlhead;
282
        }
283
 
284
        if ($this->page->pagelayout == 'frontpage') {
285
            $summary = s(strip_tags(format_text($SITE->summary, FORMAT_HTML)));
286
            if (!empty($summary)) {
287
                $output .= "<meta name=\"description\" content=\"$summary\" />\n";
288
            }
289
        }
290
 
291
        return $output;
292
    }
293
 
294
    /**
295
     * The standard tags (typically skip links) that should be output just inside
296
     * the start of the <body> tag. Designed to be called in theme layout.php files.
297
     *
298
     * @return string HTML fragment.
299
     */
300
    public function standard_top_of_body_html() {
301
        global $CFG;
302
        $output = $this->page->requires->get_top_of_body_code($this);
303
        if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmltopofbody)) {
304
            $output .= "\n" . $CFG->additionalhtmltopofbody;
305
        }
306
 
307
        // Allow components to add content to the top of the body.
308
        $hook = new before_standard_top_of_body_html_generation($this, $output);
309
        $hook->process_legacy_callbacks();
310
        di::get(hook_manager::class)->dispatch($hook);
311
        $output = $hook->get_output();
312
 
313
        $output .= $this->maintenance_warning();
314
 
315
        return $output;
316
    }
317
 
318
    /**
319
     * Scheduled maintenance warning message.
320
     *
321
     * Note: This is a nasty hack to display maintenance notice, this should be moved
322
     *       to some general notification area once we have it.
323
     *
324
     * @return string
325
     */
326
    public function maintenance_warning() {
327
        global $CFG;
328
 
329
        $output = '';
330
        if (isset($CFG->maintenance_later) and $CFG->maintenance_later > time()) {
331
            $timeleft = $CFG->maintenance_later - time();
332
            // If timeleft less than 30 sec, set the class on block to error to highlight.
333
            $errorclass = ($timeleft < 30) ? 'alert-error alert-danger' : 'alert-warning';
334
            $output .= $this->box_start($errorclass . ' moodle-has-zindex maintenancewarning alert');
335
            $a = new stdClass();
336
            $a->hour = (int)($timeleft / 3600);
337
            $a->min = (int)(floor($timeleft / 60) % 60);
338
            $a->sec = (int)($timeleft % 60);
339
            if ($a->hour > 0) {
340
                $output .= get_string('maintenancemodeisscheduledlong', 'admin', $a);
341
            } else {
342
                $output .= get_string('maintenancemodeisscheduled', 'admin', $a);
343
            }
344
 
345
            $output .= $this->box_end();
346
            $this->page->requires->yui_module(
347
                'moodle-core-maintenancemodetimer',
348
                'M.core.maintenancemodetimer',
349
                [['timeleftinsec' => $timeleft]]
350
            );
351
            $this->page->requires->strings_for_js(
352
                ['maintenancemodeisscheduled', 'maintenancemodeisscheduledlong', 'sitemaintenance'],
353
                'admin'
354
            );
355
        }
356
        return $output;
357
    }
358
 
359
    /**
360
     * content that should be output in the footer area
361
     * of the page. Designed to be called in theme layout.php files.
362
     *
363
     * @return string HTML fragment.
364
     */
365
    public function standard_footer_html() {
366
        if (during_initial_install()) {
367
            // Debugging info can not work before install is finished,
368
            // in any case we do not want any links during installation!
369
            return '';
370
        }
371
 
372
        $hook = new before_standard_footer_html_generation($this);
373
        $hook->process_legacy_callbacks();
374
        di::get(hook_manager::class)->dispatch($hook);
375
        $output = $hook->get_output();
376
 
377
        if ($this->page->devicetypeinuse == 'legacy') {
378
            // The legacy theme is in use print the notification
379
            $output .= html_writer::tag('div', get_string('legacythemeinuse'), ['class' => 'legacythemeinuse']);
380
        }
381
 
382
        // Get links to switch device types (only shown for users not on a default device)
383
        $output .= $this->theme_switch_links();
384
 
385
        return $output;
386
    }
387
 
388
    /**
389
     * Performance information and validation links for debugging.
390
     *
391
     * @return string HTML fragment.
392
     */
393
    public function debug_footer_html() {
394
        global $CFG, $SCRIPT;
395
        $output = '';
396
 
397
        if (during_initial_install()) {
398
            // Debugging info can not work before install is finished.
399
            return $output;
400
        }
401
 
402
        // This function is normally called from a layout.php file
403
        // but some of the content won't be known until later, so we return a placeholder
404
        // for now. This will be replaced with the real content in the footer.
405
        $output .= $this->unique_performance_info_token;
406
 
407
        if (!empty($CFG->debugpageinfo)) {
408
            $output .= '<div class="performanceinfo pageinfo">' . get_string(
409
                'pageinfodebugsummary',
410
                'core_admin',
411
                $this->page->debug_summary()
412
            ) . '</div>';
413
        }
414
        if (debugging(null, DEBUG_DEVELOPER) and has_capability('moodle/site:config', context_system::instance())) {  // Only in developer mode
415
            // Add link to profiling report if necessary
416
            if (function_exists('profiling_is_running') && profiling_is_running()) {
417
                $txt = get_string('profiledscript', 'admin');
418
                $title = get_string('profiledscriptview', 'admin');
419
                $url = $CFG->wwwroot . '/admin/tool/profiling/index.php?script=' . urlencode($SCRIPT);
420
                $link = '<a title="' . $title . '" href="' . $url . '">' . $txt . '</a>';
421
                $output .= '<div class="profilingfooter">' . $link . '</div>';
422
            }
423
            $purgeurl = new moodle_url('/admin/purgecaches.php', [
424
                'confirm' => 1,
425
                'sesskey' => sesskey(),
426
                'returnurl' => $this->page->url->out_as_local_url(false),
427
            ]);
428
            $output .= '<div class="purgecaches">' .
429
                    html_writer::link($purgeurl, get_string('purgecaches', 'admin')) . '</div>';
430
 
431
            // Reactive module debug panel.
432
            $output .= $this->render_from_template('core/local/reactive/debugpanel', []);
433
        }
434
        if (!empty($CFG->debugvalidators)) {
435
            $siteurl = qualified_me();
436
            $nuurl = new moodle_url('https://validator.w3.org/nu/', ['doc' => $siteurl, 'showsource' => 'yes']);
437
            $waveurl = new moodle_url('https://wave.webaim.org/report#/' . urlencode($siteurl));
438
            $validatorlinks = [
439
                html_writer::link($nuurl, get_string('validatehtml')),
440
                html_writer::link($waveurl, get_string('wcagcheck')),
441
            ];
442
            $validatorlinkslist = html_writer::alist($validatorlinks, ['class' => 'list-unstyled ms-1']);
443
            $output .= html_writer::div($validatorlinkslist, 'validators');
444
        }
445
        return $output;
446
    }
447
 
448
    /**
449
     * Returns standard main content placeholder.
450
     * Designed to be called in theme layout.php files.
451
     *
452
     * @return string HTML fragment.
453
     */
454
    public function main_content() {
455
        // This is here because it is the only place we can inject the "main" role over the entire main content area
456
        // without requiring all theme's to manually do it, and without creating yet another thing people need to
457
        // remember in the theme.
458
        // This is an unfortunate hack. DO NO EVER add anything more here.
459
        // DO NOT add classes.
460
        // DO NOT add an id.
461
        return '<div role="main">' . $this->unique_main_content_token . '</div>';
462
    }
463
 
464
    /**
465
     * @deprecated since Moodle 4.3 MDL-78744
466
     */
467
    #[\core\attribute\deprecated(null, since: '4.3', mdl: 'MDL-78744', final: true)]
468
    public function activity_information() {
469
        \core\deprecation::emit_deprecation([self::class, __FUNCTION__]);
470
    }
471
 
472
    /**
473
     * Returns standard navigation between activities in a course.
474
     *
475
     * @return string the navigation HTML.
476
     */
477
    public function activity_navigation() {
478
        // First we should check if we want to add navigation.
479
        $context = $this->page->context;
480
        if (
481
            ($this->page->pagelayout !== 'incourse' && $this->page->pagelayout !== 'frametop')
482
            || $context->contextlevel != CONTEXT_MODULE
483
        ) {
484
            return '';
485
        }
486
 
487
        // If the activity is in stealth mode, show no links.
488
        if ($this->page->cm->is_stealth()) {
489
            return '';
490
        }
491
 
492
        $course = $this->page->cm->get_course();
493
        $courseformat = course_get_format($course);
494
 
495
        // If the theme implements course index and the current course format uses course index and the current
496
        // page layout is not 'frametop' (this layout does not support course index), show no links.
497
        if (
498
            $this->page->theme->usescourseindex && $courseformat->uses_course_index() &&
499
                $this->page->pagelayout !== 'frametop'
500
        ) {
501
            return '';
502
        }
503
 
504
        // Get a list of all the activities in the course.
505
        $modules = get_fast_modinfo($course->id)->get_cms();
506
 
507
        // Put the modules into an array in order by the position they are shown in the course.
508
        $mods = [];
509
        $activitylist = [];
510
        foreach ($modules as $module) {
511
            // Only add activities the user can access, aren't in stealth mode, are of a type that is visible on the course,
512
            // and have a url (eg. mod_label does not).
513
            if (!$module->uservisible || $module->is_stealth() || empty($module->url) || !$module->is_of_type_that_can_display()) {
514
                continue;
515
            }
516
            $mods[$module->id] = $module;
517
 
518
            // No need to add the current module to the list for the activity dropdown menu.
519
            if ($module->id == $this->page->cm->id) {
520
                continue;
521
            }
522
            // Module name.
523
            $modname = $module->get_formatted_name();
524
            // Display the hidden text if necessary.
525
            if (!$module->visible) {
526
                $modname .= ' ' . get_string('hiddenwithbrackets');
527
            }
528
            // Module URL.
529
            $linkurl = new moodle_url($module->url, ['forceview' => 1]);
530
            // Add module URL (as key) and name (as value) to the activity list array.
531
            $activitylist[$linkurl->out(false)] = $modname;
532
        }
533
 
534
        $nummods = count($mods);
535
 
536
        // If there are only one or fewer mods then do nothing.
537
        if ($nummods <= 1) {
538
            return '';
539
        }
540
 
541
        // Get an array of just the course module ids used to get the cmid value based on their position in the course.
542
        $modids = array_keys($mods);
543
 
544
        // Get the position in the array of the course module we are viewing.
545
        $position = array_search($this->page->cm->id, $modids);
546
 
547
        $prevmod = null;
548
        $nextmod = null;
549
 
550
        // Check if we have a previous mod to show.
551
        if ($position > 0) {
552
            $prevmod = $mods[$modids[$position - 1]];
553
        }
554
 
555
        // Check if we have a next mod to show.
556
        if ($position < ($nummods - 1)) {
557
            $nextmod = $mods[$modids[$position + 1]];
558
        }
559
 
560
        $activitynav = new \core_course\output\activity_navigation($prevmod, $nextmod, $activitylist);
561
        $renderer = $this->page->get_renderer('core', 'course');
562
        return $renderer->render($activitynav);
563
    }
564
 
565
    /**
566
     * The standard tags (typically script tags that are not needed earlier) that
567
     * should be output after everything else. Designed to be called in theme layout.php files.
568
     *
569
     * @return string HTML fragment.
570
     */
571
    public function standard_end_of_body_html() {
572
        global $CFG;
573
 
574
        // This function is normally called from a layout.php file in {@see core_renderer::header()}
575
        // but some of the content won't be known until later, so we return a placeholder
576
        // for now. This will be replaced with the real content in {@see core_renderer::footer()}.
577
        $output = '';
578
        if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmlfooter)) {
579
            $output .= "\n" . $CFG->additionalhtmlfooter;
580
        }
581
        $output .= $this->unique_end_html_token;
582
        return $output;
583
    }
584
 
585
    /**
586
     * The standard HTML that should be output just before the <footer> tag.
587
     * Designed to be called in theme layout.php files.
588
     *
589
     * @return string HTML fragment.
590
     */
591
    public function standard_after_main_region_html() {
592
        global $CFG;
593
 
594
        $hook = new after_standard_main_region_html_generation($this);
595
 
596
        if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmlbottomofbody)) {
597
            $hook->add_html("\n");
598
            $hook->add_html($CFG->additionalhtmlbottomofbody);
599
        }
600
 
601
        $hook->process_legacy_callbacks();
602
        di::get(hook_manager::class)->dispatch($hook);
603
 
604
        return $hook->get_output();
605
    }
606
 
607
    /**
608
     * Return the standard string that says whether you are logged in (and switched
609
     * roles/logged in as another user).
610
     * @param bool $withlinks if false, then don't include any links in the HTML produced.
611
     * If not set, the default is the nologinlinks option from the theme config.php file,
612
     * and if that is not set, then links are included.
613
     * @return string HTML fragment.
614
     */
615
    public function login_info($withlinks = null) {
616
        global $USER, $CFG, $DB, $SESSION;
617
 
618
        if (during_initial_install()) {
619
            return '';
620
        }
621
 
622
        if (is_null($withlinks)) {
623
            $withlinks = empty($this->page->layout_options['nologinlinks']);
624
        }
625
 
626
        $course = $this->page->course;
627
        if (\core\session\manager::is_loggedinas()) {
628
            $realuser = \core\session\manager::get_realuser();
629
            $fullname = fullname($realuser);
630
            if ($withlinks) {
631
                $loginastitle = get_string('loginas');
632
                $realuserinfo = " [<a href=\"$CFG->wwwroot/course/loginas.php?id=$course->id&amp;sesskey=" . sesskey() . "\"";
633
                $realuserinfo .= "title =\"" . $loginastitle . "\">$fullname</a>] ";
634
            } else {
635
                $realuserinfo = " [$fullname] ";
636
            }
637
        } else {
638
            $realuserinfo = '';
639
        }
640
 
641
        $loginpage = $this->is_login_page();
642
        $loginurl = get_login_url();
643
 
644
        if (empty($course->id)) {
645
            // $course->id is not defined during installation
646
            return '';
647
        } else if (isloggedin()) {
648
            $context = context_course::instance($course->id);
649
 
650
            $fullname = fullname($USER);
651
            // Since Moodle 2.0 this link always goes to the public profile page (not the course profile page)
652
            if ($withlinks) {
653
                $linktitle = get_string('viewprofile');
654
                $username = "<a href=\"$CFG->wwwroot/user/profile.php?id=$USER->id\" title=\"$linktitle\">$fullname</a>";
655
            } else {
656
                $username = $fullname;
657
            }
658
            if (is_mnet_remote_user($USER) and $idprovider = $DB->get_record('mnet_host', ['id' => $USER->mnethostid])) {
659
                if ($withlinks) {
660
                    $username .= " from <a href=\"{$idprovider->wwwroot}\">{$idprovider->name}</a>";
661
                } else {
662
                    $username .= " from {$idprovider->name}";
663
                }
664
            }
665
            if (isguestuser()) {
666
                $loggedinas = $realuserinfo . get_string('loggedinasguest');
667
                if (!$loginpage && $withlinks) {
668
                    $loggedinas .= " (<a href=\"$loginurl\">" . get_string('login') . '</a>)';
669
                }
670
            } else if (is_role_switched($course->id)) { // Has switched roles
671
                $rolename = '';
672
                if ($role = $DB->get_record('role', ['id' => $USER->access['rsw'][$context->path]])) {
673
                    $rolename = ': ' . role_get_name($role, $context);
674
                }
675
                $loggedinas = get_string('loggedinas', 'moodle', $username) . $rolename;
676
                if ($withlinks) {
677
                    $url = new moodle_url('/course/switchrole.php', ['id' => $course->id, 'sesskey' => sesskey(), 'switchrole' => 0, 'returnurl' => $this->page->url->out_as_local_url(false)]);
678
                    $loggedinas .= ' (' . html_writer::tag('a', get_string('switchrolereturn'), ['href' => $url]) . ')';
679
                }
680
            } else {
681
                $loggedinas = $realuserinfo . get_string('loggedinas', 'moodle', $username);
682
                if ($withlinks) {
683
                    $loggedinas .= " (<a href=\"$CFG->wwwroot/login/logout.php?sesskey=" . sesskey() . "\">" . get_string('logout') . '</a>)';
684
                }
685
            }
686
        } else {
687
            $loggedinas = get_string('loggedinnot', 'moodle');
688
            if (!$loginpage && $withlinks) {
689
                $loggedinas .= " (<a href=\"$loginurl\">" . get_string('login') . '</a>)';
690
            }
691
        }
692
 
693
        $loggedinas = '<div class="logininfo">' . $loggedinas . '</div>';
694
 
695
        if (isset($SESSION->justloggedin)) {
696
            unset($SESSION->justloggedin);
697
            if (!isguestuser()) {
698
                // Include this file only when required.
699
                require_once($CFG->dirroot . '/user/lib.php');
700
                if (($count = user_count_login_failures($USER)) && !empty($CFG->displayloginfailures)) {
701
                    $loggedinas .= '<div class="loginfailures">';
702
                    $a = new stdClass();
703
                    $a->attempts = $count;
704
                    $loggedinas .= get_string('failedloginattempts', '', $a);
705
                    if (file_exists("$CFG->dirroot/report/log/index.php") and has_capability('report/log:view', context_system::instance())) {
706
                        $loggedinas .= ' (' . html_writer::link(new moodle_url('/report/log/index.php', [
707
                            'chooselog' => 1,
708
                            'id' => 0,
709
                            'modid' => 'site_errors',
710
                        ]), get_string('logs')) . ')';
711
                    }
712
                    $loggedinas .= '</div>';
713
                }
714
            }
715
        }
716
 
717
        return $loggedinas;
718
    }
719
 
720
    /**
721
     * Check whether the current page is a login page.
722
     *
723
     * @since Moodle 2.9
724
     * @return bool
725
     */
726
    protected function is_login_page() {
727
        // This is a real bit of a hack, but its a rarety that we need to do something like this.
728
        // In fact the login pages should be only these two pages and as exposing this as an option for all pages
729
        // could lead to abuse (or at least unneedingly complex code) the hack is the way to go.
730
        return in_array(
731
            $this->page->url->out_as_local_url(false, []),
732
            [
733
                '/login/index.php',
734
                '/login/forgot_password.php',
735
            ]
736
        );
737
    }
738
 
739
    /**
740
     * Return the 'back' link that normally appears in the footer.
741
     *
742
     * @return string HTML fragment.
743
     */
744
    public function home_link() {
745
        global $CFG, $SITE;
746
 
747
        if ($this->page->pagetype == 'site-index') {
748
            // Special case for site home page - please do not remove
749
            return '<div class="sitelink">' .
750
                   '<a title="Moodle" class="d-inline-block aalink" href="http://moodle.org/">' .
751
                   '<img src="' . $this->image_url('moodlelogo_grayhat') . '" alt="' . get_string('moodlelogo') . '" /></a></div>';
752
        } else if (!empty($CFG->target_release) && $CFG->target_release != $CFG->release) {
753
            // Special case for during install/upgrade.
754
            return '<div class="sitelink">' .
755
                   '<a title="Moodle" href="http://docs.moodle.org/en/Administrator_documentation" onclick="this.target=\'_blank\'">' .
756
                   '<img src="' . $this->image_url('moodlelogo_grayhat') . '" alt="' . get_string('moodlelogo') . '" /></a></div>';
757
        } else if ($this->page->course->id == $SITE->id || strpos($this->page->pagetype, 'course-view') === 0) {
758
            return '<div class="homelink"><a href="' . $CFG->wwwroot . '/">' .
759
                    get_string('home') . '</a></div>';
760
        } else {
761
            return '<div class="homelink"><a href="' . $CFG->wwwroot . '/course/view.php?id=' . $this->page->course->id . '">' .
762
                    format_string($this->page->course->shortname, true, ['context' => $this->page->context]) . '</a></div>';
763
        }
764
    }
765
 
766
    /**
767
     * Redirects the user by any means possible given the current state
768
     *
769
     * This function should not be called directly, it should always be called using
770
     * the redirect function in lib/weblib.php
771
     *
772
     * The redirect function should really only be called before page output has started
773
     * however it will allow itself to be called during the state STATE_IN_BODY
774
     *
775
     * @param string $encodedurl The URL to send to encoded if required
776
     * @param string $message The message to display to the user if any
777
     * @param int $delay The delay before redirecting a user, if $message has been
778
     *         set this is a requirement and defaults to 3, set to 0 no delay
779
     * @param boolean $debugdisableredirect this redirect has been disabled for
780
     *         debugging purposes. Display a message that explains, and don't
781
     *         trigger the redirect.
782
     * @param string $messagetype The type of notification to show the message in.
783
     *         See constants on \core\output\notification.
784
     * @return string The HTML to display to the user before dying, may contain
785
     *         meta refresh, javascript refresh, and may have set header redirects
786
     */
787
    public function redirect_message(
788
        $encodedurl,
789
        $message,
790
        $delay,
791
        $debugdisableredirect,
792
        $messagetype = notification::NOTIFY_INFO,
793
    ) {
794
        global $CFG;
795
        $url = str_replace('&amp;', '&', $encodedurl);
796
 
797
        switch ($this->page->state) {
798
            case moodle_page::STATE_BEFORE_HEADER:
799
                // No output yet it is safe to delivery the full arsenal of redirect methods
800
                if (!$debugdisableredirect) {
801
                    // Don't use exactly the same time here, it can cause problems when both redirects fire at the same time.
802
                    $this->metarefreshtag = '<meta http-equiv="refresh" content="' . $delay . '; url=' . $encodedurl . '" />' . "\n";
803
                    $this->page->requires->js_function_call('document.location.replace', [$url], false, ($delay + 3));
804
                }
805
                $output = $this->header();
806
                break;
807
            case moodle_page::STATE_PRINTING_HEADER:
808
                // We should hopefully never get here
809
                throw new coding_exception('You cannot redirect while printing the page header');
810
                break;
811
            case moodle_page::STATE_IN_BODY:
812
                // We really shouldn't be here but we can deal with this
813
                debugging("You should really redirect before you start page output");
814
                if (!$debugdisableredirect) {
815
                    $this->page->requires->js_function_call('document.location.replace', [$url], false, $delay);
816
                }
817
                $output = $this->opencontainers->pop_all_but_last();
818
                break;
819
            case moodle_page::STATE_DONE:
820
                // Too late to be calling redirect now
821
                throw new coding_exception('You cannot redirect after the entire page has been generated');
822
                break;
823
        }
824
        $output .= $this->notification($message, $messagetype);
825
        $output .= '<div class="continuebutton">(<a href="' . $encodedurl . '">' . get_string('continue') . '</a>)</div>';
826
        if ($debugdisableredirect) {
827
            $output .= '<p><strong>' . get_string('erroroutput', 'error') . '</strong></p>';
828
        }
829
        $output .= $this->footer();
830
        return $output;
831
    }
832
 
833
    /**
834
     * Start output by sending the HTTP headers, and printing the HTML <head>
835
     * and the start of the <body>.
836
     *
837
     * To control what is printed, you should set properties on $PAGE.
838
     *
839
     * @return string HTML that you must output this, preferably immediately.
840
     */
841
    public function header() {
842
        global $USER, $CFG, $SESSION;
843
 
844
        $hook = new before_http_headers($this);
845
        $hook->process_legacy_callbacks();
846
        di::get(hook_manager::class)->dispatch($hook);
847
 
848
        if (\core\session\manager::is_loggedinas()) {
849
            $this->page->add_body_class('userloggedinas');
850
        }
851
 
852
        if (isset($SESSION->justloggedin) && !empty($CFG->displayloginfailures)) {
853
            require_once($CFG->dirroot . '/user/lib.php');
854
            // Set second parameter to false as we do not want reset the counter, the same message appears on footer.
855
            if ($count = user_count_login_failures($USER, false)) {
856
                $this->page->add_body_class('loginfailures');
857
            }
858
        }
859
 
860
        // If the user is logged in, and we're not in initial install,
861
        // check to see if the user is role-switched and add the appropriate
862
        // CSS class to the body element.
863
        if (!during_initial_install() && isloggedin() && is_role_switched($this->page->course->id)) {
864
            $this->page->add_body_class('userswitchedrole');
865
        }
866
 
867
        // Give themes a chance to init/alter the page object.
868
        $this->page->theme->init_page($this->page);
869
 
870
        $this->page->set_state(moodle_page::STATE_PRINTING_HEADER);
871
 
872
        // Find the appropriate page layout file, based on $this->page->pagelayout.
873
        $layoutfile = $this->page->theme->layout_file($this->page->pagelayout);
874
        // Render the layout using the layout file.
875
        $rendered = $this->render_page_layout($layoutfile);
876
 
877
        // Slice the rendered output into header and footer.
878
        $cutpos = strpos($rendered, $this->unique_main_content_token);
879
        if ($cutpos === false) {
880
            $cutpos = strpos($rendered, self::MAIN_CONTENT_TOKEN);
881
            $token = self::MAIN_CONTENT_TOKEN;
882
        } else {
883
            $token = $this->unique_main_content_token;
884
        }
885
 
886
        if ($cutpos === false) {
887
            throw new coding_exception('page layout file ' . $layoutfile . ' does not contain the main content placeholder, please include "<?php echo $OUTPUT->main_content() ?>" in theme layout file.');
888
        }
889
 
890
        $header = substr($rendered, 0, $cutpos);
891
        $footer = substr($rendered, $cutpos + strlen($token));
892
 
893
        if (empty($this->contenttype)) {
894
            debugging('The page layout file did not call $OUTPUT->doctype()');
895
            $header = $this->doctype() . $header;
896
        }
897
 
898
        // If this theme version is below 2.4 release and this is a course view page
899
        if (
900
            (!isset($this->page->theme->settings->version) || $this->page->theme->settings->version < 2012101500) &&
901
                $this->page->pagelayout === 'course' && $this->page->url->compare(new moodle_url('/course/view.php'), URL_MATCH_BASE)
902
        ) {
903
            // check if course content header/footer have not been output during render of theme layout
904
            $coursecontentheader = $this->course_content_header(true);
905
            $coursecontentfooter = $this->course_content_footer(true);
906
            if (!empty($coursecontentheader)) {
907
                // display debug message and add header and footer right above and below main content
908
                // Please note that course header and footer (to be displayed above and below the whole page)
909
                // are not displayed in this case at all.
910
                // Besides the content header and footer are not displayed on any other course page
911
                debugging('The current theme is not optimised for 2.4, the course-specific header and footer defined in course format will not be output', DEBUG_DEVELOPER);
912
                $header .= $coursecontentheader;
913
                $footer = $coursecontentfooter . $footer;
914
            }
915
        }
916
 
917
        send_headers($this->contenttype, $this->page->cacheable);
918
 
919
        $this->opencontainers->push('header/footer', $footer);
920
        $this->page->set_state(moodle_page::STATE_IN_BODY);
921
 
922
        // If an activity record has been set, activity_header will handle this.
923
        if (!$this->page->cm || !empty($this->page->layout_options['noactivityheader'])) {
924
            $header .= $this->skip_link_target('maincontent');
925
        }
926
 
927
        $hook = new after_http_headers(
928
            renderer: $this,
929
            output: $header,
930
        );
931
        di::get(hook_manager::class)->dispatch($hook);
932
 
933
        return $hook->get_output();
934
    }
935
 
936
    /**
937
     * Renders and outputs the page layout file.
938
     *
939
     * This is done by preparing the normal globals available to a script, and
940
     * then including the layout file provided by the current theme for the
941
     * requested layout.
942
     *
943
     * @param string $layoutfile The name of the layout file
944
     * @return string HTML code
945
     */
946
    protected function render_page_layout($layoutfile) {
947
        global $CFG, $SITE, $USER;
948
        // The next lines are a bit tricky. The point is, here we are in a method
949
        // of a renderer class, and this object may, or may not, be the same as
950
        // the global $OUTPUT object. When rendering the page layout file, we want to use
951
        // this object. However, people writing Moodle code expect the current
952
        // renderer to be called $OUTPUT, not $this, so define a variable called
953
        // $OUTPUT pointing at $this. The same comment applies to $PAGE and $COURSE.
954
        $OUTPUT = $this;
955
        $PAGE = $this->page;
956
        $COURSE = $this->page->course;
957
 
958
        ob_start();
959
        include($layoutfile);
960
        $rendered = ob_get_contents();
961
        ob_end_clean();
962
        return $rendered;
963
    }
964
 
965
    /**
966
     * Outputs the page's footer
967
     *
968
     * @return string HTML fragment
969
     */
970
    public function footer() {
971
        global $CFG, $DB, $PERF;
972
 
973
        $hook = new before_footer_html_generation($this);
974
        $hook->process_legacy_callbacks();
975
        di::get(hook_manager::class)->dispatch($hook);
976
        $hook->add_html($this->container_end_all(true));
977
        $output = $hook->get_output();
978
 
979
        $footer = $this->opencontainers->pop('header/footer');
980
 
981
        if (debugging() and $DB and $DB->is_transaction_started()) {
982
            // TODO: MDL-20625 print warning - transaction will be rolled back
983
        }
984
 
985
        // Provide some performance info if required
986
        $performanceinfo = '';
987
        if (MDL_PERF || (!empty($CFG->perfdebug) && $CFG->perfdebug > 7)) {
988
            if (MDL_PERFTOFOOT || debugging() || (!empty($CFG->perfdebug) && $CFG->perfdebug > 7)) {
989
                if (NO_OUTPUT_BUFFERING) {
990
                    // If the output buffer was off then we render a placeholder and stream the
991
                    // performance debugging into it at the very end in the shutdown handler.
992
                    $PERF->perfdebugdeferred = true;
993
                    $performanceinfo .= html_writer::tag(
994
                        'div',
995
                        get_string('perfdebugdeferred', 'admin'),
996
                        [
997
                            'id' => 'perfdebugfooter',
998
                            'style' => 'min-height: 30em',
999
                        ]
1000
                    );
1001
                } else {
1002
                    $perf = get_performance_info();
1003
                    $performanceinfo = $perf['html'];
1004
                }
1005
            }
1006
        }
1007
 
1008
        // We always want performance data when running a performance test, even if the user is redirected to another page.
1009
        if (MDL_PERF_TEST && strpos($footer, $this->unique_performance_info_token) === false) {
1010
            $footer = $this->unique_performance_info_token . $footer;
1011
        }
1012
        $footer = str_replace($this->unique_performance_info_token, $performanceinfo, $footer);
1013
 
1014
        // Only show notifications when the current page has a context id.
1015
        if (!empty($this->page->context->id)) {
1016
            $this->page->requires->js_call_amd('core/notification', 'init', [
1017
                $this->page->context->id,
1018
                \core\notification::fetch_as_array($this),
1019
            ]);
1020
        }
1021
        $footer = str_replace($this->unique_end_html_token, $this->page->requires->get_end_code(), $footer);
1022
 
1023
        $this->page->set_state(moodle_page::STATE_DONE);
1024
 
1025
        // Here we remove the closing body and html tags and store them to be added back
1026
        // in the shutdown handler so we can have valid html with streaming script tags
1027
        // which are rendered after the visible footer.
1028
        $tags = '';
1029
        preg_match('#\<\/body>#i', $footer, $matches);
1030
        $tags .= $matches[0];
1031
        $footer = str_replace($matches[0], '', $footer);
1032
 
1033
        preg_match('#\<\/html>#i', $footer, $matches);
1034
        $tags .= $matches[0];
1035
        $footer = str_replace($matches[0], '', $footer);
1036
 
1037
        $CFG->closingtags = $tags;
1038
 
1039
        return $output . $footer;
1040
    }
1041
 
1042
    /**
1043
     * Close all but the last open container. This is useful in places like error
1044
     * handling, where you want to close all the open containers (apart from <body>)
1045
     * before outputting the error message.
1046
     *
1047
     * @param bool $shouldbenone assert that the stack should be empty now - causes a
1048
     *      developer debug warning if it isn't.
1049
     * @return string the HTML required to close any open containers inside <body>.
1050
     */
1051
    public function container_end_all($shouldbenone = false) {
1052
        return $this->opencontainers->pop_all_but_last($shouldbenone);
1053
    }
1054
 
1055
    /**
1056
     * Returns course-specific information to be output immediately above content on any course page
1057
     * (for the current course)
1058
     *
1059
     * @param bool $onlyifnotcalledbefore output content only if it has not been output before
1060
     * @return string
1061
     */
1062
    public function course_content_header($onlyifnotcalledbefore = false) {
1063
        global $CFG;
1064
        static $functioncalled = false;
1065
        if ($functioncalled && $onlyifnotcalledbefore) {
1066
            // we have already output the content header
1067
            return '';
1068
        }
1069
 
1070
        // Output any session notification.
1071
        $notifications = \core\notification::fetch();
1072
 
1073
        $bodynotifications = '';
1074
        foreach ($notifications as $notification) {
1075
            $bodynotifications .= $this->render_from_template(
1076
                $notification->get_template_name(),
1077
                $notification->export_for_template($this)
1078
            );
1079
        }
1080
 
1081
        $output = html_writer::span($bodynotifications, 'notifications', ['id' => 'user-notifications']);
1082
 
1083
        if ($this->page->course->id == SITEID) {
1084
            // return immediately and do not include /course/lib.php if not necessary
1085
            return $output;
1086
        }
1087
 
1088
        require_once($CFG->dirroot . '/course/lib.php');
1089
        $functioncalled = true;
1090
        $courseformat = course_get_format($this->page->course);
1091
        if (($obj = $courseformat->course_content_header()) !== null) {
1092
            $output .= html_writer::div($courseformat->get_renderer($this->page)->render($obj), 'course-content-header');
1093
        }
1094
        return $output;
1095
    }
1096
 
1097
    /**
1098
     * Returns course-specific information to be output immediately below content on any course page
1099
     * (for the current course)
1100
     *
1101
     * @param bool $onlyifnotcalledbefore output content only if it has not been output before
1102
     * @return string
1103
     */
1104
    public function course_content_footer($onlyifnotcalledbefore = false) {
1105
        global $CFG;
1106
        if ($this->page->course->id == SITEID) {
1107
            // return immediately and do not include /course/lib.php if not necessary
1108
            return '';
1109
        }
1110
        static $functioncalled = false;
1111
        if ($functioncalled && $onlyifnotcalledbefore) {
1112
            // we have already output the content footer
1113
            return '';
1114
        }
1115
        $functioncalled = true;
1116
        require_once($CFG->dirroot . '/course/lib.php');
1117
        $courseformat = course_get_format($this->page->course);
1118
        if (($obj = $courseformat->course_content_footer()) !== null) {
1119
            return html_writer::div($courseformat->get_renderer($this->page)->render($obj), 'course-content-footer');
1120
        }
1121
        return '';
1122
    }
1123
 
1124
    /**
1125
     * Returns course-specific information to be output on any course page in the header area
1126
     * (for the current course)
1127
     *
1128
     * @return string
1129
     */
1130
    public function course_header() {
1131
        global $CFG;
1132
        if ($this->page->course->id == SITEID) {
1133
            // return immediately and do not include /course/lib.php if not necessary
1134
            return '';
1135
        }
1136
        require_once($CFG->dirroot . '/course/lib.php');
1137
        $courseformat = course_get_format($this->page->course);
1138
        if (($obj = $courseformat->course_header()) !== null) {
1139
            return $courseformat->get_renderer($this->page)->render($obj);
1140
        }
1141
        return '';
1142
    }
1143
 
1144
    /**
1145
     * Returns course-specific information to be output on any course page in the footer area
1146
     * (for the current course)
1147
     *
1148
     * @return string
1149
     */
1150
    public function course_footer() {
1151
        global $CFG;
1152
        if ($this->page->course->id == SITEID) {
1153
            // return immediately and do not include /course/lib.php if not necessary
1154
            return '';
1155
        }
1156
        require_once($CFG->dirroot . '/course/lib.php');
1157
        $courseformat = course_get_format($this->page->course);
1158
        if (($obj = $courseformat->course_footer()) !== null) {
1159
            return $courseformat->get_renderer($this->page)->render($obj);
1160
        }
1161
        return '';
1162
    }
1163
 
1164
    /**
1165
     * Get the course pattern datauri to show on a course card.
1166
     *
1167
     * The datauri is an encoded svg that can be passed as a url.
1168
     * @param int $id Id to use when generating the pattern
1169
     * @return string datauri
1170
     */
1171
    public function get_generated_image_for_id($id) {
1172
        $color = $this->get_generated_color_for_id($id);
1173
        $pattern = new \core_geopattern();
1174
        $pattern->setColor($color);
1175
        $pattern->patternbyid($id);
1176
        return $pattern->datauri();
1177
    }
1178
 
1179
    /**
1180
     * Get the course pattern image URL.
1181
     *
1182
     * @param context_course $context course context object
1183
     * @return string URL of the course pattern image in SVG format
1184
     */
1185
    public function get_generated_url_for_course(context_course $context): string {
1186
        return moodle_url::make_pluginfile_url($context->id, 'course', 'generated', null, '/', 'course.svg')->out();
1187
    }
1188
 
1189
    /**
1190
     * Get the course pattern in SVG format to show on a course card.
1191
     *
1192
     * @param int $id id to use when generating the pattern
1193
     * @return string SVG file contents
1194
     */
1195
    public function get_generated_svg_for_id(int $id): string {
1196
        $color = $this->get_generated_color_for_id($id);
1197
        $pattern = new \core_geopattern();
1198
        $pattern->setColor($color);
1199
        $pattern->patternbyid($id);
1200
        return $pattern->toSVG();
1201
    }
1202
 
1203
    /**
1204
     * Get the course color to show on a course card.
1205
     *
1206
     * @param int $id Id to use when generating the color.
1207
     * @return string hex color code.
1208
     */
1209
    public function get_generated_color_for_id($id) {
1210
        $colornumbers = range(1, 10);
1211
        $basecolors = [];
1212
        foreach ($colornumbers as $number) {
1213
            $basecolors[] = get_config('core_admin', 'coursecolor' . $number);
1214
        }
1215
 
1216
        $color = $basecolors[$id % 10];
1217
        return $color;
1218
    }
1219
 
1220
    /**
1221
     * Returns lang menu or '', this method also checks forcing of languages in courses.
1222
     *
1223
     * This function calls {@see core_renderer::render_single_select()} to actually display the language menu.
1224
     *
1225
     * @return string The lang menu HTML or empty string
1226
     */
1227
    public function lang_menu() {
1228
        $languagemenu = new language_menu($this->page);
1229
        $data = $languagemenu->export_for_single_select($this);
1230
        if ($data) {
1231
            return $this->render_from_template('core/single_select', $data);
1232
        }
1233
        return '';
1234
    }
1235
 
1236
    /**
1237
     * Output the row of editing icons for a block, as defined by the controls array.
1238
     *
1239
     * @param array $actions an array like {@see block_contents::$controls}.
1240
     * @param null|string $blockid The ID given to the block.
1241
     * @return string HTML fragment.
1242
     */
1243
    public function block_controls($actions, $blockid = null) {
1244
        if (empty($actions)) {
1245
            return '';
1246
        }
1247
        $menu = new action_menu($actions);
1248
        if ($blockid !== null) {
1249
            $menu->set_owner_selector('#' . $blockid);
1250
        }
1251
        $menu->attributes['class'] .= ' block-control-actions commands';
1252
        return $this->render($menu);
1253
    }
1254
 
1255
    /**
1256
     * Returns the HTML for a basic textarea field.
1257
     *
1258
     * @param string $name Name to use for the textarea element
1259
     * @param string $id The id to use fort he textarea element
1260
     * @param string $value Initial content to display in the textarea
1261
     * @param int $rows Number of rows to display
1262
     * @param int $cols Number of columns to display
1263
     * @return string the HTML to display
1264
     */
1265
    public function print_textarea($name, $id, $value, $rows, $cols) {
1266
        editors_head_setup();
1267
        $editor = editors_get_preferred_editor(FORMAT_HTML);
1268
        $editor->set_text($value);
1269
        $editor->use_editor($id, []);
1270
 
1271
        $context = [
1272
            'id' => $id,
1273
            'name' => $name,
1274
            'value' => $value,
1275
            'rows' => $rows,
1276
            'cols' => $cols,
1277
        ];
1278
 
1279
        return $this->render_from_template('core_form/editor_textarea', $context);
1280
    }
1281
 
1282
    /**
1283
     * Renders an action menu component.
1284
     *
1285
     * @param action_menu $menu
1286
     * @return string HTML
1287
     */
1288
    public function render_action_menu(action_menu $menu) {
1289
 
1290
        // We don't want the class icon there!
1291
        foreach ($menu->get_secondary_actions() as $action) {
1292
            if ($action instanceof action_menu\link && $action->has_class('icon')) {
1293
                $action->attributes['class'] = preg_replace('/(^|\s+)icon(\s+|$)/i', '', $action->attributes['class']);
1294
            }
1295
        }
1296
 
1297
        if ($menu->is_empty()) {
1298
            return '';
1299
        }
1300
        $context = $menu->export_for_template($this);
1301
 
1302
        return $this->render_from_template('core/action_menu', $context);
1303
    }
1304
 
1305
    /**
1306
     * Renders a full check API result including summary and details
1307
     *
1308
     * @param check_check $check the check that was run to get details from
1309
     * @param check_result $result the result of a check
1310
     * @param bool $includedetails if true, details are included as well
1311
     * @return string rendered html
1312
     */
1313
    protected function render_check_full_result(check_check $check, check_result $result, bool $includedetails): string {
1314
        // Initially render just badge itself.
1315
        $renderedresult = $this->render_from_template($result->get_template_name(), $result->export_for_template($this));
1316
 
1317
        // Add summary.
1318
        $renderedresult .= ' ' . $result->get_summary();
1319
 
1320
        // Wrap in notificaiton.
1321
        $notificationmap = [
1322
            check_result::NA => notification::NOTIFY_INFO,
1323
            check_result::OK => notification::NOTIFY_SUCCESS,
1324
            check_result::INFO => notification::NOTIFY_INFO,
1325
            check_result::UNKNOWN => notification::NOTIFY_WARNING,
1326
            check_result::WARNING => notification::NOTIFY_WARNING,
1327
            check_result::ERROR => notification::NOTIFY_ERROR,
1328
            check_result::CRITICAL => notification::NOTIFY_ERROR,
1329
        ];
1330
 
1331
        // Get type, or default to error.
1332
        $notificationtype = $notificationmap[$result->get_status()] ?? notification::NOTIFY_ERROR;
1333
        $renderedresult = $this->notification($renderedresult, $notificationtype, false);
1334
 
1335
        // If adding details, add on new line.
1336
        if ($includedetails) {
1337
            $renderedresult .= $result->get_details();
1338
        }
1339
 
1340
        // Add the action link.
1341
        if ($actionlink = $check->get_action_link()) {
1342
            $renderedresult .= $this->render_action_link($actionlink);
1343
        }
1344
 
1345
        return $renderedresult;
1346
    }
1347
 
1348
    /**
1349
     * Renders a full check API result including summary and details
1350
     *
1351
     * @param check_check $check the check that was run to get details from
1352
     * @param check_result $result the result of a check
1353
     * @param bool $includedetails if details should be included
1354
     * @return string HTML fragment
1355
     */
1356
    public function check_full_result(check_check $check, check_result $result, bool $includedetails = false) {
1357
        return $this->render_check_full_result($check, $result, $includedetails);
1358
    }
1359
 
1360
    /**
1361
     * Renders a Check API result
1362
     *
1363
     * @param check_result $result
1364
     * @return string HTML fragment
1365
     */
1366
    protected function render_check_result(check_result $result) {
1367
        return $this->render_from_template($result->get_template_name(), $result->export_for_template($this));
1368
    }
1369
 
1370
    /**
1371
     * Renders a Check API result
1372
     *
1373
     * @param check_result $result
1374
     * @return string HTML fragment
1375
     */
1376
    public function check_result(check_result $result) {
1377
        return $this->render_check_result($result);
1378
    }
1379
 
1380
    /**
1381
     * @deprecated Since Moodle 4.5. Will be removed in MDL-83221
1382
     */
1383
    #[\core\attribute\deprecated(
1384
        replacement: 'render_action_menu__link',
1385
        since: '4.5',
1386
        mdl: 'MDL-83164',
1387
    )]
1388
    protected function render_action_menu_link(\action_menu_link $action) {
1389
        \core\deprecation::emit_deprecation([$this, __FUNCTION__]);
1390
        return $this->render_action_menu__link($action);
1391
    }
1392
 
1393
    /**
1394
     * @deprecated Since Moodle 4.5. Will be removed in MDL-83221
1395
     */
1396
    #[\core\attribute\deprecated(
1397
        replacement: 'render_action_menu__filler',
1398
        since: '4.5',
1399
        mdl: 'MDL-83164',
1400
    )]
1401
    protected function render_action_menu_filler(\action_menu_filler $action) {
1402
        \core\deprecation::emit_deprecation([$this, __FUNCTION__]);
1403
        return $this->render_action_menu__filler($action);
1404
    }
1405
 
1406
    /**
1407
     * @deprecated Since Moodle 4.5. Will be removed in MDL-83221
1408
     */
1409
    #[\core\attribute\deprecated(
1410
        replacement: 'render_action_menu__link_primary',
1411
        since: '4.5',
1412
        mdl: 'MDL-83164',
1413
    )]
1414
    protected function render_action_menu_primary(\action_menu_link $action) {
1415
        \core\deprecation::emit_deprecation([$this, __FUNCTION__]);
1416
        return $this->render_action_menu__link_primary($action);
1417
    }
1418
 
1419
    /**
1420
     * @deprecated Since Moodle 4.5. Will be removed in MDL-83221
1421
     */
1422
    #[\core\attribute\deprecated(
1423
        replacement: 'render_action_menu__link_secondary',
1424
        since: '4.5',
1425
        mdl: 'MDL-83164',
1426
    )]
1427
    protected function render_action_menu_secondary(\action_menu_link $action) {
1428
        \core\deprecation::emit_deprecation([$this, __FUNCTION__]);
1429
        return $this->render_action_menu__link_secondary($action);
1430
    }
1431
 
1432
    /**
1433
     * Renders an action_menu link item.
1434
     *
1435
     * @param action_menu\link $action
1436
     * @return string HTML fragment
1437
     */
1438
    protected function render_action_menu__link(action_menu\link $action) {
1439
        return $this->render_from_template('core/action_menu_link', $action->export_for_template($this));
1440
    }
1441
 
1442
    /**
1443
     * Renders a primary action_menu filler item.
1444
     *
1445
     * @param action_menu\filler $action
1446
     * @return string HTML fragment
1447
     */
1448
    protected function render_action_menu__filler(action_menu\filler $action) {
1449
        return html_writer::span('&nbsp;', 'filler');
1450
    }
1451
 
1452
    /**
1453
     * Renders a primary action_menu link item.
1454
     *
1455
     * @param action_menu\link_primary $action
1456
     * @return string HTML fragment
1457
     */
1458
    protected function render_action_menu__link_primary(action_menu\link_primary $action) {
1459
        return $this->render_action_menu__link($action);
1460
    }
1461
 
1462
    /**
1463
     * Renders a secondary action_menu link item.
1464
     *
1465
     * @param action_menu\link_secondary $action
1466
     * @return string HTML fragment
1467
     */
1468
    protected function render_action_menu__link_secondary(action_menu\link_secondary $action) {
1469
        return $this->render_action_menu__link($action);
1470
    }
1471
 
1472
    /**
1473
     * Prints a nice side block with an optional header.
1474
     *
1475
     * @param block_contents $bc HTML for the content
1476
     * @param string $region the region the block is appearing in.
1477
     * @return string the HTML to be output.
1478
     */
1479
    public function block(block_contents $bc, $region) {
1480
        $bc = clone($bc); // Avoid messing up the object passed in.
1481
        if (empty($bc->blockinstanceid) || !strip_tags($bc->title)) {
1482
            $bc->collapsible = block_contents::NOT_HIDEABLE;
1483
        }
1484
 
1485
        $id = !empty($bc->attributes['id']) ? $bc->attributes['id'] : uniqid('block-');
1486
        $context = new stdClass();
1487
        $context->skipid = $bc->skipid;
1488
        $context->blockinstanceid = $bc->blockinstanceid ?: uniqid('fakeid-');
1489
        $context->dockable = $bc->dockable;
1490
        $context->id = $id;
1491
        $context->hidden = $bc->collapsible == block_contents::HIDDEN;
1492
        $context->skiptitle = strip_tags($bc->title);
1493
        $context->showskiplink = !empty($context->skiptitle);
1494
        $context->arialabel = $bc->arialabel;
1495
        $context->ariarole = !empty($bc->attributes['role']) ? $bc->attributes['role'] : '';
1496
        $context->class = $bc->attributes['class'];
1497
        $context->type = $bc->attributes['data-block'];
1498
        $context->title = (string) $bc->title;
1499
        $context->showtitle = $context->title !== '';
1500
        $context->content = $bc->content;
1501
        $context->annotation = $bc->annotation;
1502
        $context->footer = $bc->footer;
1503
        $context->hascontrols = !empty($bc->controls);
1504
        if ($context->hascontrols) {
1505
            $context->controls = $this->block_controls($bc->controls, $id);
1506
        }
1507
 
1508
        return $this->render_from_template('core/block', $context);
1509
    }
1510
 
1511
    /**
1512
     * Render the contents of a block_list.
1513
     *
1514
     * @param array $icons the icon for each item.
1515
     * @param array $items the content of each item.
1516
     * @return string HTML
1517
     */
1518
    public function list_block_contents($icons, $items) {
1519
        $row = 0;
1520
        $lis = [];
1521
        foreach ($items as $key => $string) {
1522
            $item = html_writer::start_tag('li', ['class' => 'r' . $row]);
1523
            if (!empty($icons[$key])) { // test if the content has an assigned icon
1524
                $item .= html_writer::tag('div', $icons[$key], ['class' => 'column c0 icon-size-3']);
1525
            }
1526
            $item .= html_writer::tag('div', $string, ['class' => 'column c1']);
1527
            $item .= html_writer::end_tag('li');
1528
            $lis[] = $item;
1529
            $row = 1 - $row; // Flip even/odd.
1530
        }
1531
        return html_writer::tag('ul', implode("\n", $lis), ['class' => 'unlist']);
1532
    }
1533
 
1534
    /**
1535
     * Output all the blocks in a particular region.
1536
     *
1537
     * @param string $region the name of a region on this page.
1538
     * @param boolean $fakeblocksonly Output fake block only.
1539
     * @return string the HTML to be output.
1540
     */
1541
    public function blocks_for_region($region, $fakeblocksonly = false) {
1542
        $blockcontents = $this->page->blocks->get_content_for_region($region, $this);
1543
        $lastblock = null;
1544
        $zones = [];
1545
        foreach ($blockcontents as $bc) {
1546
            if ($bc instanceof block_contents) {
1547
                $zones[] = $bc->title;
1548
            }
1549
        }
1550
        $output = '';
1551
 
1552
        foreach ($blockcontents as $bc) {
1553
            if ($bc instanceof block_contents) {
1554
                if ($fakeblocksonly && !$bc->is_fake()) {
1555
                    // Skip rendering real blocks if we only want to show fake blocks.
1556
                    continue;
1557
                }
1558
                $output .= $this->block($bc, $region);
1559
                $lastblock = $bc->title;
1560
            } else if ($bc instanceof block_move_target) {
1561
                if (!$fakeblocksonly) {
1562
                    $output .= $this->block_move_target($bc, $zones, $lastblock, $region);
1563
                }
1564
            } else {
1565
                throw new coding_exception('Unexpected type of thing (' . get_class($bc) . ') found in list of block contents.');
1566
            }
1567
        }
1568
        return $output;
1569
    }
1570
 
1571
    /**
1572
     * Output a place where the block that is currently being moved can be dropped.
1573
     *
1574
     * @param block_move_target $target with the necessary details.
1575
     * @param array $zones array of areas where the block can be moved to
1576
     * @param string $previous the block located before the area currently being rendered.
1577
     * @param string $region the name of the region
1578
     * @return string the HTML to be output.
1579
     */
1580
    public function block_move_target($target, $zones, $previous, $region) {
1581
        if ($previous == null) {
1582
            if (empty($zones)) {
1583
                // There are no zones, probably because there are no blocks.
1584
                $regions = $this->page->theme->get_all_block_regions();
1585
                $position = get_string('moveblockinregion', 'block', $regions[$region]);
1586
            } else {
1587
                $position = get_string('moveblockbefore', 'block', $zones[0]);
1588
            }
1589
        } else {
1590
            $position = get_string('moveblockafter', 'block', $previous);
1591
        }
1592
        return html_writer::tag('a', html_writer::tag('span', $position, ['class' => 'accesshide']), ['href' => $target->url, 'class' => 'blockmovetarget']);
1593
    }
1594
 
1595
    /**
1596
     * Renders a special html link with attached action
1597
     *
1598
     * Theme developers: DO NOT OVERRIDE! Please override function
1599
     * {@see core_renderer::render_action_link()} instead.
1600
     *
1601
     * @param string|moodle_url $url
1602
     * @param string $text HTML fragment
1603
     * @param null|component_action $action
1604
     * @param null|array $attributes associative array of html link attributes + disabled
1605
     * @param null|pix_icon optional pix icon to render with the link
1606
     * @return string HTML fragment
1607
     */
1608
    public function action_link($url, $text, ?component_action $action = null, ?array $attributes = null, $icon = null) {
1609
        if (!($url instanceof moodle_url)) {
1610
            $url = new moodle_url($url);
1611
        }
1612
        $link = new action_link($url, $text, $action, $attributes, $icon);
1613
 
1614
        return $this->render($link);
1615
    }
1616
 
1617
    /**
1618
     * Renders an action_link object.
1619
     *
1620
     * The provided link is renderer and the HTML returned. At the same time the
1621
     * associated actions are setup in JS by {@see core_renderer::add_action_handler()}
1622
     *
1623
     * @param action_link $link
1624
     * @return string HTML fragment
1625
     */
1626
    protected function render_action_link(action_link $link) {
1627
        return $this->render_from_template('core/action_link', $link->export_for_template($this));
1628
    }
1629
 
1630
    /**
1631
     * Renders an action_icon.
1632
     *
1633
     * This function uses the {@see core_renderer::action_link()} method for the
1634
     * most part. What it does different is prepare the icon as HTML and use it
1635
     * as the link text.
1636
     *
1637
     * Theme developers: If you want to change how action links and/or icons are rendered,
1638
     * consider overriding function {@see core_renderer::render_action_link()} and
1639
     * {@see core_renderer::render_pix_icon()}.
1640
     *
1641
     * @param string|moodle_url $url A string URL or moodel_url
1642
     * @param pix_icon $pixicon
1643
     * @param null|component_action $action
1644
     * @param null|array $attributes associative array of html link attributes + disabled
1645
     * @param bool $linktext show title next to image in link
1646
     * @return string HTML fragment
1647
     */
1648
    public function action_icon(
1649
        $url,
1650
        pix_icon $pixicon,
1651
        ?component_action $action = null,
1652
        ?array $attributes = null,
1653
        $linktext = false,
1654
    ) {
1655
        if (!($url instanceof moodle_url)) {
1656
            $url = new moodle_url($url);
1657
        }
1658
        $attributes = (array)$attributes;
1659
 
1660
        if (empty($attributes['class'])) {
1661
            // let ppl override the class via $options
1662
            $attributes['class'] = 'action-icon';
1663
        }
1664
 
1665
        $text = $pixicon->attributes['alt'];
1666
        // Set the icon as a decorative image. The accessible label should be within the link itself and not the icon.
1667
        $pixicon->attributes['alt'] = '';
1668
        $pixicon->attributes['title'] = '';
1669
        $pixicon->attributes['aria-hidden'] = 'true';
1670
 
1671
        $attributes['class'] .= ' mx-1 p-1';
1672
        if (!$linktext) {
1673
            // Set a title attribute on the link for sighted users if no text is shown.
1674
            $attributes['title'] = $text;
1675
            // Style the icon button for increased target area.
1676
            $attributes['class'] .= ' btn btn-link icon-no-margin';
1677
            // Make the action text only available to screen readers.
1678
            $attributes['aria-label'] = $text;
1679
            $text = '';
1680
        }
1681
 
1682
        $icon = $this->render($pixicon);
1683
 
1684
        return $this->action_link($url, $text . $icon, $action, $attributes);
1685
    }
1686
 
1687
   /**
1688
    * Print a message along with button choices for Continue/Cancel
1689
    *
1690
    * If a string or moodle_url is given instead of a single_button, method defaults to post.
1691
    *
1692
    * @param string $message The question to ask the user
1693
    * @param single_button|moodle_url|string $continue The single_button component representing the Continue answer. Can also be a moodle_url or string URL
1694
    * @param single_button|moodle_url|string $cancel The single_button component representing the Cancel answer. Can also be a moodle_url or string URL
1695
    * @param array $displayoptions optional extra display options
1696
    * @return string HTML fragment
1697
    */
1698
    public function confirm($message, $continue, $cancel, array $displayoptions = []) {
1699
 
1700
        // Check existing displayoptions.
1701
        $displayoptions['confirmtitle'] = $displayoptions['confirmtitle'] ?? get_string('confirm');
1702
        $displayoptions['continuestr'] = $displayoptions['continuestr'] ?? get_string('continue');
1703
        $displayoptions['cancelstr'] = $displayoptions['cancelstr'] ?? get_string('cancel');
1704
 
1705
        if ($continue instanceof single_button) {
1706
            // Continue button should be primary if set to secondary type as it is the fefault.
1707
            if ($continue->type === single_button::BUTTON_SECONDARY) {
1708
                $continue->type = single_button::BUTTON_PRIMARY;
1709
            }
1710
        } else if (is_string($continue)) {
1711
            $continue = new single_button(
1712
                new moodle_url($continue),
1713
                $displayoptions['continuestr'],
1714
                'post',
1715
                $displayoptions['type'] ?? single_button::BUTTON_PRIMARY
1716
            );
1717
        } else if ($continue instanceof moodle_url) {
1718
            $continue = new single_button(
1719
                $continue,
1720
                $displayoptions['continuestr'],
1721
                'post',
1722
                $displayoptions['type'] ?? single_button::BUTTON_PRIMARY
1723
            );
1724
        } else {
1725
            throw new coding_exception('The continue param to $OUTPUT->confirm() must be either a URL (string/moodle_url) or a single_button instance.');
1726
        }
1727
 
1728
        if ($cancel instanceof single_button) {
1729
            // ok
1730
        } else if (is_string($cancel)) {
1731
            $cancel = new single_button(new moodle_url($cancel), $displayoptions['cancelstr'], 'get');
1732
        } else if ($cancel instanceof moodle_url) {
1733
            $cancel = new single_button($cancel, $displayoptions['cancelstr'], 'get');
1734
        } else {
1735
            throw new coding_exception('The cancel param to $OUTPUT->confirm() must be either a URL (string/moodle_url) or a single_button instance.');
1736
        }
1737
 
1738
        $attributes = [
1739
            'role' => 'alertdialog',
1740
            'aria-labelledby' => 'modal-header',
1741
            'aria-describedby' => 'modal-body',
1742
            'aria-modal' => 'true',
1743
        ];
1744
 
1745
        $output = $this->box_start('generalbox modal modal-dialog modal-in-page show', 'notice', $attributes);
1746
        $output .= $this->box_start('modal-content', 'modal-content');
1747
        $output .= $this->box_start('modal-header px-3', 'modal-header');
1748
        $output .= html_writer::tag('h4', $displayoptions['confirmtitle']);
1749
        $output .= $this->box_end();
1750
        $attributes = [
1751
            'role' => 'alert',
1752
            'data-aria-autofocus' => 'true',
1753
        ];
1754
        $output .= $this->box_start('modal-body', 'modal-body', $attributes);
1755
        $output .= html_writer::tag('p', $message);
1756
        $output .= $this->box_end();
1757
        $output .= $this->box_start('modal-footer', 'modal-footer');
1758
        $output .= html_writer::tag('div', $this->render($cancel) . $this->render($continue), ['class' => 'buttons']);
1759
        $output .= $this->box_end();
1760
        $output .= $this->box_end();
1761
        $output .= $this->box_end();
1762
        return $output;
1763
    }
1764
 
1765
    /**
1766
     * Returns a form with a single button.
1767
     *
1768
     * Theme developers: DO NOT OVERRIDE! Please override function
1769
     * {@see core_renderer::render_single_button()} instead.
1770
     *
1771
     * @param string|moodle_url $url
1772
     * @param string $label button text
1773
     * @param string $method get or post submit method
1774
     * @param null|array $options associative array {disabled, title, etc.}
1775
     * @return string HTML fragment
1776
     */
1777
    public function single_button($url, $label, $method = 'post', ?array $options = null) {
1778
        if (!($url instanceof moodle_url)) {
1779
            $url = new moodle_url($url);
1780
        }
1781
        $button = new single_button($url, $label, $method);
1782
 
1783
        foreach ((array)$options as $key => $value) {
1784
            if (property_exists($button, $key)) {
1785
                $button->$key = $value;
1786
            } else {
1787
                $button->set_attribute($key, $value);
1788
            }
1789
        }
1790
 
1791
        return $this->render($button);
1792
    }
1793
 
1794
    /**
1795
     * Renders a single button widget.
1796
     *
1797
     * This will return HTML to display a form containing a single button.
1798
     *
1799
     * @param single_button $button
1800
     * @return string HTML fragment
1801
     */
1802
    protected function render_single_button(single_button $button) {
1803
        return $this->render_from_template('core/single_button', $button->export_for_template($this));
1804
    }
1805
 
1806
    /**
1807
     * Returns a form with a single select widget.
1808
     *
1809
     * Theme developers: DO NOT OVERRIDE! Please override function
1810
     * {@see core_renderer::render_single_select()} instead.
1811
     *
1812
     * @param moodle_url $url form action target, includes hidden fields
1813
     * @param string $name name of selection field - the changing parameter in url
1814
     * @param array $options list of options
1815
     * @param string $selected selected element
1816
     * @param array $nothing
1817
     * @param string $formid
1818
     * @param array $attributes other attributes for the single select
1819
     * @return string HTML fragment
1820
     */
1821
    public function single_select(
1822
        $url,
1823
        $name,
1824
        array $options,
1825
        $selected = '',
1826
        $nothing = ['' => 'choosedots'],
1827
        $formid = null,
1828
        $attributes = []
1829
    ) {
1830
        if (!($url instanceof moodle_url)) {
1831
            $url = new moodle_url($url);
1832
        }
1833
        $select = new single_select($url, $name, $options, $selected, $nothing, $formid);
1834
 
1835
        if (array_key_exists('label', $attributes)) {
1836
            $select->set_label($attributes['label']);
1837
            unset($attributes['label']);
1838
        }
1839
        $select->attributes = $attributes;
1840
 
1841
        return $this->render($select);
1842
    }
1843
 
1844
    /**
1845
     * Returns a dataformat selection and download form
1846
     *
1847
     * @param string $label A text label
1848
     * @param moodle_url|string $base The download page url
1849
     * @param string $name The query param which will hold the type of the download
1850
     * @param array $params Extra params sent to the download page
1851
     * @return string HTML fragment
1852
     */
1853
    public function download_dataformat_selector($label, $base, $name = 'dataformat', $params = []) {
1854
        $formats = plugin_manager::instance()->get_plugins_of_type('dataformat');
1855
        $options = [];
1856
        foreach ($formats as $format) {
1857
            if ($format->is_enabled()) {
1858
                $options[] = [
1859
                    'value' => $format->name,
1860
                    'label' => get_string('dataformat', $format->component),
1861
                ];
1862
            }
1863
        }
1864
        $hiddenparams = [];
1865
        foreach ($params as $key => $value) {
1866
            $hiddenparams[] = [
1867
                'name' => $key,
1868
                'value' => $value,
1869
            ];
1870
        }
1871
        $data = [
1872
            'label' => $label,
1873
            'base' => $base,
1874
            'name' => $name,
1875
            'params' => $hiddenparams,
1876
            'options' => $options,
1877
            'sesskey' => sesskey(),
1878
            'submit' => get_string('download'),
1879
        ];
1880
 
1881
        return $this->render_from_template('core/dataformat_selector', $data);
1882
    }
1883
 
1884
 
1885
    /**
1886
     * Internal implementation of single_select rendering
1887
     *
1888
     * @param single_select $select
1889
     * @return string HTML fragment
1890
     */
1891
    protected function render_single_select(single_select $select) {
1892
        return $this->render_from_template('core/single_select', $select->export_for_template($this));
1893
    }
1894
 
1895
    /**
1896
     * Returns a form with a url select widget.
1897
     *
1898
     * Theme developers: DO NOT OVERRIDE! Please override function
1899
     * {@see core_renderer::render_url_select()} instead.
1900
     *
1901
     * @param array $urls list of urls - array('/course/view.php?id=1'=>'Frontpage', ....)
1902
     * @param string $selected selected element
1903
     * @param array $nothing
1904
     * @param string $formid
1905
     * @return string HTML fragment
1906
     */
1907
    public function url_select(array $urls, $selected, $nothing = ['' => 'choosedots'], $formid = null) {
1908
        $select = new url_select($urls, $selected, $nothing, $formid);
1909
        return $this->render($select);
1910
    }
1911
 
1912
    /**
1913
     * Internal implementation of url_select rendering
1914
     *
1915
     * @param url_select $select
1916
     * @return string HTML fragment
1917
     */
1918
    protected function render_url_select(url_select $select) {
1919
        return $this->render_from_template('core/url_select', $select->export_for_template($this));
1920
    }
1921
 
1922
    /**
1923
     * Returns a string containing a link to the user documentation.
1924
     * Also contains an icon by default. Shown to teachers and admin only.
1925
     *
1926
     * @param string $path The page link after doc root and language, no leading slash.
1927
     * @param string $text The text to be displayed for the link
1928
     * @param boolean $forcepopup Whether to force a popup regardless of the value of $CFG->doctonewwindow
1929
     * @param array $attributes htm attributes
1930
     * @return string
1931
     */
1932
    public function doc_link($path, $text = '', $forcepopup = false, array $attributes = []) {
1933
        global $CFG;
1934
 
1935
        $icon = $this->pix_icon('book', '', 'moodle');
1936
 
1937
        $attributes['href'] = new moodle_url(get_docs_url($path));
1938
        $newwindowicon = '';
1939
        if (!empty($CFG->doctonewwindow) || $forcepopup) {
1940
            $attributes['target'] = '_blank';
1941
            $newwindowicon = $this->pix_icon(
1942
                'i/externallink',
1943
                get_string('opensinnewwindow'),
1944
                'moodle',
1945
                ['class' => 'fa fa-externallink fa-fw']
1946
            );
1947
        }
1948
 
1949
        return html_writer::tag('a', $icon . $text . $newwindowicon, $attributes);
1950
    }
1951
 
1952
    /**
1953
     * Return HTML for an image_icon.
1954
     *
1955
     * Theme developers: DO NOT OVERRIDE! Please override function
1956
     * {@see core_renderer::render_image_icon()} instead.
1957
     *
1958
     * @param string $pix short pix name
1959
     * @param string $alt mandatory alt attribute
1960
     * @param string $component standard compoennt name like 'moodle', 'mod_forum', etc.
1961
     * @param null|array $attributes htm attributes
1962
     * @return string HTML fragment
1963
     */
1964
    public function image_icon($pix, $alt, $component = 'moodle', ?array $attributes = null) {
1965
        $icon = new image_icon($pix, $alt, $component, $attributes);
1966
        return $this->render($icon);
1967
    }
1968
 
1969
    /**
1970
     * Renders a pix_icon widget and returns the HTML to display it.
1971
     *
1972
     * @param image_icon $icon
1973
     * @return string HTML fragment
1974
     */
1975
    protected function render_image_icon(image_icon $icon) {
1976
        $system = icon_system::instance(icon_system::STANDARD);
1977
        return $system->render_pix_icon($this, $icon);
1978
    }
1979
 
1980
    /**
1981
     * Return HTML for a pix_icon.
1982
     *
1983
     * Theme developers: DO NOT OVERRIDE! Please override function
1984
     * {@see core_renderer::render_pix_icon()} instead.
1985
     *
1986
     * @param string $pix short pix name
1987
     * @param string $alt mandatory alt attribute
1988
     * @param string $component standard compoennt name like 'moodle', 'mod_forum', etc.
1989
     * @param null|array $attributes htm lattributes
1990
     * @return string HTML fragment
1991
     */
1992
    public function pix_icon($pix, $alt, $component = 'moodle', ?array $attributes = null) {
1993
        $icon = new pix_icon($pix, $alt, $component, $attributes);
1994
        return $this->render($icon);
1995
    }
1996
 
1997
    /**
1998
     * Renders a pix_icon widget and returns the HTML to display it.
1999
     *
2000
     * @param pix_icon $icon
2001
     * @return string HTML fragment
2002
     */
2003
    protected function render_pix_icon(pix_icon $icon) {
2004
        $system = icon_system::instance();
2005
        return $system->render_pix_icon($this, $icon);
2006
    }
2007
 
2008
    /**
2009
     * Return HTML to display an emoticon icon.
2010
     *
2011
     * @param pix_emoticon $emoticon
2012
     * @return string HTML fragment
2013
     */
2014
    protected function render_pix_emoticon(pix_emoticon $emoticon) {
2015
        $system = icon_system::instance(icon_system::STANDARD);
2016
        return $system->render_pix_icon($this, $emoticon);
2017
    }
2018
 
2019
    /**
2020
     * Produces the html that represents this rating in the UI
2021
     *
2022
     * @param rating $rating the page object on which this rating will appear
2023
     * @return string
2024
     */
2025
    function render_rating(rating $rating) {
2026
        global $CFG, $USER;
2027
 
2028
        if ($rating->settings->aggregationmethod == RATING_AGGREGATE_NONE) {
2029
            return null;// ratings are turned off
2030
        }
2031
 
2032
        $ratingmanager = new rating_manager();
2033
        // Initialise the JavaScript so ratings can be done by AJAX.
2034
        $ratingmanager->initialise_rating_javascript($this->page);
2035
 
2036
        $strrate = get_string("rate", "rating");
2037
        $ratinghtml = ''; // the string we'll return
2038
 
2039
        // permissions check - can they view the aggregate?
2040
        if ($rating->user_can_view_aggregate()) {
2041
            $aggregatelabel = $ratingmanager->get_aggregate_label($rating->settings->aggregationmethod);
2042
            $aggregatelabel = html_writer::tag('span', $aggregatelabel, ['class' => 'rating-aggregate-label']);
2043
            $aggregatestr   = $rating->get_aggregate_string();
2044
 
2045
            $aggregatehtml  = html_writer::tag('span', $aggregatestr, ['id' => 'ratingaggregate' . $rating->itemid, 'class' => 'ratingaggregate']) . ' ';
2046
            if ($rating->count > 0) {
2047
                $countstr = "({$rating->count})";
2048
            } else {
2049
                $countstr = '-';
2050
            }
2051
            $aggregatehtml .= html_writer::tag('span', $countstr, ['id' => "ratingcount{$rating->itemid}", 'class' => 'ratingcount']) . ' ';
2052
 
2053
            if ($rating->settings->permissions->viewall && $rating->settings->pluginpermissions->viewall) {
2054
                $nonpopuplink = $rating->get_view_ratings_url();
2055
                $popuplink = $rating->get_view_ratings_url(true);
2056
 
2057
                $action = new popup_action('click', $popuplink, 'ratings', ['height' => 400, 'width' => 600]);
2058
                $aggregatehtml = $this->action_link($nonpopuplink, $aggregatehtml, $action);
2059
            }
2060
 
2061
            $ratinghtml .= html_writer::tag('span', $aggregatelabel . $aggregatehtml, ['class' => 'rating-aggregate-container']);
2062
        }
2063
 
2064
        $formstart = null;
2065
        // if the item doesn't belong to the current user, the user has permission to rate
2066
        // and we're within the assessable period
2067
        if ($rating->user_can_rate()) {
2068
            $rateurl = $rating->get_rate_url();
2069
            $inputs = $rateurl->params();
2070
 
2071
            // start the rating form
2072
            $formattrs = [
2073
                'id'     => "postrating{$rating->itemid}",
2074
                'class'  => 'postratingform',
2075
                'method' => 'post',
2076
                'action' => $rateurl->out_omit_querystring(),
2077
            ];
2078
            $formstart  = html_writer::start_tag('form', $formattrs);
2079
            $formstart .= html_writer::start_tag('div', ['class' => 'ratingform hstack gap-2']);
2080
 
2081
            // add the hidden inputs
2082
            foreach ($inputs as $name => $value) {
2083
                $attributes = ['type' => 'hidden', 'class' => 'ratinginput', 'name' => $name, 'value' => $value];
2084
                $formstart .= html_writer::empty_tag('input', $attributes);
2085
            }
2086
 
2087
            if (empty($ratinghtml)) {
2088
                $ratinghtml .= $strrate . ': ';
2089
            }
2090
            $ratinghtml = $formstart . $ratinghtml;
2091
 
2092
            $scalearray = [RATING_UNSET_RATING => $strrate . '...'] + $rating->settings->scale->scaleitems;
2093
            $scaleattrs = ['class' => 'postratingmenu ratinginput', 'id' => 'menurating' . $rating->itemid];
2094
            $ratinghtml .= html_writer::label($rating->rating, 'menurating' . $rating->itemid, false, ['class' => 'accesshide']);
2095
            $ratinghtml .= html_writer::select($scalearray, 'rating', $rating->rating, false, $scaleattrs);
2096
 
2097
            // output submit button
2098
            $ratinghtml .= html_writer::start_tag('span', ['class' => "ratingsubmit"]);
2099
 
2100
            $attributes = ['type' => 'submit', 'class' => 'postratingmenusubmit', 'id' => 'postratingsubmit' . $rating->itemid, 'value' => s(get_string('rate', 'rating'))];
2101
            $ratinghtml .= html_writer::empty_tag('input', $attributes);
2102
 
2103
            if (!$rating->settings->scale->isnumeric) {
2104
                // If a global scale, try to find current course ID from the context
2105
                if (empty($rating->settings->scale->courseid) and $coursecontext = $rating->context->get_course_context(false)) {
2106
                    $courseid = $coursecontext->instanceid;
2107
                } else {
2108
                    $courseid = $rating->settings->scale->courseid;
2109
                }
2110
                $ratinghtml .= $this->help_icon_scale($courseid, $rating->settings->scale);
2111
            }
2112
            $ratinghtml .= html_writer::end_tag('span');
2113
            $ratinghtml .= html_writer::end_tag('div');
2114
            $ratinghtml .= html_writer::end_tag('form');
2115
        }
2116
 
2117
        return $ratinghtml;
2118
    }
2119
 
2120
    /**
2121
     * Centered heading with attached help button (same title text)
2122
     * and optional icon attached.
2123
     *
2124
     * @param string $text A heading text
2125
     * @param string $helpidentifier The keyword that defines a help page
2126
     * @param string $component component name
2127
     * @param string|moodle_url $icon
2128
     * @param string $iconalt icon alt text
2129
     * @param int $level The level of importance of the heading. Defaulting to 2
2130
     * @param string $classnames A space-separated list of CSS classes. Defaulting to null
2131
     * @return string HTML fragment
2132
     */
2133
    public function heading_with_help($text, $helpidentifier, $component = 'moodle', $icon = '', $iconalt = '', $level = 2, $classnames = null) {
2134
        $image = '';
2135
        if ($icon) {
2136
            $image = $this->pix_icon($icon, $iconalt, $component);
2137
        }
2138
 
2139
        $help = '';
2140
        if ($helpidentifier) {
2141
            $help = $this->help_icon($helpidentifier, $component);
2142
        }
2143
 
2144
        return $this->heading($image . $text . $help, $level, $classnames);
2145
    }
2146
 
2147
    /**
2148
     * Returns HTML to display a help icon.
2149
     *
2150
     * Theme developers: DO NOT OVERRIDE! Please override function
2151
     * {@see core_renderer::render_help_icon()} instead.
2152
     *
2153
     * @param string $identifier The keyword that defines a help page
2154
     * @param string $component component name
2155
     * @param string|bool $linktext true means use $title as link text, string means link text value
2156
     * @param string|object|array|int $a An object, string or number that can be used
2157
     *      within translation strings
2158
     * @return string HTML fragment
2159
     */
2160
    public function help_icon($identifier, $component = 'moodle', $linktext = '', $a = null) {
2161
        $icon = new help_icon($identifier, $component, $a);
2162
        $icon->diag_strings();
2163
        if ($linktext === true) {
2164
            $icon->linktext = get_string($icon->identifier, $icon->component, $a);
2165
        } else if (!empty($linktext)) {
2166
            $icon->linktext = $linktext;
2167
        }
2168
        return $this->render($icon);
2169
    }
2170
 
2171
    /**
2172
     * Implementation of user image rendering.
2173
     *
2174
     * @param help_icon $helpicon A help icon instance
2175
     * @return string HTML fragment
2176
     */
2177
    protected function render_help_icon(help_icon $helpicon) {
2178
        $context = $helpicon->export_for_template($this);
2179
        return $this->render_from_template('core/help_icon', $context);
2180
    }
2181
 
2182
    /**
2183
     * Returns HTML to display a scale help icon.
2184
     *
2185
     * @param int $courseid
2186
     * @param stdClass $scale instance
2187
     * @return string HTML fragment
2188
     */
2189
    public function help_icon_scale($courseid, stdClass $scale) {
2190
        global $CFG;
2191
 
2192
        $title = get_string('helpprefix2', '', $scale->name) . ' (' . get_string('newwindow') . ')';
2193
 
2194
        $icon = $this->pix_icon('help', get_string('scales'), 'moodle');
2195
 
2196
        $scaleid = abs($scale->id);
2197
 
2198
        $link = new moodle_url('/course/scales.php', ['id' => $courseid, 'list' => true, 'scaleid' => $scaleid]);
2199
        $action = new popup_action('click', $link, 'ratingscale');
2200
 
2201
        return html_writer::tag('span', $this->action_link($link, $icon, $action), ['class' => 'helplink']);
2202
    }
2203
 
2204
    /**
2205
     * Creates and returns a spacer image with optional line break.
2206
     *
2207
     * @param null|array $attributes Any HTML attributes to add to the spaced.
2208
     * @param bool $br Include a BR after the spacer.... DON'T USE THIS. Don't be
2209
     *     laxy do it with CSS which is a much better solution.
2210
     * @return string HTML fragment
2211
     */
2212
    public function spacer(?array $attributes = null, $br = false) {
2213
        $attributes = (array)$attributes;
2214
        if (empty($attributes['width'])) {
2215
            $attributes['width'] = 1;
2216
        }
2217
        if (empty($attributes['height'])) {
2218
            $attributes['height'] = 1;
2219
        }
2220
        $attributes['class'] = 'spacer';
2221
 
2222
        $output = $this->pix_icon('spacer', '', 'moodle', $attributes);
2223
 
2224
        if (!empty($br)) {
2225
            $output .= '<br />';
2226
        }
2227
 
2228
        return $output;
2229
    }
2230
 
2231
    /**
2232
     * Returns HTML to display the specified user's avatar.
2233
     *
2234
     * User avatar may be obtained in two ways:
2235
     * <pre>
2236
     * // Option 1: (shortcut for simple cases, preferred way)
2237
     * // $user has come from the DB and has fields id, picture, imagealt, firstname and lastname
2238
     * $OUTPUT->user_picture($user, array('popup'=>true));
2239
     *
2240
     * // Option 2:
2241
     * $userpic = new user_picture($user);
2242
     * // Set properties of $userpic
2243
     * $userpic->popup = true;
2244
     * $OUTPUT->render($userpic);
2245
     * </pre>
2246
     *
2247
     * Theme developers: DO NOT OVERRIDE! Please override function
2248
     * {@see core_renderer::render_user_picture()} instead.
2249
     *
2250
     * @param stdClass $user Object with at least fields id, picture, imagealt, firstname, lastname
2251
     *     If any of these are missing, the database is queried. Avoid this
2252
     *     if at all possible, particularly for reports. It is very bad for performance.
2253
     * @param null|array $options associative array with user picture options, used only if not a user_picture object,
2254
     *     options are:
2255
     *     - courseid=$this->page->course->id (course id of user profile in link)
2256
     *     - size=35 (size of image)
2257
     *     - link=true (make image clickable - the link leads to user profile)
2258
     *     - popup=false (open in popup)
2259
     *     - alttext=true (add image alt attribute)
2260
     *     - class = image class attribute (default 'userpicture')
2261
     *     - visibletoscreenreaders=true (whether to be visible to screen readers)
2262
     *     - includefullname=false (whether to include the user's full name together with the user picture)
2263
     *     - includetoken = false (whether to use a token for authentication. True for current user, int value for other user id)
2264
     * @return string HTML fragment
2265
     */
2266
    public function user_picture(stdClass $user, ?array $options = null) {
2267
        $userpicture = new user_picture($user);
2268
        foreach ((array)$options as $key => $value) {
2269
            if (property_exists($userpicture, $key)) {
2270
                $userpicture->$key = $value;
2271
            }
2272
        }
2273
        return $this->render($userpicture);
2274
    }
2275
 
2276
    /**
2277
     * Internal implementation of user image rendering.
2278
     *
2279
     * @param user_picture $userpicture
2280
     * @return string
2281
     */
2282
    protected function render_user_picture(user_picture $userpicture) {
2283
        global $CFG;
2284
 
2285
        $user = $userpicture->user;
2286
        $canviewfullnames = has_capability('moodle/site:viewfullnames', $this->page->context);
2287
 
2288
        $alt = '';
2289
        if ($userpicture->alttext) {
2290
            if (!empty($user->imagealt)) {
2291
                $alt = trim($user->imagealt);
2292
            }
2293
        }
2294
 
2295
        // If the user picture is being rendered as a link but without the full name, an empty alt text for the user picture
2296
        // would mean that the link displayed will not have any discernible text. This becomes an accessibility issue,
2297
        // especially to screen reader users. Use the user's full name by default for the user picture's alt-text if this is
2298
        // the case.
2299
        if ($userpicture->link && !$userpicture->includefullname && empty($alt)) {
2300
            $alt = fullname($user);
2301
        }
2302
 
2303
        if (empty($userpicture->size)) {
2304
            $size = 35;
2305
        } else if ($userpicture->size === true or $userpicture->size == 1) {
2306
            $size = 100;
2307
        } else {
2308
            $size = $userpicture->size;
2309
        }
2310
 
2311
        $class = $userpicture->class;
2312
 
2313
        if ($user->picture == 0) {
2314
            $class .= ' defaultuserpic';
2315
        }
2316
 
2317
        $src = $userpicture->get_url($this->page, $this);
2318
 
2319
        $attributes = ['src' => $src, 'class' => $class, 'width' => $size, 'height' => $size];
2320
        if (!$userpicture->visibletoscreenreaders) {
2321
            $alt = '';
2322
        }
2323
        $attributes['alt'] = $alt;
2324
 
2325
        if (!empty($alt)) {
2326
            $attributes['title'] = $alt;
2327
        }
2328
 
2329
        // Get the image html output first, auto generated based on initials if one isn't already set.
2330
        if ($user->picture == 0 && empty($CFG->enablegravatar) && !defined('BEHAT_SITE_RUNNING')) {
2331
            $initials = \core_user::get_initials($user);
2332
            $fullname = fullname($userpicture->user, $canviewfullnames);
2333
            // Don't modify in corner cases where neither the firstname nor the lastname appears.
2334
            $output = html_writer::tag(
2335
                'span',
2336
                $initials,
2337
                [
2338
                    'class' => 'userinitials size-' . $size,
2339
                    'title' => $fullname,
2340
                    'aria-label' => $fullname,
2341
                    'role' => 'img',
2342
                ]
2343
            );
2344
        } else {
2345
            $output = html_writer::empty_tag('img', $attributes);
2346
        }
2347
 
2348
        // Show fullname together with the picture when desired.
2349
        if ($userpicture->includefullname) {
2350
            $output .= fullname($userpicture->user, $canviewfullnames);
2351
        }
2352
 
2353
        if (empty($userpicture->courseid)) {
2354
            $courseid = $this->page->course->id;
2355
        } else {
2356
            $courseid = $userpicture->courseid;
2357
        }
2358
        if ($courseid == SITEID) {
2359
            $url = new moodle_url('/user/profile.php', ['id' => $user->id]);
2360
        } else {
2361
            $url = new moodle_url('/user/view.php', ['id' => $user->id, 'course' => $courseid]);
2362
        }
2363
 
2364
        // Then wrap it in link if needed. Also we don't wrap it in link if the link redirects to itself.
2365
        if (
2366
            !$userpicture->link ||
2367
                ($this->page->has_set_url() && $this->page->url == $url)
2368
        ) { // Protect against unset page->url.
2369
            return $output;
2370
        }
2371
 
2372
        $attributes = ['href' => $url, 'class' => 'd-inline-block aabtn'];
2373
        if (!$userpicture->visibletoscreenreaders) {
2374
            $attributes['tabindex'] = '-1';
2375
            $attributes['aria-hidden'] = 'true';
2376
        }
2377
 
2378
        if ($userpicture->popup) {
2379
            $id = html_writer::random_id('userpicture');
2380
            $attributes['id'] = $id;
2381
            $this->add_action_handler(new popup_action('click', $url), $id);
2382
        }
2383
 
2384
        return html_writer::tag('a', $output, $attributes);
2385
    }
2386
 
2387
    /**
2388
     * @deprecated since Moodle 4.3
2389
     */
2390
    public function htmllize_file_tree() {
2391
        throw new coding_exception('This function is deprecated and no longer relevant.');
2392
    }
2393
 
2394
    /**
2395
     * Returns HTML to display the file picker
2396
     *
2397
     * <pre>
2398
     * $OUTPUT->file_picker($options);
2399
     * </pre>
2400
     *
2401
     * Theme developers: DO NOT OVERRIDE! Please override function
2402
     * {@see core_renderer::render_file_picker()} instead.
2403
     *
2404
     * @param stdClass $options file manager options
2405
     *   options are:
2406
     *       maxbytes=>-1,
2407
     *       itemid=>0,
2408
     *       client_id=>uniqid(),
2409
     *       acepted_types=>'*',
2410
     *       return_types=>FILE_INTERNAL,
2411
     *       context=>current page context
2412
     * @return string HTML fragment
2413
     */
2414
    public function file_picker($options) {
2415
        $fp = new file_picker($options);
2416
        return $this->render($fp);
2417
    }
2418
 
2419
    /**
2420
     * Internal implementation of file picker rendering.
2421
     *
2422
     * @param file_picker $fp
2423
     * @return string
2424
     */
2425
    public function render_file_picker(file_picker $fp) {
2426
        $options = $fp->options;
2427
        $client_id = $options->client_id;
2428
        $strsaved = get_string('filesaved', 'repository');
2429
        $straddfile = get_string('openpicker', 'repository');
2430
        $strloading  = get_string('loading', 'repository');
2431
        $strdndenabled = get_string('dndenabled_inbox', 'moodle');
2432
        $strdroptoupload = get_string('droptoupload', 'moodle');
2433
        $iconprogress = $this->pix_icon('i/loading_small', $strloading) . '';
2434
 
2435
        $currentfile = $options->currentfile;
2436
        if (empty($currentfile)) {
2437
            $currentfile = '';
2438
        } else {
2439
            $currentfile .= ' - ';
2440
        }
2441
        if ($options->maxbytes) {
2442
            $size = $options->maxbytes;
2443
        } else {
2444
            $size = get_max_upload_file_size();
2445
        }
2446
        if ($size == -1) {
2447
            $maxsize = '';
2448
        } else {
2449
            $maxsize = get_string('maxfilesize', 'moodle', display_size($size, 0));
2450
        }
2451
        if ($options->buttonname) {
2452
            $buttonname = ' name="' . $options->buttonname . '"';
2453
        } else {
2454
            $buttonname = '';
2455
        }
2456
        $html = <<<EOD
2457
<div class="filemanager-loading mdl-align" id='filepicker-loading-{$client_id}'>
2458
$iconprogress
2459
</div>
2460
<div id="filepicker-wrapper-{$client_id}" class="mdl-left w-100" style="display:none">
2461
    <div>
2462
        <input type="button" class="btn btn-secondary fp-btn-choose" id="filepicker-button-{$client_id}" value="{$straddfile}"{$buttonname}/>
2463
        <span> $maxsize </span>
2464
    </div>
2465
EOD;
2466
        if ($options->env != 'url') {
2467
            $html .= <<<EOD
2468
    <div id="file_info_{$client_id}" class="mdl-left filepicker-filelist" style="position: relative">
2469
    <div class="filepicker-filename">
2470
        <div class="filepicker-container">$currentfile
2471
            <div class="dndupload-message">$strdndenabled <br/>
2472
                <div class="dndupload-arrow d-flex"><i class="fa fa-arrow-circle-o-down fa-3x m-auto"></i></div>
2473
            </div>
2474
        </div>
2475
        <div class="dndupload-progressbars"></div>
2476
    </div>
2477
    <div>
2478
        <div class="dndupload-target">{$strdroptoupload}<br/>
2479
            <div class="dndupload-arrow d-flex"><i class="fa fa-arrow-circle-o-down fa-3x m-auto"></i></div>
2480
        </div>
2481
    </div>
2482
    </div>
2483
EOD;
2484
        }
2485
        $html .= '</div>';
2486
        return $html;
2487
    }
2488
 
2489
    /**
2490
     * Returns HTML to display a "Turn editing on/off" button in a form.
2491
     *
2492
     * @param moodle_url $url The URL + params to send through when clicking the button
2493
     * @param string $method
2494
     * @return ?string HTML the button
2495
     */
2496
    public function edit_button(moodle_url $url, string $method = 'post') {
2497
 
2498
        if ($this->page->theme->haseditswitch == true) {
2499
            return;
2500
        }
2501
        $url->param('sesskey', sesskey());
2502
        if ($this->page->user_is_editing()) {
2503
            $url->param('edit', 'off');
2504
            $editstring = get_string('turneditingoff');
2505
        } else {
2506
            $url->param('edit', 'on');
2507
            $editstring = get_string('turneditingon');
2508
        }
2509
 
2510
        return $this->single_button($url, $editstring, $method);
2511
    }
2512
 
2513
    /**
2514
     * Create a navbar switch for toggling editing mode.
2515
     *
2516
     * @return ?string Html containing the edit switch
2517
     */
2518
    public function edit_switch() {
2519
        if ($this->page->user_allowed_editing()) {
2520
            $temp = (object) [
2521
                'legacyseturl' => (new moodle_url('/editmode.php'))->out(false),
2522
                'pagecontextid' => $this->page->context->id,
2523
                'pageurl' => $this->page->url,
2524
                'sesskey' => sesskey(),
2525
            ];
2526
            if ($this->page->user_is_editing()) {
2527
                $temp->checked = true;
2528
            }
2529
            return $this->render_from_template('core/editswitch', $temp);
2530
        }
2531
    }
2532
 
2533
    /**
2534
     * Returns HTML to display a simple button to close a window
2535
     *
2536
     * @param string $text The lang string for the button's label (already output from get_string())
2537
     * @return string html fragment
2538
     */
2539
    public function close_window_button($text = '') {
2540
        if (empty($text)) {
2541
            $text = get_string('closewindow');
2542
        }
2543
        $button = new single_button(new moodle_url('#'), $text, 'get');
2544
        $button->add_action(new component_action('click', 'close_window'));
2545
 
2546
        return $this->container($this->render($button), 'closewindow');
2547
    }
2548
 
2549
    /**
2550
     * Output an error message. By default wraps the error message in <span class="error">.
2551
     * If the error message is blank, nothing is output.
2552
     *
2553
     * @param string $message the error message.
2554
     * @return string the HTML to output.
2555
     */
2556
    public function error_text($message) {
2557
        if (empty($message)) {
2558
            return '';
2559
        }
2560
        $message = $this->pix_icon('i/warning', get_string('error'), '', ['class' => 'icon', 'title' => '']) . $message;
2561
        return html_writer::tag('span', $message, ['class' => 'error']);
2562
    }
2563
 
2564
    /**
2565
     * Do not call this function directly.
2566
     *
2567
     * To terminate the current script with a fatal error, throw an exception.
2568
     * Doing this will then call this function to display the error, before terminating the execution.
2569
     *
2570
     * @param string $message The message to output
2571
     * @param string $moreinfourl URL where more info can be found about the error
2572
     * @param string $link Link for the Continue button
2573
     * @param array $backtrace The execution backtrace
2574
     * @param null|string $debuginfo Debugging information
2575
     * @param string $errorcode
2576
     * @return string the HTML to output.
2577
     */
2578
    public function fatal_error($message, $moreinfourl, $link, $backtrace, $debuginfo = null, $errorcode = "") {
2579
        global $CFG;
2580
 
2581
        $output = '';
2582
        $obbuffer = '';
2583
 
2584
        if ($this->has_started()) {
2585
            // we can not always recover properly here, we have problems with output buffering,
2586
            // html tables, etc.
2587
            $output .= $this->opencontainers->pop_all_but_last();
2588
        } else {
2589
            // It is really bad if library code throws exception when output buffering is on,
2590
            // because the buffered text would be printed before our start of page.
2591
            // NOTE: this hack might be behave unexpectedly in case output buffering is enabled in PHP.ini
2592
            error_reporting(0); // disable notices from gzip compression, etc.
2593
            while (ob_get_level() > 0) {
2594
                $buff = ob_get_clean();
2595
                if ($buff === false) {
2596
                    break;
2597
                }
2598
                $obbuffer .= $buff;
2599
            }
2600
            error_reporting($CFG->debug);
2601
 
2602
            // Output not yet started.
2603
            $protocol = (isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0');
2604
            if (empty($_SERVER['HTTP_RANGE'])) {
2605
                @header($protocol . ' 404 Not Found');
2606
            } else if (core_useragent::check_safari_ios_version(602) && !empty($_SERVER['HTTP_X_PLAYBACK_SESSION_ID'])) {
2607
                // Coax iOS 10 into sending the session cookie.
2608
                @header($protocol . ' 403 Forbidden');
2609
            } else {
2610
                // Must stop byteserving attempts somehow,
2611
                // this is weird but Chrome PDF viewer can be stopped only with 407!
2612
                @header($protocol . ' 407 Proxy Authentication Required');
2613
            }
2614
 
2615
            $this->page->set_context(null); // ugly hack - make sure page context is set to something, we do not want bogus warnings here
2616
            $this->page->set_url('/'); // no url
2617
            // $this->page->set_pagelayout('base'); //TODO: MDL-20676 blocks on error pages are weird, unfortunately it somehow detect the pagelayout from URL :-(
2618
            $this->page->set_title(get_string('error'));
2619
            $this->page->set_heading($this->page->course->fullname);
2620
            // No need to display the activity header when encountering an error.
2621
            $this->page->activityheader->disable();
2622
            $output .= $this->header();
2623
        }
2624
 
2625
        $message = '<p class="errormessage">' . s($message) . '</p>' .
2626
                '<p class="errorcode"><a href="' . s($moreinfourl) . '">' .
2627
                get_string('moreinformation') . '</a></p>';
2628
        if (empty($CFG->rolesactive)) {
2629
            $message .= '<p class="errormessage">' . get_string('installproblem', 'error') . '</p>';
2630
            // It is usually not possible to recover from errors triggered during installation, you may need to create a new database or use a different database prefix for new installation.
2631
        }
2632
        $output .= $this->box($message, 'errorbox alert alert-danger', null, ['data-rel' => 'fatalerror']);
2633
 
2634
        if ($CFG->debugdeveloper) {
2635
            $labelsep = get_string('labelsep', 'langconfig');
2636
            if (!empty($debuginfo)) {
2637
                $debuginfo = s($debuginfo); // removes all nasty JS
2638
                $debuginfo = str_replace("\n", '<br />', $debuginfo); // keep newlines
2639
                $label = get_string('debuginfo', 'debug') . $labelsep;
2640
                $output .= $this->notification("<strong>$label</strong> " . $debuginfo, 'notifytiny');
2641
            }
2642
            if (!empty($backtrace)) {
2643
                $label = get_string('stacktrace', 'debug') . $labelsep;
2644
                $output .= $this->notification("<strong>$label</strong> " . format_backtrace($backtrace), 'notifytiny');
2645
            }
2646
            if ($obbuffer !== '') {
2647
                $label = get_string('outputbuffer', 'debug') . $labelsep;
2648
                $output .= $this->notification("<strong>$label</strong> " . s($obbuffer), 'notifytiny');
2649
            }
2650
        }
2651
 
2652
        if (empty($CFG->rolesactive)) {
2653
            // continue does not make much sense if moodle is not installed yet because error is most probably not recoverable
2654
        } else if (!empty($link)) {
2655
            $output .= $this->continue_button($link);
2656
        }
2657
 
2658
        $output .= $this->footer();
2659
 
2660
        // Padding to encourage IE to display our error page, rather than its own.
2661
        $output .= str_repeat(' ', 512);
2662
 
2663
        return $output;
2664
    }
2665
 
2666
    /**
2667
     * Output a notification (that is, a status message about something that has just happened).
2668
     *
2669
     * Note: \core\notification::add() may be more suitable for your usage.
2670
     *
2671
     * @param string $message The message to print out.
2672
     * @param ?string $type   The type of notification. See constants on notification.
2673
     * @param bool $closebutton Whether to show a close icon to remove the notification (default true).
2674
     * @param ?string $title  The title of the notification.
2675
     * @param ?string $titleicon if the title should have an icon you can give the icon name with the component
2676
     *  (e.g. 'i/circleinfo, core' or 'i/circleinfo' if the icon is from core)
2677
     * @return string the HTML to output.
2678
     */
2679
    public function notification($message, $type = null, $closebutton = true, ?string $title = null, ?string $titleicon = null) {
2680
        $typemappings = [
2681
            // Valid types.
2682
            'success'           => notification::NOTIFY_SUCCESS,
2683
            'info'              => notification::NOTIFY_INFO,
2684
            'warning'           => notification::NOTIFY_WARNING,
2685
            'error'             => notification::NOTIFY_ERROR,
2686
 
2687
            // Legacy types mapped to current types.
2688
            'notifyproblem'     => notification::NOTIFY_ERROR,
2689
            'notifytiny'        => notification::NOTIFY_ERROR,
2690
            'notifyerror'       => notification::NOTIFY_ERROR,
2691
            'notifysuccess'     => notification::NOTIFY_SUCCESS,
2692
            'notifymessage'     => notification::NOTIFY_INFO,
2693
            'notifyredirect'    => notification::NOTIFY_INFO,
2694
            'redirectmessage'   => notification::NOTIFY_INFO,
2695
        ];
2696
 
2697
        $extraclasses = [];
2698
 
2699
        if ($type) {
2700
            if (strpos($type, ' ') === false) {
2701
                // No spaces in the list of classes, therefore no need to loop over and determine the class.
2702
                if (isset($typemappings[$type])) {
2703
                    $type = $typemappings[$type];
2704
                } else {
2705
                    // The value provided did not match a known type. It must be an extra class.
2706
                    $extraclasses = [$type];
2707
                }
2708
            } else {
2709
                // Identify what type of notification this is.
2710
                $classarray = explode(' ', self::prepare_classes($type));
2711
 
2712
                // Separate out the type of notification from the extra classes.
2713
                foreach ($classarray as $class) {
2714
                    if (isset($typemappings[$class])) {
2715
                        $type = $typemappings[$class];
2716
                    } else {
2717
                        $extraclasses[] = $class;
2718
                    }
2719
                }
2720
            }
2721
        }
2722
 
2723
        $notification = new notification($message, $type, $closebutton, $title, $titleicon);
2724
        if (count($extraclasses)) {
2725
            $notification->set_extra_classes($extraclasses);
2726
        }
2727
 
2728
        // Return the rendered template.
2729
        return $this->render_from_template($notification->get_template_name(), $notification->export_for_template($this));
2730
    }
2731
 
2732
    /**
2733
     * Render a notification (that is, a status message about something that has
2734
     * just happened).
2735
     *
2736
     * @param notification $notification the notification to print out
2737
     * @return string the HTML to output.
2738
     */
2739
    protected function render_notification(notification $notification) {
2740
        return $this->render_from_template($notification->get_template_name(), $notification->export_for_template($this));
2741
    }
2742
 
2743
    /**
2744
     * Returns HTML to display a continue button that goes to a particular URL.
2745
     *
2746
     * @param string|moodle_url $url The url the button goes to.
2747
     * @return string the HTML to output.
2748
     */
2749
    public function continue_button($url) {
2750
        if (!($url instanceof moodle_url)) {
2751
            $url = new moodle_url($url);
2752
        }
2753
        $button = new single_button($url, get_string('continue'), 'get', single_button::BUTTON_PRIMARY);
2754
        $button->class = 'continuebutton';
2755
 
2756
        return $this->render($button);
2757
    }
2758
 
2759
    /**
2760
     * Returns HTML to display a single paging bar to provide access to other pages  (usually in a search)
2761
     *
2762
     * Theme developers: DO NOT OVERRIDE! Please override function
2763
     * {@see core_renderer::render_paging_bar()} instead.
2764
     *
2765
     * @param int $totalcount The total number of entries available to be paged through
2766
     * @param int $page The page you are currently viewing
2767
     * @param int $perpage The number of entries that should be shown per page
2768
     * @param string|moodle_url $baseurl url of the current page, the $pagevar parameter is added
2769
     * @param string $pagevar name of page parameter that holds the page number
2770
     * @return string the HTML to output.
2771
     */
2772
    public function paging_bar($totalcount, $page, $perpage, $baseurl, $pagevar = 'page') {
2773
        $pb = new paging_bar($totalcount, $page, $perpage, $baseurl, $pagevar);
2774
        return $this->render($pb);
2775
    }
2776
 
2777
    /**
2778
     * Returns HTML to display the paging bar.
2779
     *
2780
     * @param paging_bar $pagingbar
2781
     * @return string the HTML to output.
2782
     */
2783
    protected function render_paging_bar(paging_bar $pagingbar) {
2784
        // Any more than 10 is not usable and causes weird wrapping of the pagination.
2785
        $pagingbar->maxdisplay = 10;
2786
        return $this->render_from_template('core/paging_bar', $pagingbar->export_for_template($this));
2787
    }
2788
 
2789
    /**
2790
     * Returns HTML to display initials bar to provide access to other pages  (usually in a search)
2791
     *
2792
     * @param string $current the currently selected letter.
2793
     * @param string $class class name to add to this initial bar.
2794
     * @param string $title the name to put in front of this initial bar.
2795
     * @param string $urlvar URL parameter name for this initial.
2796
     * @param string $url URL object.
2797
     * @param array $alpha of letters in the alphabet.
2798
     * @param bool $minirender Return a trimmed down view of the initials bar.
2799
     * @return string the HTML to output.
2800
     */
2801
    public function initials_bar($current, $class, $title, $urlvar, $url, $alpha = null, bool $minirender = false) {
2802
        $ib = new initials_bar($current, $class, $title, $urlvar, $url, $alpha, $minirender);
2803
        return $this->render($ib);
2804
    }
2805
 
2806
    /**
2807
     * Internal implementation of initials bar rendering.
2808
     *
2809
     * @param initials_bar $initialsbar
2810
     * @return string
2811
     */
2812
    protected function render_initials_bar(initials_bar $initialsbar) {
2813
        return $this->render_from_template('core/initials_bar', $initialsbar->export_for_template($this));
2814
    }
2815
 
2816
    /**
2817
     * Output the place a skip link goes to.
2818
     *
2819
     * @param string $id The target name from the corresponding $PAGE->requires->skip_link_to($target) call.
2820
     * @return string the HTML to output.
2821
     */
2822
    public function skip_link_target($id = null) {
2823
        return html_writer::span('', '', ['id' => $id]);
2824
    }
2825
 
2826
    /**
2827
     * Outputs a heading
2828
     *
2829
     * @param string $text The text of the heading
2830
     * @param int $level The level of importance of the heading. Defaulting to 2
2831
     * @param string $classes A space-separated list of CSS classes. Defaulting to null
2832
     * @param string $id An optional ID
2833
     * @return string the HTML to output.
2834
     */
2835
    public function heading($text, $level = 2, $classes = null, $id = null) {
2836
        $level = (int) $level;
2837
        if ($level < 1 or $level > 6) {
2838
            throw new coding_exception('Heading level must be an integer between 1 and 6.');
2839
        }
2840
        return html_writer::tag('h' . $level, $text, ['id' => $id, 'class' => renderer_base::prepare_classes($classes)]);
2841
    }
2842
 
2843
    /**
2844
     * Outputs a box.
2845
     *
2846
     * @param string $contents The contents of the box
2847
     * @param string $classes A space-separated list of CSS classes
2848
     * @param string $id An optional ID
2849
     * @param array $attributes An array of other attributes to give the box.
2850
     * @return string the HTML to output.
2851
     */
2852
    public function box($contents, $classes = 'generalbox', $id = null, $attributes = []) {
2853
        return $this->box_start($classes, $id, $attributes) . $contents . $this->box_end();
2854
    }
2855
 
2856
    /**
2857
     * Outputs the opening section of a box.
2858
     *
2859
     * @param string $classes A space-separated list of CSS classes
2860
     * @param string $id An optional ID
2861
     * @param array $attributes An array of other attributes to give the box.
2862
     * @return string the HTML to output.
2863
     */
2864
    public function box_start($classes = 'generalbox', $id = null, $attributes = []) {
2865
        $this->opencontainers->push('box', html_writer::end_tag('div'));
2866
        $attributes['id'] = $id;
2867
        $attributes['class'] = 'box py-3 ' . renderer_base::prepare_classes($classes);
2868
        return html_writer::start_tag('div', $attributes);
2869
    }
2870
 
2871
    /**
2872
     * Outputs the closing section of a box.
2873
     *
2874
     * @return string the HTML to output.
2875
     */
2876
    public function box_end() {
2877
        return $this->opencontainers->pop('box');
2878
    }
2879
 
2880
    /**
2881
     * Outputs a paragraph.
2882
     *
2883
     * @param string $contents The contents of the paragraph
2884
     * @param string|null $classes A space-separated list of CSS classes
2885
     * @param string|null $id An optional ID
2886
     * @return string the HTML to output.
2887
     */
2888
    public function paragraph(string $contents, ?string $classes = null, ?string $id = null): string {
2889
        return html_writer::tag(
2890
            'p',
2891
            $contents,
2892
            ['id' => $id, 'class' => renderer_base::prepare_classes($classes)]
2893
        );
2894
    }
2895
 
2896
    /**
2897
     * Outputs a screen reader only inline text.
2898
     *
2899
     * @param string $contents The contents of the paragraph
2900
     * @return string the HTML to output.
2901
     * @deprecated since 5.0. Use visually_hidden_text() instead.
2902
     * @todo Final deprecation in Moodle 6.0. See MDL-83671.
2903
     */
2904
    #[\core\attribute\deprecated('core_renderer::visually_hidden_text()', since: '5.0', mdl: 'MDL-81825')]
2905
    public function sr_text(string $contents): string {
2906
        \core\deprecation::emit_deprecation([$this, __FUNCTION__]);
2907
        return $this->visually_hidden_text($contents);
2908
    }
2909
 
2910
    /**
2911
     * Outputs a visually hidden inline text (but accessible to assistive technologies).
2912
     *
2913
     * @param string $contents The contents of the paragraph
2914
     * @return string the HTML to output.
2915
     */
2916
    public function visually_hidden_text(string $contents): string {
2917
        return html_writer::tag(
2918
            'span',
2919
            $contents,
2920
            ['class' => 'visually-hidden']
2921
        ) . ' ';
2922
    }
2923
 
2924
    /**
2925
     * Outputs a screen reader only inline text.
2926
     *
2927
     * @param string $contents The content of the badge
2928
     * @param badge $badgestyle The style of the badge (default is PRIMARY)
2929
     * @param string $title An optional title of the badge
2930
     * @return string the HTML to output.
2931
     */
2932
    public function notice_badge(
2933
        string $contents,
2934
        badge $badgestyle = badge::PRIMARY,
2935
        string $title = '',
2936
    ): string {
2937
        if ($contents === '') {
2938
            return '';
2939
        }
2940
        // We want the badges to be read as content in parentesis.
2941
        $contents = trim($this->visually_hidden_text(' ('))
2942
            . $contents
2943
            . trim($this->visually_hidden_text(')'));
2944
 
2945
        $attributes = ['class' => 'ms-1 ' . $badgestyle->classes()];
2946
        if ($title !== '') {
2947
            $attributes['title'] = $title;
2948
        }
2949
 
2950
        return html_writer::tag('span', $contents, $attributes);
2951
    }
2952
 
2953
    /**
2954
     * Outputs a container.
2955
     *
2956
     * @param string $contents The contents of the box
2957
     * @param string $classes A space-separated list of CSS classes
2958
     * @param string $id An optional ID
2959
     * @param array $attributes Optional other attributes as array
2960
     * @return string the HTML to output.
2961
     */
2962
    public function container($contents, $classes = null, $id = null, $attributes = []) {
2963
        return $this->container_start($classes, $id, $attributes) . $contents . $this->container_end();
2964
    }
2965
 
2966
    /**
2967
     * Outputs the opening section of a container.
2968
     *
2969
     * @param string $classes A space-separated list of CSS classes
2970
     * @param string $id An optional ID
2971
     * @param array $attributes Optional other attributes as array
2972
     * @return string the HTML to output.
2973
     */
2974
    public function container_start($classes = null, $id = null, $attributes = []) {
2975
        $this->opencontainers->push('container', html_writer::end_tag('div'));
2976
        $attributes = array_merge(['id' => $id, 'class' => renderer_base::prepare_classes($classes)], $attributes);
2977
        return html_writer::start_tag('div', $attributes);
2978
    }
2979
 
2980
    /**
2981
     * Outputs the closing section of a container.
2982
     *
2983
     * @return string the HTML to output.
2984
     */
2985
    public function container_end() {
2986
        return $this->opencontainers->pop('container');
2987
    }
2988
 
2989
    /**
2990
     * Make nested HTML lists out of the items
2991
     *
2992
     * The resulting list will look something like this:
2993
     *
2994
     * <pre>
2995
     * <<ul>>
2996
     * <<li>><div class='tree_item parent'>(item contents)</div>
2997
     *      <<ul>
2998
     *      <<li>><div class='tree_item'>(item contents)</div><</li>>
2999
     *      <</ul>>
3000
     * <</li>>
3001
     * <</ul>>
3002
     * </pre>
3003
     *
3004
     * @param array $items
3005
     * @param array $attrs html attributes passed to the top ofs the list
3006
     * @return string HTML
3007
     */
3008
    public function tree_block_contents($items, $attrs = []) {
3009
        // exit if empty, we don't want an empty ul element
3010
        if (empty($items)) {
3011
            return '';
3012
        }
3013
        // array of nested li elements
3014
        $lis = [];
3015
        foreach ($items as $item) {
3016
            // this applies to the li item which contains all child lists too
3017
            $content = $item->content($this);
3018
            $liclasses = [$item->get_css_type()];
3019
            if (!$item->forceopen || (!$item->forceopen && $item->collapse) || ($item->children->count() == 0  && $item->nodetype == navigation_node::NODETYPE_BRANCH)) {
3020
                $liclasses[] = 'collapsed';
3021
            }
3022
            if ($item->isactive === true) {
3023
                $liclasses[] = 'current_branch';
3024
            }
3025
            $liattr = ['class' => join(' ', $liclasses)];
3026
            // class attribute on the div item which only contains the item content
3027
            $divclasses = ['tree_item'];
3028
            if ($item->children->count() > 0  || $item->nodetype == navigation_node::NODETYPE_BRANCH) {
3029
                $divclasses[] = 'branch';
3030
            } else {
3031
                $divclasses[] = 'leaf';
3032
            }
3033
            if (!empty($item->classes) && count($item->classes) > 0) {
3034
                $divclasses[] = join(' ', $item->classes);
3035
            }
3036
            $divattr = ['class' => join(' ', $divclasses)];
3037
            if (!empty($item->id)) {
3038
                $divattr['id'] = $item->id;
3039
            }
3040
            $content = html_writer::tag('p', $content, $divattr) . $this->tree_block_contents($item->children);
3041
            if (!empty($item->preceedwithhr) && $item->preceedwithhr === true) {
3042
                $content = html_writer::empty_tag('hr') . $content;
3043
            }
3044
            $content = html_writer::tag('li', $content, $liattr);
3045
            $lis[] = $content;
3046
        }
3047
        return html_writer::tag('ul', implode("\n", $lis), $attrs);
3048
    }
3049
 
3050
    /**
3051
     * Returns a search box.
3052
     *
3053
     * @param  string $id     The search box wrapper div id, defaults to an autogenerated one.
3054
     * @return string         HTML with the search form hidden by default.
3055
     */
3056
    public function search_box($id = false) {
3057
        global $CFG;
3058
 
3059
        // Accessing $CFG directly as using \core_search::is_global_search_enabled would
3060
        // result in an extra included file for each site, even the ones where global search
3061
        // is disabled.
3062
        if (empty($CFG->enableglobalsearch) || !has_capability('moodle/search:query', context_system::instance())) {
3063
            return '';
3064
        }
3065
 
3066
        $data = [
3067
            'action' => new moodle_url('/search/index.php'),
3068
            'hiddenfields' => (object) ['name' => 'context', 'value' => $this->page->context->id],
3069
            'inputname' => 'q',
3070
            'searchstring' => get_string('search'),
3071
            'grouplabel' => get_string('sitewidesearch', 'search'),
3072
        ];
3073
        return $this->render_from_template('core/search_input_navbar', $data);
3074
    }
3075
 
3076
    /**
3077
     * Allow plugins to provide some content to be rendered in the navbar.
3078
     * The plugin must define a PLUGIN_render_navbar_output function that returns
3079
     * the HTML they wish to add to the navbar.
3080
     *
3081
     * @return string HTML for the navbar
3082
     */
3083
    public function navbar_plugin_output() {
3084
        $output = '';
3085
 
3086
        // Give subsystems an opportunity to inject extra html content. The callback
3087
        // must always return a string containing valid html.
3088
        foreach (\core_component::get_core_subsystems() as $name => $path) {
3089
            if ($path) {
3090
                $output .= component_callback($name, 'render_navbar_output', [$this], '');
3091
            }
3092
        }
3093
 
3094
        if ($pluginsfunction = get_plugins_with_function('render_navbar_output')) {
3095
            foreach ($pluginsfunction as $plugintype => $plugins) {
3096
                foreach ($plugins as $pluginfunction) {
3097
                    $output .= $pluginfunction($this);
3098
                }
3099
            }
3100
        }
3101
 
3102
        return $output;
3103
    }
3104
 
3105
    /**
3106
     * Construct a user menu, returning HTML that can be echoed out by a
3107
     * layout file.
3108
     *
3109
     * @param stdClass $user A user object, usually $USER.
3110
     * @param bool $withlinks true if a dropdown should be built.
3111
     * @return string HTML fragment.
3112
     */
3113
    public function user_menu($user = null, $withlinks = null) {
3114
        global $USER, $CFG;
3115
        require_once($CFG->dirroot . '/user/lib.php');
3116
 
3117
        if (is_null($user)) {
3118
            $user = $USER;
3119
        }
3120
 
3121
        // Note: this behaviour is intended to match that of core_renderer::login_info,
3122
        // but should not be considered to be good practice; layout options are
3123
        // intended to be theme-specific. Please don't copy this snippet anywhere else.
3124
        if (is_null($withlinks)) {
3125
            $withlinks = empty($this->page->layout_options['nologinlinks']);
3126
        }
3127
 
3128
        // Add a class for when $withlinks is false.
3129
        $usermenuclasses = 'usermenu';
3130
        if (!$withlinks) {
3131
            $usermenuclasses .= ' withoutlinks';
3132
        }
3133
 
3134
        $returnstr = "";
3135
 
3136
        // If during initial install, return the empty return string.
3137
        if (during_initial_install()) {
3138
            return $returnstr;
3139
        }
3140
 
3141
        $loginpage = $this->is_login_page();
3142
        $loginurl = get_login_url();
3143
 
3144
        // Get some navigation opts.
3145
        $opts = user_get_user_navigation_info($user, $this->page);
3146
 
3147
        if (!empty($opts->unauthenticateduser)) {
3148
            $returnstr = get_string($opts->unauthenticateduser['content'], 'moodle');
3149
            // If not logged in, show the typical not-logged-in string.
3150
            if (!$loginpage && (!$opts->unauthenticateduser['guest'] || $withlinks)) {
3151
                $returnstr .= " (<a href=\"$loginurl\">" . get_string('login') . '</a>)';
3152
            }
3153
 
3154
            return html_writer::div(
3155
                html_writer::span(
3156
                    $returnstr,
3157
                    'login nav-link'
3158
                ),
3159
                $usermenuclasses
3160
            );
3161
        }
3162
 
3163
        $avatarclasses = "avatars";
3164
        $avatarcontents = html_writer::span($opts->metadata['useravatar'], 'avatar current');
3165
        $usertextcontents = $opts->metadata['userfullname'];
3166
 
3167
        // Other user.
3168
        if (!empty($opts->metadata['asotheruser'])) {
3169
            $avatarcontents .= html_writer::span(
3170
                $opts->metadata['realuseravatar'],
3171
                'avatar realuser'
3172
            );
3173
            $usertextcontents = $opts->metadata['realuserfullname'];
3174
            $usertextcontents .= html_writer::tag(
3175
                'span',
3176
                get_string(
3177
                    'loggedinas',
3178
                    'moodle',
3179
                    html_writer::span(
3180
                        $opts->metadata['userfullname'],
3181
                        'value'
3182
                    )
3183
                ),
3184
                ['class' => 'meta viewingas']
3185
            );
3186
        }
3187
 
3188
        // Role.
3189
        if (!empty($opts->metadata['asotherrole'])) {
3190
            $role = core_text::strtolower(preg_replace('#[ ]+#', '-', trim($opts->metadata['rolename'])));
3191
            $usertextcontents .= html_writer::span(
3192
                $opts->metadata['rolename'],
3193
                'meta role role-' . $role
3194
            );
3195
        }
3196
 
3197
        // User login failures.
3198
        if (!empty($opts->metadata['userloginfail'])) {
3199
            $usertextcontents .= html_writer::span(
3200
                $opts->metadata['userloginfail'],
3201
                'meta loginfailures'
3202
            );
3203
        }
3204
 
3205
        // MNet.
3206
        if (!empty($opts->metadata['asmnetuser'])) {
3207
            $mnet = strtolower(preg_replace('#[ ]+#', '-', trim($opts->metadata['mnetidprovidername'])));
3208
            $usertextcontents .= html_writer::span(
3209
                $opts->metadata['mnetidprovidername'],
3210
                'meta mnet mnet-' . $mnet
3211
            );
3212
        }
3213
 
3214
        $returnstr .= html_writer::span(
3215
            html_writer::span($usertextcontents, 'usertext me-1') .
3216
            html_writer::span($avatarcontents, $avatarclasses),
3217
            'userbutton'
3218
        );
3219
 
3220
        // Create a divider (well, a filler).
3221
        $divider = new action_menu\filler();
3222
        $divider->primary = false;
3223
 
3224
        $am = new action_menu();
3225
        $am->set_menu_trigger(
3226
            $returnstr,
3227
            'nav-link'
3228
        );
3229
        $am->set_action_label(get_string('usermenu'));
3230
        $am->set_nowrap_on_items();
3231
        if ($withlinks) {
3232
            $navitemcount = count($opts->navitems);
3233
            $idx = 0;
3234
            foreach ($opts->navitems as $key => $value) {
3235
                switch ($value->itemtype) {
3236
                    case 'divider':
3237
                        // If the nav item is a divider, add one and skip link processing.
3238
                        $am->add($divider);
3239
                        break;
3240
 
3241
                    case 'invalid':
3242
                        // Silently skip invalid entries (should we post a notification?).
3243
                        break;
3244
 
3245
                    case 'link':
3246
                        // Process this as a link item.
3247
                        $pix = null;
3248
                        if (isset($value->pix) && !empty($value->pix)) {
3249
                            $pix = new pix_icon($value->pix, '', null, ['class' => 'iconsmall']);
3250
                        } else if (isset($value->imgsrc) && !empty($value->imgsrc)) {
3251
                            $value->title = html_writer::img(
3252
                                $value->imgsrc,
3253
                                $value->title,
3254
                                ['class' => 'iconsmall']
3255
                            ) . $value->title;
3256
                        }
3257
 
3258
                        $al = new action_menu\link_secondary(
3259
                            $value->url,
3260
                            $pix,
3261
                            $value->title,
3262
                            ['class' => 'icon']
3263
                        );
3264
                        if (!empty($value->titleidentifier)) {
3265
                            $al->attributes['data-title'] = $value->titleidentifier;
3266
                        }
3267
                        $am->add($al);
3268
                        break;
3269
                }
3270
 
3271
                $idx++;
3272
 
3273
                // Add dividers after the first item and before the last item.
3274
                if ($idx == 1 || $idx == $navitemcount - 1) {
3275
                    $am->add($divider);
3276
                }
3277
            }
3278
        }
3279
 
3280
        return html_writer::div(
3281
            $this->render($am),
3282
            $usermenuclasses
3283
        );
3284
    }
3285
 
3286
    /**
3287
     * Secure layout login info.
3288
     *
3289
     * @return string
3290
     */
3291
    public function secure_layout_login_info() {
3292
        if (get_config('core', 'logininfoinsecurelayout')) {
3293
            return $this->login_info(false);
3294
        } else {
3295
            return '';
3296
        }
3297
    }
3298
 
3299
    /**
3300
     * Returns the language menu in the secure layout.
3301
     *
3302
     * No custom menu items are passed though, such that it will render only the language selection.
3303
     *
3304
     * @return string
3305
     */
3306
    public function secure_layout_language_menu() {
3307
        if (get_config('core', 'langmenuinsecurelayout')) {
3308
            $custommenu = new custom_menu('', current_language());
3309
            return $this->render_custom_menu($custommenu);
3310
        } else {
3311
            return '';
3312
        }
3313
    }
3314
 
3315
    /**
3316
     * This renders the navbar.
3317
     * Uses bootstrap compatible html.
3318
     */
3319
    public function navbar() {
3320
        return $this->render_from_template('core/navbar', $this->page->navbar);
3321
    }
3322
 
3323
    /**
3324
     * Renders a breadcrumb navigation node object.
3325
     *
3326
     * @param breadcrumb_navigation_node $item The navigation node to render.
3327
     * @return string HTML fragment
3328
     */
3329
    protected function render_breadcrumb_navigation_node(breadcrumb_navigation_node $item) {
3330
 
3331
        if ($item->action instanceof moodle_url) {
3332
            $content = $item->get_content();
3333
            $title = $item->get_title();
3334
            $attributes = [];
3335
            $attributes['itemprop'] = 'url';
3336
            if ($title !== '') {
3337
                $attributes['title'] = $title;
3338
            }
3339
            if ($item->hidden) {
3340
                $attributes['class'] = 'dimmed_text';
3341
            }
3342
            if ($item->is_last()) {
3343
                $attributes['aria-current'] = 'page';
3344
            }
3345
            $content = html_writer::tag('span', $content, ['itemprop' => 'title']);
3346
            $content = html_writer::link($item->action, $content, $attributes);
3347
 
3348
            $attributes = [];
3349
            $attributes['itemscope'] = '';
3350
            $attributes['itemtype'] = 'http://data-vocabulary.org/Breadcrumb';
3351
            $content = html_writer::tag('span', $content, $attributes);
3352
        } else {
3353
            $content = $this->render_navigation_node($item);
3354
        }
3355
        return $content;
3356
    }
3357
 
3358
    /**
3359
     * Renders a navigation node object.
3360
     *
3361
     * @param navigation_node $item The navigation node to render.
3362
     * @return string HTML fragment
3363
     */
3364
    protected function render_navigation_node(navigation_node $item) {
3365
        $content = $item->get_content();
3366
        $title = $item->get_title();
3367
        if ($item->icon instanceof renderable && !$item->hideicon) {
3368
            $icon = $this->render($item->icon);
3369
            $content = $icon . $content; // use CSS for spacing of icons
3370
        }
3371
        if ($item->helpbutton !== null) {
3372
            $content = trim($item->helpbutton) . html_writer::tag('span', $content, ['class' => 'clearhelpbutton', 'tabindex' => '0']);
3373
        }
3374
        if ($content === '') {
3375
            return '';
3376
        }
3377
        if ($item->action instanceof action_link) {
3378
            $link = $item->action;
3379
            if ($item->hidden) {
3380
                $link->add_class('dimmed');
3381
            }
3382
            if (!empty($content)) {
3383
                // Providing there is content we will use that for the link content.
3384
                $link->text = $content;
3385
            }
3386
            $content = $this->render($link);
3387
        } else if ($item->action instanceof moodle_url) {
3388
            $attributes = [];
3389
            if ($title !== '') {
3390
                $attributes['title'] = $title;
3391
            }
3392
            if ($item->hidden) {
3393
                $attributes['class'] = 'dimmed_text';
3394
            }
3395
            $content = html_writer::link($item->action, $content, $attributes);
3396
        } else if (is_string($item->action) || empty($item->action)) {
3397
            $attributes = ['tabindex' => '0']; // add tab support to span but still maintain character stream sequence.
3398
            if ($title !== '') {
3399
                $attributes['title'] = $title;
3400
            }
3401
            if ($item->hidden) {
3402
                $attributes['class'] = 'dimmed_text';
3403
            }
3404
            $content = html_writer::tag('span', $content, $attributes);
3405
        }
3406
        return $content;
3407
    }
3408
 
3409
    /**
3410
     * Accessibility: Right arrow-like character is
3411
     * used in the breadcrumb trail, course navigation menu
3412
     * (previous/next activity), calendar, and search forum block.
3413
     * If the theme does not set characters, appropriate defaults
3414
     * are set automatically. Please DO NOT
3415
     * use &lt; &gt; &raquo; - these are confusing for blind users.
3416
     *
3417
     * @return string
3418
     */
3419
    public function rarrow() {
3420
        return $this->page->theme->rarrow;
3421
    }
3422
 
3423
    /**
3424
     * Accessibility: Left arrow-like character is
3425
     * used in the breadcrumb trail, course navigation menu
3426
     * (previous/next activity), calendar, and search forum block.
3427
     * If the theme does not set characters, appropriate defaults
3428
     * are set automatically. Please DO NOT
3429
     * use &lt; &gt; &raquo; - these are confusing for blind users.
3430
     *
3431
     * @return string
3432
     */
3433
    public function larrow() {
3434
        return $this->page->theme->larrow;
3435
    }
3436
 
3437
    /**
3438
     * Accessibility: Up arrow-like character is used in
3439
     * the book heirarchical navigation.
3440
     * If the theme does not set characters, appropriate defaults
3441
     * are set automatically. Please DO NOT
3442
     * use ^ - this is confusing for blind users.
3443
     *
3444
     * @return string
3445
     */
3446
    public function uarrow() {
3447
        return $this->page->theme->uarrow;
3448
    }
3449
 
3450
    /**
3451
     * Accessibility: Down arrow-like character.
3452
     * If the theme does not set characters, appropriate defaults
3453
     * are set automatically.
3454
     *
3455
     * @return string
3456
     */
3457
    public function darrow() {
3458
        return $this->page->theme->darrow;
3459
    }
3460
 
3461
    /**
3462
     * Returns the custom menu if one has been set
3463
     *
3464
     * A custom menu can be configured by browsing to a theme's settings page
3465
     * and then configuring the custommenu config setting as described.
3466
     *
3467
     * Theme developers: DO NOT OVERRIDE! Please override function
3468
     * {@see core_renderer::render_custom_menu()} instead.
3469
     *
3470
     * @param string $custommenuitems - custom menuitems set by theme instead of global theme settings
3471
     * @return string
3472
     */
3473
    public function custom_menu($custommenuitems = '') {
3474
        global $CFG;
3475
 
3476
        if (empty($custommenuitems) && !empty($CFG->custommenuitems)) {
3477
            $custommenuitems = $CFG->custommenuitems;
3478
        }
3479
 
3480
        // If filtering of the primary custom menu is enabled, apply only the string filters.
3481
        if (!empty($CFG->navfilter) && !empty($CFG->stringfilters)) {
3482
            // Apply filters that are enabled for Content and Headings.
3483
            $filtermanager = \filter_manager::instance();
3484
            $custommenuitems = $filtermanager->filter_string($custommenuitems, \context_system::instance());
3485
        }
3486
 
3487
        $custommenu = new custom_menu($custommenuitems, current_language());
3488
        return $this->render_custom_menu($custommenu);
3489
    }
3490
 
3491
    /**
3492
     * We want to show the custom menus as a list of links in the footer on small screens.
3493
     * Just return the menu object exported so we can render it differently.
3494
     */
3495
    public function custom_menu_flat() {
3496
        global $CFG;
3497
        $custommenuitems = '';
3498
 
3499
        if (empty($custommenuitems) && !empty($CFG->custommenuitems)) {
3500
            $custommenuitems = $CFG->custommenuitems;
3501
        }
3502
 
3503
        // If filtering of the primary custom menu is enabled, apply only the string filters.
3504
        if (!empty($CFG->navfilter) && !empty($CFG->stringfilters)) {
3505
            // Apply filters that are enabled for Content and Headings.
3506
            $filtermanager = \filter_manager::instance();
3507
            $custommenuitems = $filtermanager->filter_string($custommenuitems, \context_system::instance());
3508
        }
3509
 
3510
        $custommenu = new custom_menu($custommenuitems, current_language());
3511
        $langs = get_string_manager()->get_list_of_translations();
3512
        $haslangmenu = $this->lang_menu() != '';
3513
 
3514
        if ($haslangmenu) {
3515
            $strlang = get_string('language');
3516
            $currentlang = current_language();
3517
            if (isset($langs[$currentlang])) {
3518
                $currentlang = $langs[$currentlang];
3519
            } else {
3520
                $currentlang = $strlang;
3521
            }
3522
            $this->language = $custommenu->add($currentlang, new moodle_url('#'), $strlang, 10000);
3523
            foreach ($langs as $langtype => $langname) {
3524
                $this->language->add($langname, new moodle_url($this->page->url, ['lang' => $langtype]), $langname);
3525
            }
3526
        }
3527
 
3528
        return $custommenu->export_for_template($this);
3529
    }
3530
 
3531
    /**
3532
     * Renders a custom menu object (located in outputcomponents.php)
3533
     *
3534
     * The custom menu this method produces makes use of the YUI3 menunav widget
3535
     * and requires very specific html elements and classes.
3536
     *
3537
     * @staticvar int $menucount
3538
     * @param custom_menu $menu
3539
     * @return string
3540
     */
3541
    protected function render_custom_menu(custom_menu $menu) {
3542
        global $CFG;
3543
 
3544
        $langs = get_string_manager()->get_list_of_translations();
3545
        $haslangmenu = $this->lang_menu() != '';
3546
 
3547
        if (!$menu->has_children() && !$haslangmenu) {
3548
            return '';
3549
        }
3550
 
3551
        if ($haslangmenu) {
3552
            $strlang = get_string('language');
3553
            $currentlang = current_language();
3554
            if (isset($langs[$currentlang])) {
3555
                $currentlangstr = $langs[$currentlang];
3556
            } else {
3557
                $currentlangstr = $strlang;
3558
            }
3559
            $this->language = $menu->add($currentlangstr, new moodle_url('#'), $strlang, 10000);
3560
            foreach ($langs as $langtype => $langname) {
3561
                $attributes = [];
3562
                // Set the lang attribute for languages different from the page's current language.
3563
                if ($langtype !== $currentlang) {
3564
                    $attributes[] = [
3565
                        'key' => 'lang',
3566
                        'value' => get_html_lang_attribute_value($langtype),
3567
                    ];
3568
                }
3569
                $this->language->add($langname, new moodle_url($this->page->url, ['lang' => $langtype]), null, null, $attributes);
3570
            }
3571
        }
3572
 
3573
        $content = '';
3574
        foreach ($menu->get_children() as $item) {
3575
            $context = $item->export_for_template($this);
3576
            $content .= $this->render_from_template('core/custom_menu_item', $context);
3577
        }
3578
 
3579
        return $content;
3580
    }
3581
 
3582
    /**
3583
     * Renders a custom menu node as part of a submenu
3584
     *
3585
     * The custom menu this method produces makes use of the YUI3 menunav widget
3586
     * and requires very specific html elements and classes.
3587
     *
3588
     * @see core:renderer::render_custom_menu()
3589
     *
3590
     * @staticvar int $submenucount
3591
     * @param custom_menu_item $menunode
3592
     * @return string
3593
     */
3594
    protected function render_custom_menu_item(custom_menu_item $menunode) {
3595
        // Required to ensure we get unique trackable id's
3596
        static $submenucount = 0;
3597
        if ($menunode->has_children()) {
3598
            // If the child has menus render it as a sub menu
3599
            $submenucount++;
3600
            $content = html_writer::start_tag('li');
3601
            if ($menunode->get_url() !== null) {
3602
                $url = $menunode->get_url();
3603
            } else {
3604
                $url = '#cm_submenu_' . $submenucount;
3605
            }
3606
            $content .= html_writer::link($url, $menunode->get_text(), ['class' => 'yui3-menu-label', 'title' => $menunode->get_title()]);
3607
            $content .= html_writer::start_tag('div', ['id' => 'cm_submenu_' . $submenucount, 'class' => 'yui3-menu custom_menu_submenu']);
3608
            $content .= html_writer::start_tag('div', ['class' => 'yui3-menu-content']);
3609
            $content .= html_writer::start_tag('ul');
3610
            foreach ($menunode->get_children() as $menunode) {
3611
                $content .= $this->render_custom_menu_item($menunode);
3612
            }
3613
            $content .= html_writer::end_tag('ul');
3614
            $content .= html_writer::end_tag('div');
3615
            $content .= html_writer::end_tag('div');
3616
            $content .= html_writer::end_tag('li');
3617
        } else {
3618
            // The node doesn't have children so produce a final menuitem.
3619
            // Also, if the node's text matches '####', add a class so we can treat it as a divider.
3620
            $content = '';
3621
            if (preg_match("/^#+$/", $menunode->get_text())) {
3622
                // This is a divider.
3623
                $content = html_writer::start_tag('li', ['class' => 'yui3-menuitem divider']);
3624
            } else {
3625
                $content = html_writer::start_tag(
3626
                    'li',
3627
                    [
3628
                        'class' => 'yui3-menuitem',
3629
                    ]
3630
                );
3631
                if ($menunode->get_url() !== null) {
3632
                    $url = $menunode->get_url();
3633
                } else {
3634
                    $url = '#';
3635
                }
3636
                $content .= html_writer::link(
3637
                    $url,
3638
                    $menunode->get_text(),
3639
                    ['class' => 'yui3-menuitem-content', 'title' => $menunode->get_title()]
3640
                );
3641
            }
3642
            $content .= html_writer::end_tag('li');
3643
        }
3644
        // Return the sub menu
3645
        return $content;
3646
    }
3647
 
3648
    /**
3649
     * Renders theme links for switching between default and other themes.
3650
     *
3651
     * @return string
3652
     */
3653
    protected function theme_switch_links() {
3654
 
3655
        $actualdevice = core_useragent::get_device_type();
3656
        $currentdevice = $this->page->devicetypeinuse;
3657
        $switched = ($actualdevice != $currentdevice);
3658
 
3659
        if (!$switched && $currentdevice == 'default' && $actualdevice == 'default') {
3660
            // The user is using the a default device and hasn't switched so don't shown the switch
3661
            // device links.
3662
            return '';
3663
        }
3664
 
3665
        if ($switched) {
3666
            $linktext = get_string('switchdevicerecommended');
3667
            $devicetype = $actualdevice;
3668
        } else {
3669
            $linktext = get_string('switchdevicedefault');
3670
            $devicetype = 'default';
3671
        }
3672
        $linkurl = new moodle_url('/theme/switchdevice.php', ['url' => $this->page->url, 'device' => $devicetype, 'sesskey' => sesskey()]);
3673
 
3674
        $content  = html_writer::start_tag('div', ['id' => 'theme_switch_link']);
3675
        $content .= html_writer::link($linkurl, $linktext, ['rel' => 'nofollow']);
3676
        $content .= html_writer::end_tag('div');
3677
 
3678
        return $content;
3679
    }
3680
 
3681
    /**
3682
     * Renders tabs
3683
     *
3684
     * This function replaces print_tabs() used before Moodle 2.5 but with slightly different arguments
3685
     *
3686
     * Theme developers: In order to change how tabs are displayed please override functions
3687
     * {@see core_renderer::render_tabtree()} and/or {@see core_renderer::render_tabobject()}
3688
     *
3689
     * @param array $tabs array of tabs, each of them may have it's own ->subtree
3690
     * @param string|null $selected which tab to mark as selected, all parent tabs will
3691
     *     automatically be marked as activated
3692
     * @param array|string|null $inactive list of ids of inactive tabs, regardless of
3693
     *     their level. Note that you can as weel specify tabobject::$inactive for separate instances
3694
     * @return string
3695
     */
3696
    final public function tabtree($tabs, $selected = null, $inactive = null) {
3697
        return $this->render(new tabtree($tabs, $selected, $inactive));
3698
    }
3699
 
3700
    /**
3701
     * Renders tabtree
3702
     *
3703
     * @param tabtree $tabtree
3704
     * @return string
3705
     */
3706
    protected function render_tabtree(tabtree $tabtree) {
3707
        if (empty($tabtree->subtree)) {
3708
            return '';
3709
        }
3710
        $data = $tabtree->export_for_template($this);
3711
        return $this->render_from_template('core/tabtree', $data);
3712
    }
3713
 
3714
    /**
3715
     * Renders tabobject (part of tabtree)
3716
     *
3717
     * This function is called from {@see core_renderer::render_tabtree()}
3718
     * and also it calls itself when printing the $tabobject subtree recursively.
3719
     *
3720
     * Property $tabobject->level indicates the number of row of tabs.
3721
     *
3722
     * @param tabobject $tabobject
3723
     * @return string HTML fragment
3724
     */
3725
    protected function render_tabobject(tabobject $tabobject) {
3726
        $str = '';
3727
 
3728
        // Print name of the current tab.
3729
        if ($tabobject instanceof tabtree) {
3730
            // No name for tabtree root.
3731
        } else if ($tabobject->inactive || $tabobject->activated || ($tabobject->selected && !$tabobject->linkedwhenselected)) {
3732
            // Tab name without a link. The <a> tag is used for styling.
3733
            $str .= html_writer::tag('a', html_writer::span($tabobject->text), ['class' => 'nolink moodle-has-zindex']);
3734
        } else {
3735
            // Tab name with a link.
3736
            if (!($tabobject->link instanceof moodle_url)) {
3737
                // backward compartibility when link was passed as quoted string
3738
                $str .= "<a href=\"$tabobject->link\" title=\"$tabobject->title\"><span>$tabobject->text</span></a>";
3739
            } else {
3740
                $str .= html_writer::link($tabobject->link, html_writer::span($tabobject->text), ['title' => $tabobject->title]);
3741
            }
3742
        }
3743
 
3744
        if (empty($tabobject->subtree)) {
3745
            if ($tabobject->selected) {
3746
                $str .= html_writer::tag('div', '&nbsp;', ['class' => 'tabrow' . ($tabobject->level + 1) . ' empty']);
3747
            }
3748
            return $str;
3749
        }
3750
 
3751
        // Print subtree.
3752
        if ($tabobject->level == 0 || $tabobject->selected || $tabobject->activated) {
3753
            $str .= html_writer::start_tag('ul', ['class' => 'tabrow' . $tabobject->level]);
3754
            $cnt = 0;
3755
            foreach ($tabobject->subtree as $tab) {
3756
                $liclass = '';
3757
                if (!$cnt) {
3758
                    $liclass .= ' first';
3759
                }
3760
                if ($cnt == count($tabobject->subtree) - 1) {
3761
                    $liclass .= ' last';
3762
                }
3763
                if ((empty($tab->subtree)) && (!empty($tab->selected))) {
3764
                    $liclass .= ' onerow';
3765
                }
3766
 
3767
                if ($tab->selected) {
3768
                    $liclass .= ' here selected';
3769
                } else if ($tab->activated) {
3770
                    $liclass .= ' here active';
3771
                }
3772
 
3773
                // This will recursively call function render_tabobject() for each item in subtree.
3774
                $str .= html_writer::tag('li', $this->render($tab), ['class' => trim($liclass)]);
3775
                $cnt++;
3776
            }
3777
            $str .= html_writer::end_tag('ul');
3778
        }
3779
 
3780
        return $str;
3781
    }
3782
 
3783
    /**
3784
     * Get the HTML for blocks in the given region.
3785
     *
3786
     * @since Moodle 2.5.1 2.6
3787
     * @param string $region The region to get HTML for.
3788
     * @param array $classes Wrapping tag classes.
3789
     * @param string $tag Wrapping tag.
3790
     * @param boolean $fakeblocksonly Include fake blocks only.
3791
     * @return string HTML.
3792
     */
3793
    public function blocks($region, $classes = [], $tag = 'aside', $fakeblocksonly = false) {
3794
        $displayregion = $this->page->apply_theme_region_manipulations($region);
3795
        $headingid = $displayregion . '-block-region-heading';
3796
        $classes = (array)$classes;
3797
        $classes[] = 'block-region';
3798
        $attributes = [
3799
            'id' => 'block-region-' . preg_replace('#[^a-zA-Z0-9_\-]+#', '-', $displayregion),
3800
            'class' => join(' ', $classes),
3801
            'data-blockregion' => $displayregion,
3802
            'data-droptarget' => '1',
3803
            'aria-labelledby' => $headingid,
3804
        ];
3805
        // Generate an appropriate heading to uniquely identify the block region.
3806
        $blocksheading = match ($displayregion) {
3807
            'side-post' => get_string('blocks_supplementary'),
3808
            'content' => get_string('blocks_main'),
3809
            default => get_string('blocks'),
3810
        };
3811
        $content = html_writer::tag('h2', $blocksheading, ['class' => 'visually-hidden', 'id' => $headingid]);
3812
        if ($this->page->blocks->region_has_content($displayregion, $this)) {
3813
            $content .= $this->blocks_for_region($displayregion, $fakeblocksonly);
3814
        }
3815
        // Given that <aside> has a default role of a complementary landmark and is supposed to be a top-level landmark,
3816
        // blocks rendered as part of the main content should not have a complementary role and should be rendered in a more generic
3817
        // container.
3818
        if ($displayregion === 'content' && $tag === 'aside') {
3819
            $tag = 'section';
3820
        }
3821
        return html_writer::tag($tag, $content, $attributes);
3822
    }
3823
 
3824
    /**
3825
     * Renders a custom block region.
3826
     *
3827
     * Use this method if you want to add an additional block region to the content of the page.
3828
     * Please note this should only be used in special situations.
3829
     * We want to leave the theme is control where ever possible!
3830
     *
3831
     * This method must use the same method that the theme uses within its layout file.
3832
     * As such it asks the theme what method it is using.
3833
     * It can be one of two values, blocks or blocks_for_region (deprecated).
3834
     *
3835
     * @param string $regionname The name of the custom region to add.
3836
     * @return string HTML for the block region.
3837
     */
3838
    public function custom_block_region($regionname) {
3839
        if ($this->page->theme->get_block_render_method() === 'blocks') {
3840
            return $this->blocks($regionname);
3841
        } else {
3842
            return $this->blocks_for_region($regionname);
3843
        }
3844
    }
3845
 
3846
    /**
3847
     * Returns the CSS classes to apply to the body tag.
3848
     *
3849
     * @since Moodle 2.5.1 2.6
3850
     * @param array $additionalclasses Any additional classes to apply.
3851
     * @return string
3852
     */
3853
    public function body_css_classes(array $additionalclasses = []) {
3854
        return $this->page->bodyclasses . ' ' . implode(' ', $additionalclasses);
3855
    }
3856
 
3857
    /**
3858
     * The ID attribute to apply to the body tag.
3859
     *
3860
     * @since Moodle 2.5.1 2.6
3861
     * @return string
3862
     */
3863
    public function body_id() {
3864
        return $this->page->bodyid;
3865
    }
3866
 
3867
    /**
3868
     * Returns HTML attributes to use within the body tag. This includes an ID and classes.
3869
     *
3870
     * @since Moodle 2.5.1 2.6
3871
     * @param string|array $additionalclasses Any additional classes to give the body tag,
3872
     * @return string
3873
     */
3874
    public function body_attributes($additionalclasses = []) {
3875
        if (!is_array($additionalclasses)) {
3876
            $additionalclasses = explode(' ', $additionalclasses);
3877
        }
3878
        return ' id="' . $this->body_id() . '" class="' . $this->body_css_classes($additionalclasses) . '"';
3879
    }
3880
 
3881
    /**
3882
     * Gets HTML for the page heading.
3883
     *
3884
     * @since Moodle 2.5.1 2.6
3885
     * @param string $tag The tag to encase the heading in. h1 by default.
3886
     * @return string HTML.
3887
     */
3888
    public function page_heading($tag = 'h1') {
3889
        return html_writer::tag($tag, $this->page->heading);
3890
    }
3891
 
3892
    /**
3893
     * Gets the HTML for the page heading button.
3894
     *
3895
     * @since Moodle 2.5.1 2.6
3896
     * @return string HTML.
3897
     */
3898
    public function page_heading_button() {
3899
        return $this->page->button;
3900
    }
3901
 
3902
    /**
3903
     * Returns the Moodle docs link to use for this page.
3904
     *
3905
     * @since Moodle 2.5.1 2.6
3906
     * @param string $text
3907
     * @return string
3908
     */
3909
    public function page_doc_link($text = null) {
3910
        if ($text === null) {
3911
            $text = get_string('moodledocslink');
3912
        }
3913
        $path = page_get_doc_link_path($this->page);
3914
        if (!$path) {
3915
            return '';
3916
        }
3917
        return $this->doc_link($path, $text);
3918
    }
3919
 
3920
    /**
3921
     * Returns the HTML for the site support email link
3922
     *
3923
     * @param array $customattribs Array of custom attributes for the support email anchor tag.
3924
     * @param bool $embed Set to true if you want to embed the link in other inline content.
3925
     * @return string The html code for the support email link.
3926
     */
3927
    public function supportemail(array $customattribs = [], bool $embed = false): string {
3928
        global $CFG;
3929
 
3930
        // Do not provide a link to contact site support if it is unavailable to this user. This would be where the site has
3931
        // disabled support, or limited it to authenticated users and the current user is a guest or not logged in.
3932
        if (
3933
            !isset($CFG->supportavailability) ||
3934
                $CFG->supportavailability == CONTACT_SUPPORT_DISABLED ||
3935
                ($CFG->supportavailability == CONTACT_SUPPORT_AUTHENTICATED && (!isloggedin() || isguestuser()))
3936
        ) {
3937
            return '';
3938
        }
3939
 
3940
        $label = get_string('contactsitesupport', 'admin');
3941
        $icon = $this->pix_icon('t/email', '');
3942
 
3943
        if (!$embed) {
3944
            $content = $icon . $label;
3945
        } else {
3946
            $content = $label;
3947
        }
3948
 
3949
        if (!empty($CFG->supportpage)) {
3950
            $attributes = ['href' => $CFG->supportpage, 'target' => 'blank'];
3951
            $content .= $this->pix_icon('i/externallink', '', 'moodle', ['class' => 'ms-1']);
3952
        } else {
3953
            $attributes = ['href' => $CFG->wwwroot . '/user/contactsitesupport.php'];
3954
        }
3955
 
3956
        $attributes += $customattribs;
3957
 
3958
        return html_writer::tag('a', $content, $attributes);
3959
    }
3960
 
3961
    /**
3962
     * Returns the services and support link for the help pop-up.
3963
     *
3964
     * @return string
3965
     */
3966
    public function services_support_link(): string {
3967
        global $CFG;
3968
 
3969
        if (
3970
            during_initial_install() ||
3971
            (isset($CFG->showservicesandsupportcontent) && $CFG->showservicesandsupportcontent == false) ||
3972
            !is_siteadmin()
3973
        ) {
3974
            return '';
3975
        }
3976
 
3977
        $liferingicon = $this->pix_icon('t/life-ring', '', 'moodle', ['class' => 'fa fa-life-ring']);
3978
        $newwindowicon = $this->pix_icon('i/externallink', get_string('opensinnewwindow'), 'moodle', ['class' => 'ms-1']);
3979
        $link = !empty($CFG->servicespage)
3980
            ? $CFG->servicespage
3981
            : 'https://moodle.com/help/?utm_source=CTA-banner&utm_medium=platform&utm_campaign=name~Moodle4+cat~lms+mp~no';
3982
        $content = $liferingicon . get_string('moodleservicesandsupport') . $newwindowicon;
3983
 
3984
        return html_writer::tag('a', $content, ['target' => '_blank', 'href' => $link]);
3985
    }
3986
 
3987
    /**
3988
     * Helper function to decide whether to show the help popover header or not.
3989
     *
3990
     * @return bool
3991
     */
3992
    public function has_popover_links(): bool {
3993
        return !empty($this->services_support_link()) || !empty($this->page_doc_link()) || !empty($this->supportemail());
3994
    }
3995
 
3996
    /**
3997
     * Helper function to decide whether to show the communication link or not.
3998
     *
3999
     * @return bool
4000
     */
4001
    public function has_communication_links(): bool {
4002
        if (during_initial_install() || !\core_communication\api::is_available()) {
4003
            return false;
4004
        }
4005
        return !empty($this->communication_link());
4006
    }
4007
 
4008
    /**
4009
     * Returns the communication link, complete with html.
4010
     *
4011
     * @return string
4012
     */
4013
    public function communication_link(): string {
4014
        $link = $this->communication_url() ?? '';
4015
        $commicon = $this->pix_icon('t/messages-o', '', 'moodle', ['class' => 'fa fa-comments']);
4016
        $newwindowicon = $this->pix_icon('i/externallink', get_string('opensinnewwindow'), 'moodle', ['class' => 'ms-1']);
4017
        $content = $commicon . get_string('communicationroomlink', 'course') . $newwindowicon;
4018
        $html = html_writer::tag('a', $content, ['target' => '_blank', 'href' => $link]);
4019
 
4020
        return !empty($link) ? $html : '';
4021
    }
4022
 
4023
    /**
4024
     * Returns the communication url for a given instance if it exists.
4025
     *
4026
     * @return string
4027
     */
4028
    public function communication_url(): string {
4029
        global $COURSE;
4030
        return \core_communication\helper::get_course_communication_url($COURSE);
4031
    }
4032
 
4033
    /**
4034
     * Returns the page heading menu.
4035
     *
4036
     * @since Moodle 2.5.1 2.6
4037
     * @return string HTML.
4038
     */
4039
    public function page_heading_menu() {
4040
        return $this->page->headingmenu;
4041
    }
4042
 
4043
    /**
4044
     * Returns the title to use on the page.
4045
     *
4046
     * @since Moodle 2.5.1 2.6
4047
     * @return string
4048
     */
4049
    public function page_title() {
4050
        return $this->page->title;
4051
    }
4052
 
4053
    /**
4054
     * Returns the moodle_url for the favicon.
4055
     *
4056
     * @since Moodle 2.5.1 2.6
4057
     * @return moodle_url The moodle_url for the favicon
4058
     */
4059
    public function favicon() {
4060
        $logo = null;
4061
        if (!during_initial_install()) {
4062
            $logo = get_config('core_admin', 'favicon');
4063
        }
4064
        if (empty($logo)) {
4065
            return $this->image_url('favicon', 'theme');
4066
        }
4067
 
4068
        // Use $CFG->themerev to prevent browser caching when the file changes.
4069
        return moodle_url::make_pluginfile_url(
4070
            context_system::instance()->id,
4071
            'core_admin',
4072
            'favicon',
4073
            '64x64/',
4074
            theme_get_revision(),
4075
            $logo
4076
        );
4077
    }
4078
 
4079
    /**
4080
     * Renders preferences groups.
4081
     *
4082
     * @param  preferences_groups $renderable The renderable
4083
     * @return string The output.
4084
     */
4085
    public function render_preferences_groups(preferences_groups $renderable) {
4086
        return $this->render_from_template('core/preferences_groups', $renderable);
4087
    }
4088
 
4089
    /**
4090
     * Renders preferences group.
4091
     *
4092
     * @param  preferences_group $renderable The renderable
4093
     * @return string The output.
4094
     */
4095
    public function render_preferences_group(preferences_group $renderable) {
4096
        $html = '';
4097
        $html .= html_writer::start_tag('div', ['class' => 'col-sm-4 preferences-group']);
4098
        $html .= $this->heading($renderable->title, 3);
4099
        $html .= html_writer::start_tag('ul');
4100
        foreach ($renderable->nodes as $node) {
4101
            if ($node->has_children()) {
4102
                debugging('Preferences nodes do not support children', DEBUG_DEVELOPER);
4103
            }
4104
            $html .= html_writer::tag('li', $this->render($node));
4105
        }
4106
        $html .= html_writer::end_tag('ul');
4107
        $html .= html_writer::end_tag('div');
4108
        return $html;
4109
    }
4110
 
4111
    public function context_header($headerinfo = null, $headinglevel = 1) {
4112
        global $DB, $USER, $CFG, $SITE;
4113
        require_once($CFG->dirroot . '/user/lib.php');
4114
        $context = $this->page->context;
4115
        $heading = null;
4116
        $imagedata = null;
4117
        $subheader = null;
4118
        $userbuttons = null;
4119
 
4120
        // Make sure to use the heading if it has been set.
4121
        if (isset($headerinfo['heading'])) {
4122
            $heading = $headerinfo['heading'];
4123
        } else {
4124
            $heading = $this->page->heading;
4125
        }
4126
 
4127
        // The user context currently has images and buttons. Other contexts may follow.
4128
        if ((isset($headerinfo['user']) || $context->contextlevel == CONTEXT_USER) && $this->page->pagetype !== 'my-index') {
4129
            if (isset($headerinfo['user'])) {
4130
                $user = $headerinfo['user'];
4131
            } else {
4132
                // Look up the user information if it is not supplied.
4133
                $user = $DB->get_record('user', ['id' => $context->instanceid]);
4134
            }
4135
 
4136
            // If the user context is set, then use that for capability checks.
4137
            if (isset($headerinfo['usercontext'])) {
4138
                $context = $headerinfo['usercontext'];
4139
            }
4140
 
4141
            // Only provide user information if the user is the current user, or a user which the current user can view.
4142
            // When checking user_can_view_profile(), either:
4143
            // If the page context is course, check the course context (from the page object) or;
4144
            // If page context is NOT course, then check across all courses.
4145
            $course = ($this->page->context->contextlevel == CONTEXT_COURSE) ? $this->page->course : null;
4146
 
4147
            if (user_can_view_profile($user, $course)) {
4148
                // Use the user's full name if the heading isn't set.
4149
                if (empty($heading)) {
4150
                    $heading = fullname($user);
4151
                }
4152
 
4153
                $imagedata = $this->user_picture($user, ['size' => 100]);
4154
 
4155
                // Check to see if we should be displaying a message button.
4156
                if (!empty($CFG->messaging) && has_capability('moodle/site:sendmessage', $context)) {
4157
                    $userbuttons = [
4158
                        'messages' => [
4159
                            'buttontype' => 'message',
4160
                            'title' => get_string('message', 'message'),
4161
                            'url' => new moodle_url('/message/index.php', ['id' => $user->id]),
4162
                            'image' => 't/message',
4163
                            'linkattributes' => \core_message\helper::messageuser_link_params($user->id),
4164
                            'page' => $this->page,
4165
                        ],
4166
                    ];
4167
 
4168
                    if ($USER->id != $user->id) {
4169
                        $cancreatecontact = \core_message\api::can_create_contact($USER->id, $user->id);
4170
                        $iscontact = \core_message\api::is_contact($USER->id, $user->id);
4171
                        $isrequested = \core_message\api::get_contact_requests_between_users($USER->id, $user->id);
4172
                        $contacturlaction = '';
4173
                        $linkattributes = \core_message\helper::togglecontact_link_params(
4174
                            $user,
4175
                            $iscontact,
4176
                            true,
4177
                            !empty($isrequested),
4178
                        );
4179
                        // If the user is not a contact.
4180
                        if (!$iscontact) {
4181
                            if ($isrequested) {
4182
                                // Set it to true if a request has been sent.
4183
                                $cancreatecontact = true;
4184
 
4185
                                // We just need the first request.
4186
                                $requests = array_shift($isrequested);
4187
                                if ($requests->userid == $USER->id) {
4188
                                    // If the user has requested to be a contact.
4189
                                    $contacttitle = 'contactrequestsent';
4190
                                } else {
4191
                                    // If the user has been requested to be a contact.
4192
                                    $contacttitle = 'waitingforcontactaccept';
4193
                                }
4194
                                $linkattributes = array_merge($linkattributes, [
4195
                                    'class' => 'disabled',
4196
                                    'tabindex' => '-1',
4197
                                ]);
4198
                            } else {
4199
                                // If the user is not a contact and has not requested to be a contact.
4200
                                $contacttitle = 'addtoyourcontacts';
4201
                                $contacturlaction = 'addcontact';
4202
                            }
4203
                            $contactimage = 't/addcontact';
4204
                        } else {
4205
                            // If the user is a contact.
4206
                            $contacttitle = 'removefromyourcontacts';
4207
                            $contacturlaction = 'removecontact';
4208
                            $contactimage = 't/removecontact';
4209
                        }
4210
                        if ($cancreatecontact) {
4211
                            $userbuttons['togglecontact'] = [
4212
                                'buttontype' => 'togglecontact',
4213
                                'title' => get_string($contacttitle, 'message'),
4214
                                'url' => new moodle_url('/message/index.php', [
4215
                                    'user1' => $USER->id,
4216
                                    'user2' => $user->id,
4217
                                    $contacturlaction => $user->id,
4218
                                    'sesskey' => sesskey(),
4219
                                ]),
4220
                                'image' => $contactimage,
4221
                                'linkattributes' => $linkattributes,
4222
                                'page' => $this->page,
4223
                            ];
4224
                        }
4225
                    }
4226
                }
4227
            } else {
4228
                $heading = null;
4229
            }
4230
        }
4231
 
4232
        // Return the heading wrapped in an visually-hidden element so it is only visible to screen-readers
4233
        // for nocontextheader layouts.
4234
        if (!empty($this->page->layout_options['nocontextheader'])) {
4235
            return html_writer::div($heading, 'visually-hidden');
4236
        }
4237
 
4238
        $contextheader = new context_header($heading, $headinglevel, $imagedata, $userbuttons);
4239
        return $this->render($contextheader);
4240
    }
4241
 
4242
    /**
4243
     * Renders the header bar.
4244
     *
4245
     * @param context_header $contextheader Header bar object.
4246
     * @return string HTML for the header bar.
4247
     * @deprecated since 4.5 Please use core_renderer::render($contextheader) instead
4248
     * @todo MDL-82163 This will be deleted in Moodle 6.0.
4249
     */
4250
    #[\core\attribute\deprecated('core_renderer::render($contextheader)', since: '4.5', mdl: 'MDL-82160')]
4251
    protected function render_context_header(context_header $contextheader) {
4252
        $context = $contextheader->export_for_template($this);
4253
        return $this->render_from_template('core/context_header', $context);
4254
    }
4255
 
4256
    /**
4257
     * Renders the skip links for the page.
4258
     *
4259
     * @param array $links List of skip links.
4260
     * @return string HTML for the skip links.
4261
     */
4262
    public function render_skip_links($links) {
4263
        $context = [ 'links' => []];
4264
 
4265
        foreach ($links as $url => $text) {
4266
            $context['links'][] = [ 'url' => $url, 'text' => $text];
4267
        }
4268
 
4269
        return $this->render_from_template('core/skip_links', $context);
4270
    }
4271
 
4272
    /**
4273
     * Wrapper for header elements.
4274
     *
4275
     * @return string HTML to display the main header.
4276
     */
4277
    public function full_header() {
4278
        $pagetype = $this->page->pagetype;
4279
        $homepage = get_home_page();
4280
        $homepagetype = null;
4281
        // Add a special case since /my/courses is a part of the /my subsystem.
4282
        if ($homepage == HOMEPAGE_MY || $homepage == HOMEPAGE_MYCOURSES) {
4283
            $homepagetype = 'my-index';
4284
        } else if ($homepage == HOMEPAGE_SITE) {
4285
            $homepagetype = 'site-index';
4286
        }
4287
        if (
4288
            $this->page->include_region_main_settings_in_header_actions() &&
4289
                !$this->page->blocks->is_block_present('settings')
4290
        ) {
4291
            // Only include the region main settings if the page has requested it and it doesn't already have
4292
            // the settings block on it. The region main settings are included in the settings block and
4293
            // duplicating the content causes behat failures.
4294
            $this->page->add_header_action(html_writer::div(
4295
                $this->region_main_settings_menu(),
4296
                'd-print-none',
4297
                ['id' => 'region-main-settings-menu']
4298
            ));
4299
        }
4300
 
4301
        $header = new stdClass();
4302
        $header->settingsmenu = $this->context_header_settings_menu();
4303
        $header->contextheader = $this->context_header();
4304
        $header->hasnavbar = empty($this->page->layout_options['nonavbar']);
4305
        $header->navbar = $this->navbar();
4306
        $header->pageheadingbutton = $this->page_heading_button();
4307
        $header->courseheader = $this->course_header();
4308
        $header->headeractions = $this->page->get_header_actions();
4309
        if (!empty($pagetype) && !empty($homepagetype) && $pagetype == $homepagetype) {
4310
            $header->welcomemessage = \core_user::welcome_message();
4311
        }
4312
        return $this->render_from_template('core/full_header', $header);
4313
    }
4314
 
4315
    /**
4316
     * This is an optional menu that can be added to a layout by a theme. It contains the
4317
     * menu for the course administration, only on the course main page.
4318
     *
4319
     * @return string
4320
     */
4321
    public function context_header_settings_menu() {
4322
        $context = $this->page->context;
4323
        $menu = new action_menu();
4324
 
4325
        $items = $this->page->navbar->get_items();
4326
        $currentnode = end($items);
4327
 
4328
        $showcoursemenu = false;
4329
        $showfrontpagemenu = false;
4330
        $showusermenu = false;
4331
 
4332
        // We are on the course home page.
4333
        if (
4334
            ($context->contextlevel == CONTEXT_COURSE) &&
4335
                !empty($currentnode) &&
4336
                ($currentnode->type == navigation_node::TYPE_COURSE || $currentnode->type == navigation_node::TYPE_SECTION)
4337
        ) {
4338
            $showcoursemenu = true;
4339
        }
4340
 
4341
        $courseformat = course_get_format($this->page->course);
4342
        // This is a single activity course format, always show the course menu on the activity main page.
4343
        if (
4344
            $context->contextlevel == CONTEXT_MODULE &&
4345
                !$courseformat->has_view_page()
4346
        ) {
4347
            $this->page->navigation->initialise();
4348
            $activenode = $this->page->navigation->find_active_node();
4349
            // If the settings menu has been forced then show the menu.
4350
            if ($this->page->is_settings_menu_forced()) {
4351
                $showcoursemenu = true;
4352
            } else if (
4353
                !empty($activenode) && ($activenode->type == navigation_node::TYPE_ACTIVITY ||
4354
                            $activenode->type == navigation_node::TYPE_RESOURCE)
4355
            ) {
4356
                // We only want to show the menu on the first page of the activity. This means
4357
                // the breadcrumb has no additional nodes.
4358
                if ($currentnode && ($currentnode->key == $activenode->key && $currentnode->type == $activenode->type)) {
4359
                    $showcoursemenu = true;
4360
                }
4361
            }
4362
        }
4363
 
4364
        // This is the site front page.
4365
        if (
4366
            $context->contextlevel == CONTEXT_COURSE &&
4367
                !empty($currentnode) &&
4368
                $currentnode->key === 'home'
4369
        ) {
4370
            $showfrontpagemenu = true;
4371
        }
4372
 
4373
        // This is the user profile page.
4374
        if (
4375
            $context->contextlevel == CONTEXT_USER &&
4376
                !empty($currentnode) &&
4377
                ($currentnode->key === 'myprofile')
4378
        ) {
4379
            $showusermenu = true;
4380
        }
4381
 
4382
        if ($showfrontpagemenu) {
4383
            $settingsnode = $this->page->settingsnav->find('frontpage', navigation_node::TYPE_SETTING);
4384
            if ($settingsnode) {
4385
                // Build an action menu based on the visible nodes from this navigation tree.
4386
                $skipped = $this->build_action_menu_from_navigation($menu, $settingsnode, false, true);
4387
 
4388
                // We only add a list to the full settings menu if we didn't include every node in the short menu.
4389
                if ($skipped) {
4390
                    $text = get_string('morenavigationlinks');
4391
                    $url = \core\router\util::get_path_for_callable(
4392
                        [\core_course\route\controller\course_management::class, 'administer_course'],
4393
                        ['course' => $this->page->course->id],
4394
                    );
4395
                    $link = new action_link($url, $text, null, null, new pix_icon('t/edit', $text));
4396
                    $menu->add_secondary_action($link);
4397
                }
4398
            }
4399
        } else if ($showcoursemenu) {
4400
            $settingsnode = $this->page->settingsnav->find('courseadmin', navigation_node::TYPE_COURSE);
4401
            if ($settingsnode) {
4402
                // Build an action menu based on the visible nodes from this navigation tree.
4403
                $skipped = $this->build_action_menu_from_navigation($menu, $settingsnode, false, true);
4404
 
4405
                // We only add a list to the full settings menu if we didn't include every node in the short menu.
4406
                if ($skipped) {
4407
                    $text = get_string('morenavigationlinks');
4408
                    $url = \core\router\util::get_path_for_callable(
4409
                        [\core_course\route\controller\course_management::class, 'administer_course'],
4410
                        ['course' => $this->page->course->id],
4411
                    );
4412
                    $link = new action_link($url, $text, null, null, new pix_icon('t/edit', $text));
4413
                    $menu->add_secondary_action($link);
4414
                }
4415
            }
4416
        } else if ($showusermenu) {
4417
            // Get the course admin node from the settings navigation.
4418
            $settingsnode = $this->page->settingsnav->find('useraccount', navigation_node::TYPE_CONTAINER);
4419
            if ($settingsnode) {
4420
                // Build an action menu based on the visible nodes from this navigation tree.
4421
                $this->build_action_menu_from_navigation($menu, $settingsnode);
4422
            }
4423
        }
4424
 
4425
        return $this->render($menu);
4426
    }
4427
 
4428
    /**
4429
     * Take a node in the nav tree and make an action menu out of it.
4430
     * The links are injected in the action menu.
4431
     *
4432
     * @param action_menu $menu
4433
     * @param navigation_node $node
4434
     * @param boolean $indent
4435
     * @param boolean $onlytopleafnodes
4436
     * @return boolean nodesskipped - True if nodes were skipped in building the menu
4437
     */
4438
    protected function build_action_menu_from_navigation(
4439
        action_menu $menu,
4440
        navigation_node $node,
4441
        $indent = false,
4442
        $onlytopleafnodes = false
4443
    ) {
4444
        $skipped = false;
4445
        // Build an action menu based on the visible nodes from this navigation tree.
4446
        foreach ($node->children as $menuitem) {
4447
            if ($menuitem->display) {
4448
                if ($onlytopleafnodes && $menuitem->children->count()) {
4449
                    $skipped = true;
4450
                    continue;
4451
                }
4452
                if ($menuitem->action) {
4453
                    if ($menuitem->action instanceof action_link) {
4454
                        $link = $menuitem->action;
4455
                        // Give preference to setting icon over action icon.
4456
                        if (!empty($menuitem->icon)) {
4457
                            $link->icon = $menuitem->icon;
4458
                        }
4459
                    } else {
4460
                        $link = new action_link($menuitem->action, $menuitem->text, null, null, $menuitem->icon);
4461
                    }
4462
                } else {
4463
                    if ($onlytopleafnodes) {
4464
                        $skipped = true;
4465
                        continue;
4466
                    }
4467
                    $link = new action_link(new moodle_url('#'), $menuitem->text, null, ['disabled' => true], $menuitem->icon);
4468
                }
4469
                if ($indent) {
4470
                    $link->add_class('ms-4');
4471
                }
4472
                if (!empty($menuitem->classes)) {
4473
                    $link->add_class(implode(" ", $menuitem->classes));
4474
                }
4475
 
4476
                $menu->add_secondary_action($link);
4477
                $skipped = $skipped || $this->build_action_menu_from_navigation($menu, $menuitem, true);
4478
            }
4479
        }
4480
        return $skipped;
4481
    }
4482
 
4483
    /**
4484
     * This is an optional menu that can be added to a layout by a theme. It contains the
4485
     * menu for the most specific thing from the settings block. E.g. Module administration.
4486
     *
4487
     * @return string
4488
     */
4489
    public function region_main_settings_menu() {
4490
        $context = $this->page->context;
4491
        $menu = new action_menu();
4492
 
4493
        if ($context->contextlevel == CONTEXT_MODULE) {
4494
            $this->page->navigation->initialise();
4495
            $node = $this->page->navigation->find_active_node();
4496
            $buildmenu = false;
4497
            // If the settings menu has been forced then show the menu.
4498
            if ($this->page->is_settings_menu_forced()) {
4499
                $buildmenu = true;
4500
            } else if (
4501
                !empty($node) && ($node->type == navigation_node::TYPE_ACTIVITY ||
4502
                            $node->type == navigation_node::TYPE_RESOURCE)
4503
            ) {
4504
                $items = $this->page->navbar->get_items();
4505
                $navbarnode = end($items);
4506
                // We only want to show the menu on the first page of the activity. This means
4507
                // the breadcrumb has no additional nodes.
4508
                if ($navbarnode && ($navbarnode->key === $node->key && $navbarnode->type == $node->type)) {
4509
                    $buildmenu = true;
4510
                }
4511
            }
4512
            if ($buildmenu) {
4513
                // Get the course admin node from the settings navigation.
4514
                $node = $this->page->settingsnav->find('modulesettings', navigation_node::TYPE_SETTING);
4515
                if ($node) {
4516
                    // Build an action menu based on the visible nodes from this navigation tree.
4517
                    $this->build_action_menu_from_navigation($menu, $node);
4518
                }
4519
            }
4520
        } else if ($context->contextlevel == CONTEXT_COURSECAT) {
4521
            // For course category context, show category settings menu, if we're on the course category page.
4522
            if ($this->page->pagetype === 'course-index-category') {
4523
                $node = $this->page->settingsnav->find('categorysettings', navigation_node::TYPE_CONTAINER);
4524
                if ($node) {
4525
                    // Build an action menu based on the visible nodes from this navigation tree.
4526
                    $this->build_action_menu_from_navigation($menu, $node);
4527
                }
4528
            }
4529
        } else {
4530
            $items = $this->page->navbar->get_items();
4531
            $navbarnode = end($items);
4532
 
4533
            if ($navbarnode && ($navbarnode->key === 'participants')) {
4534
                $node = $this->page->settingsnav->find('users', navigation_node::TYPE_CONTAINER);
4535
                if ($node) {
4536
                    // Build an action menu based on the visible nodes from this navigation tree.
4537
                    $this->build_action_menu_from_navigation($menu, $node);
4538
                }
4539
            }
4540
        }
4541
        return $this->render($menu);
4542
    }
4543
 
4544
    /**
4545
     * Displays the list of tags associated with an entry
4546
     *
4547
     * @param array $tags list of instances of core_tag or stdClass
4548
     * @param string $label label to display in front, by default 'Tags' (get_string('tags')), set to null
4549
     *               to use default, set to '' (empty string) to omit the label completely
4550
     * @param string $classes additional classes for the enclosing div element
4551
     * @param int $limit limit the number of tags to display, if size of $tags is more than this limit the "more" link
4552
     *               will be appended to the end, JS will toggle the rest of the tags
4553
     * @param context $pagecontext specify if needed to overwrite the current page context for the view tag link
4554
     * @param bool $accesshidelabel if true, the label should have class="accesshide" added.
4555
     * @param bool $displaylink Indicates whether the tag should be displayed as a link.
4556
     * @return string
4557
     */
4558
    public function tag_list(
4559
        $tags,
4560
        $label = null,
4561
        $classes = '',
4562
        $limit = 10,
4563
        $pagecontext = null,
4564
        $accesshidelabel = false,
4565
        $displaylink = true,
4566
    ) {
4567
        $list = new taglist($tags, $label, $classes, $limit, $pagecontext, $accesshidelabel, $displaylink);
4568
        return $this->render_from_template('core_tag/taglist', $list->export_for_template($this));
4569
    }
4570
 
4571
    /**
4572
     * Renders element for inline editing of any value
4573
     *
4574
     * @param inplace_editable $element
4575
     * @return string
4576
     */
4577
    public function render_inplace_editable(inplace_editable $element) {
4578
        return $this->render_from_template('core/inplace_editable', $element->export_for_template($this));
4579
    }
4580
 
4581
    /**
4582
     * Renders a bar chart.
4583
     *
4584
     * @param \core\chart_bar $chart The chart.
4585
     * @return string
4586
     */
4587
    public function render_chart_bar(\core\chart_bar $chart) {
4588
        return $this->render_chart($chart);
4589
    }
4590
 
4591
    /**
4592
     * Renders a line chart.
4593
     *
4594
     * @param \core\chart_line $chart The chart.
4595
     * @return string
4596
     */
4597
    public function render_chart_line(\core\chart_line $chart) {
4598
        return $this->render_chart($chart);
4599
    }
4600
 
4601
    /**
4602
     * Renders a pie chart.
4603
     *
4604
     * @param \core\chart_pie $chart The chart.
4605
     * @return string
4606
     */
4607
    public function render_chart_pie(\core\chart_pie $chart) {
4608
        return $this->render_chart($chart);
4609
    }
4610
 
4611
    /**
4612
     * Renders a chart.
4613
     *
4614
     * @param \core\chart_base $chart The chart.
4615
     * @param bool $withtable Whether to include a data table with the chart.
4616
     * @return string
4617
     */
4618
    public function render_chart(\core\chart_base $chart, $withtable = true) {
4619
        $chartdata = json_encode($chart);
4620
        return $this->render_from_template('core/chart', (object) [
4621
            'chartdata' => $chartdata,
4622
            'withtable' => $withtable,
4623
        ]);
4624
    }
4625
 
4626
    /**
4627
     * Renders the login form.
4628
     *
4629
     * @param \core_auth\output\login $form The renderable.
4630
     * @return string
4631
     */
4632
    public function render_login(\core_auth\output\login $form) {
4633
        global $CFG, $SITE;
4634
 
4635
        $context = $form->export_for_template($this);
4636
 
4637
        $context->errorformatted = $this->error_text($context->error);
4638
        $url = $this->get_logo_url();
4639
        if ($url) {
4640
            $url = $url->out(false);
4641
        }
4642
        $context->logourl = $url;
4643
        $context->sitename = format_string(
4644
            $SITE->fullname,
4645
            true,
4646
            ['context' => context_course::instance(SITEID), "escape" => false]
4647
        );
4648
 
4649
        return $this->render_from_template('core/loginform', $context);
4650
    }
4651
 
4652
    /**
4653
     * Renders an mform element from a template.
4654
     *
4655
     * @param HTML_QuickForm_element $element element
4656
     * @param bool $required if input is required field
4657
     * @param bool $advanced if input is an advanced field
4658
     * @param string $error error message to display
4659
     * @param bool $ingroup True if this element is rendered as part of a group
4660
     * @return mixed string|bool
4661
     */
4662
    public function mform_element($element, $required, $advanced, $error, $ingroup) {
4663
        $templatename = 'core_form/element-' . $element->getType();
4664
        if ($ingroup) {
4665
            $templatename .= "-inline";
4666
        }
4667
        try {
4668
            // We call this to generate a file not found exception if there is no template.
4669
            // We don't want to call export_for_template if there is no template.
4670
            mustache_template_finder::get_template_filepath($templatename);
4671
 
4672
            if ($element instanceof templatable) {
4673
                $elementcontext = $element->export_for_template($this);
4674
 
4675
                $helpbutton = '';
4676
                if (method_exists($element, 'getHelpButton')) {
4677
                    $helpbutton = $element->getHelpButton();
4678
                }
4679
                $label = $element->getLabel();
4680
                $text = '';
4681
                if (method_exists($element, 'getText')) {
4682
                    // There currently exists code that adds a form element with an empty label.
4683
                    // If this is the case then set the label to the description.
4684
                    if (empty($label)) {
4685
                        $label = $element->getText();
4686
                    } else {
4687
                        $text = $element->getText();
4688
                    }
4689
                }
4690
 
4691
                // Generate the form element wrapper ids and names to pass to the template.
4692
                // This differs between group and non-group elements.
4693
                if ($element->getType() === 'group') {
4694
                    // Group element.
4695
                    // The id will be something like 'fgroup_id_NAME'. E.g. fgroup_id_mygroup.
4696
                    $elementcontext['wrapperid'] = $elementcontext['id'];
4697
 
4698
                    // Ensure group elements pass through the group name as the element name.
4699
                    $elementcontext['name'] = $elementcontext['groupname'];
4700
                } else {
4701
                    // Non grouped element.
4702
                    // Creates an id like 'fitem_id_NAME'. E.g. fitem_id_mytextelement.
4703
                    $elementcontext['wrapperid'] = 'fitem_' . $elementcontext['id'];
4704
                }
4705
 
4706
                $context = [
4707
                    'element' => $elementcontext,
4708
                    'label' => $label,
4709
                    'text' => $text,
4710
                    'required' => $required,
4711
                    'advanced' => $advanced,
4712
                    'helpbutton' => $helpbutton,
4713
                    'error' => $error,
4714
                ];
4715
                return $this->render_from_template($templatename, $context);
4716
            }
4717
        } catch (\Exception $e) {
4718
            // No template for this element.
4719
            return false;
4720
        }
4721
    }
4722
 
4723
    /**
4724
     * Render the login signup form into a nice template for the theme.
4725
     *
4726
     * @param moodleform $form
4727
     * @return string
4728
     */
4729
    public function render_login_signup_form($form) {
4730
        global $SITE;
4731
 
4732
        $context = $form->export_for_template($this);
4733
        $url = $this->get_logo_url();
4734
        if ($url) {
4735
            $url = $url->out(false);
4736
        }
4737
        $context['logourl'] = $url;
4738
        $context['sitename'] = format_string(
4739
            $SITE->fullname,
4740
            true,
4741
            ['context' => context_course::instance(SITEID), "escape" => false]
4742
        );
4743
 
4744
        return $this->render_from_template('core/signup_form_layout', $context);
4745
    }
4746
 
4747
    /**
4748
     * Render the verify age and location page into a nice template for the theme.
4749
     *
4750
     * @param \core_auth\output\verify_age_location_page $page The renderable
4751
     * @return string
4752
     */
4753
    protected function render_verify_age_location_page($page) {
4754
        $context = $page->export_for_template($this);
4755
 
4756
        return $this->render_from_template('core/auth_verify_age_location_page', $context);
4757
    }
4758
 
4759
    /**
4760
     * Render the digital minor contact information page into a nice template for the theme.
4761
     *
4762
     * @param \core_auth\output\digital_minor_page $page The renderable
4763
     * @return string
4764
     */
4765
    protected function render_digital_minor_page($page) {
4766
        $context = $page->export_for_template($this);
4767
 
4768
        return $this->render_from_template('core/auth_digital_minor_page', $context);
4769
    }
4770
 
4771
    /**
4772
     * Renders a progress bar.
4773
     *
4774
     * Do not use $OUTPUT->render($bar), instead use progress_bar::create().
4775
     *
4776
     * @param  progress_bar $bar The bar.
4777
     * @return string HTML fragment
4778
     */
4779
    public function render_progress_bar(progress_bar $bar) {
4780
        $data = $bar->export_for_template($this);
4781
        return $this->render_from_template('core/progress_bar', $data);
4782
    }
4783
 
4784
    /**
4785
     * Renders an update to a progress bar.
4786
     *
4787
     * Note: This does not cleanly map to a renderable class and should
4788
     * never be used directly.
4789
     *
4790
     * @param  string $id
4791
     * @param  float $percent
4792
     * @param  string $msg Message
4793
     * @param  string $estimate time remaining message
4794
     * @param  bool $error Was there an error?
4795
     * @return string ascii fragment
4796
     */
4797
    public function render_progress_bar_update(string $id, float $percent, string $msg, string $estimate,
4798
        bool $error = false): string {
4799
        return html_writer::script(js_writer::function_call('updateProgressBar', [
4800
            $id,
4801
            round($percent, 1),
4802
            $msg,
4803
            $estimate,
4804
            $error,
4805
        ]));
4806
    }
4807
 
4808
    /**
4809
     * Renders element for a toggle-all checkbox.
4810
     *
4811
     * @param checkbox_toggleall $element
4812
     * @return string
4813
     */
4814
    public function render_checkbox_toggleall(checkbox_toggleall $element) {
4815
        return $this->render_from_template($element->get_template(), $element->export_for_template($this));
4816
    }
4817
 
4818
    /**
4819
     * Renders the tertiary nav for the participants page
4820
     *
4821
     * @param object $course The course we are operating within
4822
     * @param string|null $renderedbuttons Any additional buttons/content to be displayed in line with the nav
4823
     * @return string
4824
     */
4825
    public function render_participants_tertiary_nav(object $course, ?string $renderedbuttons = null) {
4826
        $actionbar = new participants_action_bar($course, $this->page, $renderedbuttons);
4827
        $content = $this->render_from_template('core_course/participants_actionbar', $actionbar->export_for_template($this));
4828
        return $content ?: "";
4829
    }
4830
 
4831
    /**
4832
     * Renders release information in the footer popup
4833
     * @return ?string Moodle release info.
4834
     */
4835
    public function moodle_release() {
4836
        global $CFG;
4837
        if (!during_initial_install() && is_siteadmin()) {
4838
            return $CFG->release;
4839
        }
4840
    }
4841
 
4842
    /**
4843
     * Generate the add block button when editing mode is turned on and the user can edit blocks.
4844
     *
4845
     * @param string $region where new blocks should be added.
4846
     * @return string html for the add block button.
4847
     */
4848
    public function addblockbutton($region = ''): string {
4849
        $addblockbutton = '';
4850
        $regions = $this->page->blocks->get_regions();
4851
        if (count($regions) == 0) {
4852
            return '';
4853
        }
4854
        if (
4855
            isset($this->page->theme->addblockposition) &&
4856
                $this->page->user_is_editing() &&
4857
                $this->page->user_can_edit_blocks() &&
4858
                $this->page->pagelayout !== 'mycourses'
4859
        ) {
4860
            $params = ['bui_addblock' => '', 'sesskey' => sesskey()];
4861
            if (!empty($region)) {
4862
                $params['bui_blockregion'] = $region;
4863
            }
4864
            $url = new moodle_url($this->page->url, $params);
4865
            $addblockbutton = $this->render_from_template(
4866
                'core/add_block_button',
4867
                [
4868
                    'link' => $url->out(false),
4869
                    'escapedlink' => "?{$url->get_query_string(false)}",
4870
                    'pagehash' => $this->page->get_edited_page_hash(),
4871
                    'blockregion' => $region,
4872
                    // The following parameters are not used since Moodle 4.2 but are
4873
                    // still passed for backward-compatibility.
4874
                    'pageType' => $this->page->pagetype,
4875
                    'pageLayout' => $this->page->pagelayout,
4876
                    'subPage' => $this->page->subpage,
4877
                ]
4878
            );
4879
        }
4880
        return $addblockbutton;
4881
    }
4882
 
4883
    /**
4884
     * Prepares an element for streaming output
4885
     *
4886
     * This must be used with NO_OUTPUT_BUFFERING set to true. After using this method
4887
     * any subsequent prints or echos to STDOUT result in the outputted content magically
4888
     * being appended inside that element rather than where the current html would be
4889
     * normally. This enables pages which take some time to render incremental content to
4890
     * first output a fully formed html page, including the footer, and to then stream
4891
     * into an element such as the main content div. This fixes a class of page layout
4892
     * bugs and reduces layout shift issues and was inspired by Facebook BigPipe.
4893
     *
4894
     * Some use cases such as a simple page which loads content via ajax could be swapped
4895
     * to this method wich saves another http request and its network latency resulting
4896
     * in both lower server load and better front end performance.
4897
     *
4898
     * You should consider giving the element you stream into a minimum height to further
4899
     * reduce layout shift as the content initally streams into the element.
4900
     *
4901
     * You can safely finish the output without closing the streamed element. You can also
4902
     * call this method again to swap the target of the streaming to a new element as
4903
     * often as you want.
4904
 
4905
     * https://www.youtube.com/watch?v=LLRig4s1_yA&t=1022s
4906
     * Watch this video segment to explain how and why this 'One Weird Trick' works.
4907
     *
4908
     * @param string $selector where new content should be appended
4909
     * @param string $element which contains the streamed content
4910
     * @return string html to be written
4911
     */
4912
    public function select_element_for_append(string $selector = '#region-main [role=main]', string $element = 'div') {
4913
 
4914
        if (!CLI_SCRIPT && !NO_OUTPUT_BUFFERING) {
4915
            throw new coding_exception(
4916
                'select_element_for_append used in a non-CLI script without setting NO_OUTPUT_BUFFERING.',
4917
                DEBUG_DEVELOPER
4918
            );
4919
        }
4920
 
4921
        // We are already streaming into this element so don't change anything.
4922
        if ($this->currentselector === $selector && $this->currentelement === $element) {
4923
            return;
4924
        }
4925
 
4926
        // If we have a streaming element close it before starting a new one.
4927
        $html = $this->close_element_for_append();
4928
 
4929
        $this->currentselector = $selector;
4930
        $this->currentelement = $element;
4931
 
4932
        // Create an unclosed element for the streamed content to append into.
4933
        $id = uniqid();
4934
        $html .= html_writer::start_tag($element, ['id' => $id]);
4935
        $html .= html_writer::tag('script', "document.querySelector('$selector').append(document.getElementById('$id'))");
4936
        $html .= "\n";
4937
        return $html;
4938
    }
4939
 
4940
    /**
4941
     * This closes any opened stream elements
4942
     *
4943
     * @return string html to be written
4944
     */
4945
    public function close_element_for_append() {
4946
        $html = '';
4947
        if ($this->currentselector !== '') {
4948
            $html .= html_writer::end_tag($this->currentelement);
4949
            $html .= "\n";
4950
            $this->currentelement = '';
4951
        }
4952
        return $html;
4953
    }
4954
 
4955
    /**
4956
     * A companion method to select_element_for_append
4957
     *
4958
     * This must be used with NO_OUTPUT_BUFFERING set to true.
4959
     *
4960
     * This is similar but instead of appending into the element it replaces
4961
     * the content in the element. Depending on the 3rd argument it can replace
4962
     * the innerHTML or the outerHTML which can be useful to completely remove
4963
     * the element if needed.
4964
     *
4965
     * @param string $selector where new content should be replaced
4966
     * @param string $html A chunk of well formed html
4967
     * @param bool $outer Wether it replaces the innerHTML or the outerHTML
4968
     * @return string html to be written
4969
     */
4970
    public function select_element_for_replace(string $selector, string $html, bool $outer = false) {
4971
 
4972
        if (!CLI_SCRIPT && !NO_OUTPUT_BUFFERING) {
4973
            throw new coding_exception(
4974
                'select_element_for_replace used in a non-CLI script without setting NO_OUTPUT_BUFFERING.',
4975
                DEBUG_DEVELOPER
4976
            );
4977
        }
4978
 
4979
        // Escape html for use inside a javascript string.
4980
        $html = addslashes_js($html);
4981
        $property = $outer ? 'outerHTML' : 'innerHTML';
4982
        $output = html_writer::tag('script', "document.querySelector('$selector').$property = '$html';");
4983
        $output .= "\n";
4984
        return $output;
4985
    }
4986
}
4987
// Alias this class to the old name.
4988
// This file will be autoloaded by the legacyclasses autoload system.
4989
// In future all uses of this class will be corrected and the legacy references will be removed.
4990
class_alias(core_renderer::class, \core_renderer::class);