outputrenderers.php 199 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * Classes for rendering HTML output for Moodle.
 *
20
 * Please see {@link http://docs.moodle.org/en/Developement:How_Moodle_outputs_HTML}
21
22
 * for an overview.
 *
23
24
25
26
27
28
29
30
31
 * Included in this file are the primary renderer classes:
 *     - renderer_base:         The renderer outline class that all renderers
 *                              should inherit from.
 *     - core_renderer:         The standard HTML renderer.
 *     - core_renderer_cli:     An adaption of the standard renderer for CLI scripts.
 *     - core_renderer_ajax:    An adaption of the standard renderer for AJAX scripts.
 *     - plugin_renderer_base:  A renderer class that should be extended by all
 *                              plugin renderers.
 *
32
 * @package core
33
 * @category output
34
35
 * @copyright  2009 Tim Hunt
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
36
37
 */

38
39
defined('MOODLE_INTERNAL') || die();

40
41
42
43
44
45
46
47
/**
 * Simple base class for Moodle renderers.
 *
 * Tracks the xhtml_container_stack to use, which is passed in in the constructor.
 *
 * Also has methods to facilitate generating HTML output.
 *
 * @copyright 2009 Tim Hunt
48
49
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 * @since Moodle 2.0
50
 * @package core
51
 * @category output
52
 */
53
class renderer_base {
54
    /**
55
     * @var xhtml_container_stack The xhtml_container_stack to use.
56
     */
57
    protected $opencontainers;
58
59

    /**
60
     * @var moodle_page The Moodle page the renderer has been created to assist with.
61
     */
62
    protected $page;
63
64

    /**
65
     * @var string The requested rendering target.
66
     */
67
    protected $target;
68

69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
    /**
     * @var Mustache_Engine $mustache The mustache template compiler
     */
    private $mustache;

    /**
     * Return an instance of the mustache class.
     *
     * @since 2.9
     * @return Mustache_Engine
     */
    protected function get_mustache() {
        global $CFG;

        if ($this->mustache === null) {
84
85
            require_once("{$CFG->libdir}/filelib.php");

86
87
88
            $themename = $this->page->theme->name;
            $themerev = theme_get_revision();

89
            // Create new localcache directory.
90
91
            $cachedir = make_localcache_directory("mustache/$themerev/$themename");

92
93
94
95
96
97
98
99
100
101
102
            // Remove old localcache directories.
            $mustachecachedirs = glob("{$CFG->localcachedir}/mustache/*", GLOB_ONLYDIR);
            foreach ($mustachecachedirs as $localcachedir) {
                $cachedrev = [];
                preg_match("/\/mustache\/([0-9]+)$/", $localcachedir, $cachedrev);
                $cachedrev = isset($cachedrev[1]) ? intval($cachedrev[1]) : 0;
                if ($cachedrev > 0 && $cachedrev < $themerev) {
                    fulldelete($localcachedir);
                }
            }

103
            $loader = new \core\output\mustache_filesystem_loader();
104
            $stringhelper = new \core\output\mustache_string_helper();
105
            $quotehelper = new \core\output\mustache_quote_helper();
106
            $jshelper = new \core\output\mustache_javascript_helper($this->page);
107
            $pixhelper = new \core\output\mustache_pix_helper($this);
108
            $shortentexthelper = new \core\output\mustache_shorten_text_helper();
109
            $userdatehelper = new \core\output\mustache_user_date_helper();
110
111
112
113
114
115

            // We only expose the variables that are exposed to JS templates.
            $safeconfig = $this->page->requires->get_config_for_javascript($this->page, $this);

            $helpers = array('config' => $safeconfig,
                             'str' => array($stringhelper, 'str'),
116
                             'quote' => array($quotehelper, 'quote'),
117
                             'js' => array($jshelper, 'help'),
118
                             'pix' => array($pixhelper, 'pix'),
119
120
121
                             'shortentext' => array($shortentexthelper, 'shorten'),
                             'userdate' => array($userdatehelper, 'transform'),
                         );
122
123
124
125
126

            $this->mustache = new Mustache_Engine(array(
                'cache' => $cachedir,
                'escape' => 's',
                'loader' => $loader,
127
128
                'helpers' => $helpers,
                'pragmas' => [Mustache_Engine::PRAGMA_BLOCKS]));
129
130
131
132
133
134
135

        }

        return $this->mustache;
    }


136
137
    /**
     * Constructor
138
     *
139
     * The constructor takes two arguments. The first is the page that the renderer
140
141
142
143
     * has been created to assist with, and the second is the target.
     * The target is an additional identifier that can be used to load different
     * renderers for different options.
     *
144
     * @param moodle_page $page the page we are doing output for.
145
     * @param string $target one of rendering target constants
146
     */
147
    public function __construct(moodle_page $page, $target) {
148
149
        $this->opencontainers = $page->opencontainers;
        $this->page = $page;
150
        $this->target = $target;
151
152
    }

153
154
155
156
157
158
159
160
161
162
163
164
165
166
    /**
     * Renders a template by name with the given context.
     *
     * The provided data needs to be array/stdClass made up of only simple types.
     * Simple types are array,stdClass,bool,int,float,string
     *
     * @since 2.9
     * @param array|stdClass $context Context containing data for the template.
     * @return string|boolean
     */
    public function render_from_template($templatename, $context) {
        static $templatecache = array();
        $mustache = $this->get_mustache();

167
168
        try {
            // Grab a copy of the existing helper to be restored later.
169
            $uniqidhelper = $mustache->getHelper('uniqid');
170
171
        } catch (Mustache_Exception_UnknownHelperException $e) {
            // Helper doesn't exist.
172
            $uniqidhelper = null;
173
174
        }

175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
        // Provide 1 random value that will not change within a template
        // but will be different from template to template. This is useful for
        // e.g. aria attributes that only work with id attributes and must be
        // unique in a page.
        $mustache->addHelper('uniqid', new \core\output\mustache_uniqid_helper());
        if (isset($templatecache[$templatename])) {
            $template = $templatecache[$templatename];
        } else {
            try {
                $template = $mustache->loadTemplate($templatename);
                $templatecache[$templatename] = $template;
            } catch (Mustache_Exception_UnknownTemplateException $e) {
                throw new moodle_exception('Unknown template: ' . $templatename);
            }
        }
190

191
        $renderedtemplate = trim($template->render($context));
192
193
194

        // If we had an existing uniqid helper then we need to restore it to allow
        // handle nested calls of render_from_template.
195
196
        if ($uniqidhelper) {
            $mustache->addHelper('uniqid', $uniqidhelper);
197
198
        }

199
        return $renderedtemplate;
200
201
202
    }


203
    /**
204
     * Returns rendered widget.
205
206
207
     *
     * The provided widget needs to be an object that extends the renderable
     * interface.
208
     * If will then be rendered by a method based upon the classname for the widget.
209
210
     * For instance a widget of class `crazywidget` will be rendered by a protected
     * render_crazywidget method of this renderer.
211
212
     * If no render_crazywidget method exists and crazywidget implements templatable,
     * look for the 'crazywidget' template in the same component and render that.
213
     *
Petr Skoda's avatar
Petr Skoda committed
214
     * @param renderable $widget instance with renderable interface
215
     * @return string
216
     */
217
    public function render(renderable $widget) {
218
        $classparts = explode('\\', get_class($widget));
219
        // Strip namespaces.
220
        $classname = array_pop($classparts);
221
222
223
224
        // Remove _renderable suffixes
        $classname = preg_replace('/_renderable$/', '', $classname);

        $rendermethod = 'render_'.$classname;
225
226
227
        if (method_exists($this, $rendermethod)) {
            return $this->$rendermethod($widget);
        }
228
229
230
231
232
233
234
235
236
        if ($widget instanceof templatable) {
            $component = array_shift($classparts);
            if (!$component) {
                $component = 'core';
            }
            $template = $component . '/' . $classname;
            $context = $widget->export_for_template($this);
            return $this->render_from_template($template, $context);
        }
237
        throw new coding_exception('Can not render widget, renderer method ('.$rendermethod.') not found.');
238
239
240
    }

    /**
241
242
243
244
     * Adds a JS action for the element with the provided id.
     *
     * This method adds a JS event for the provided component action to the page
     * and then returns the id that the event has been attached to.
245
     * If no id has been provided then a new ID is generated by {@link html_writer::random_id()}
246
     *
247
     * @param component_action $action
248
249
     * @param string $id
     * @return string id of element, either original submitted or random new if not supplied
250
     */
251
    public function add_action_handler(component_action $action, $id = null) {
252
253
254
        if (!$id) {
            $id = html_writer::random_id($action->event);
        }
255
        $this->page->requires->event_handler("#$id", $action->event, $action->jsfunction, $action->jsfunctionargs);
256
        return $id;
257
258
259
    }

    /**
260
261
     * Returns true is output has already started, and false if not.
     *
262
     * @return boolean true if the header has been printed.
263
     */
264
265
    public function has_started() {
        return $this->page->state >= moodle_page::STATE_IN_BODY;
266
267
268
269
    }

    /**
     * Given an array or space-separated list of classes, prepares and returns the HTML class attribute value
270
     *
271
272
273
274
275
276
277
278
279
280
     * @param mixed $classes Space-separated string or array of classes
     * @return string HTML class attribute value
     */
    public static function prepare_classes($classes) {
        if (is_array($classes)) {
            return implode(' ', array_unique($classes));
        }
        return $classes;
    }

281
282
283
284
285
286
287
288
289
290
    /**
     * Return the direct URL for an image from the pix folder.
     *
     * Use this function sparingly and never for icons. For icons use pix_icon or the pix helper in a mustache template.
     *
     * @deprecated since Moodle 3.3
     * @param string $imagename the name of the icon.
     * @param string $component specification of one plugin like in get_string()
     * @return moodle_url
     */
291
    public function pix_url($imagename, $component = 'moodle') {
292
        debugging('pix_url is deprecated. Use image_url for images and pix_icon for icons.', DEBUG_DEVELOPER);
293
294
295
        return $this->page->theme->image_url($imagename, $component);
    }

296
    /**
Petr Skoda's avatar
Petr Skoda committed
297
     * Return the moodle_url for an image.
298
     *
Petr Skoda's avatar
Petr Skoda committed
299
300
301
302
303
304
305
     * The exact image location and extension is determined
     * automatically by searching for gif|png|jpg|jpeg, please
     * note there can not be diferent images with the different
     * extension. The imagename is for historical reasons
     * a relative path name, it may be changed later for core
     * images. It is recommended to not use subdirectories
     * in plugin and theme pix directories.
306
     *
Petr Skoda's avatar
Petr Skoda committed
307
308
309
310
311
312
313
     * There are three types of images:
     * 1/ theme images  - stored in theme/mytheme/pix/,
     *                    use component 'theme'
     * 2/ core images   - stored in /pix/,
     *                    overridden via theme/mytheme/pix_core/
     * 3/ plugin images - stored in mod/mymodule/pix,
     *                    overridden via theme/mytheme/pix_plugins/mod/mymodule/,
314
     *                    example: image_url('comment', 'mod_glossary')
Petr Skoda's avatar
Petr Skoda committed
315
316
317
     *
     * @param string $imagename the pathname of the image
     * @param string $component full plugin name (aka component) or 'theme'
318
     * @return moodle_url
319
     */
320
321
    public function image_url($imagename, $component = 'moodle') {
        return $this->page->theme->image_url($imagename, $component);
322
    }
323
324
325
326
327
328
329
330

    /**
     * Return the site's logo URL, if any.
     *
     * @param int $maxwidth The maximum width, or null when the maximum width does not matter.
     * @param int $maxheight The maximum height, or null when the maximum height does not matter.
     * @return moodle_url|false
     */
331
    public function get_logo_url($maxwidth = null, $maxheight = 200) {
332
333
334
335
336
337
        global $CFG;
        $logo = get_config('core_admin', 'logo');
        if (empty($logo)) {
            return false;
        }

338
339
340
        // 200px high is the default image size which should be displayed at 100px in the page to account for retina displays.
        // It's not worth the overhead of detecting and serving 2 different images based on the device.

341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
        // Hide the requested size in the file path.
        $filepath = ((int) $maxwidth . 'x' . (int) $maxheight) . '/';

        // Use $CFG->themerev to prevent browser caching when the file changes.
        return moodle_url::make_pluginfile_url(context_system::instance()->id, 'core_admin', 'logo', $filepath,
            theme_get_revision(), $logo);
    }

    /**
     * Return the site's compact logo URL, if any.
     *
     * @param int $maxwidth The maximum width, or null when the maximum width does not matter.
     * @param int $maxheight The maximum height, or null when the maximum height does not matter.
     * @return moodle_url|false
     */
    public function get_compact_logo_url($maxwidth = 100, $maxheight = 100) {
        global $CFG;
        $logo = get_config('core_admin', 'logocompact');
        if (empty($logo)) {
            return false;
        }

        // Hide the requested size in the file path.
        $filepath = ((int) $maxwidth . 'x' . (int) $maxheight) . '/';

        // Use $CFG->themerev to prevent browser caching when the file changes.
        return moodle_url::make_pluginfile_url(context_system::instance()->id, 'core_admin', 'logocompact', $filepath,
            theme_get_revision(), $logo);
    }

371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
    /**
     * Whether we should display the logo in the navbar.
     *
     * We will when there are no main logos, and we have compact logo.
     *
     * @return bool
     */
    public function should_display_navbar_logo() {
        $logo = $this->get_compact_logo_url();
        return !empty($logo) && !$this->should_display_main_logo();
    }

    /**
     * Whether we should display the main logo.
     *
     * @param int $headinglevel
     * @return bool
     */
    public function should_display_main_logo($headinglevel = 1) {
        global $PAGE;

        // Only render the logo if we're on the front page or login page and the we have a logo.
        $logo = $this->get_logo_url();
        if ($headinglevel == 1 && !empty($logo)) {
            if ($PAGE->pagelayout == 'frontpage' || $PAGE->pagelayout == 'login') {
                return true;
            }
        }

        return false;
    }

403
404
}

405

406
407
408
/**
 * Basis for all plugin renderers.
 *
409
410
411
412
 * @copyright Petr Skoda (skodak)
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 * @since Moodle 2.0
 * @package core
413
 * @category output
414
415
 */
class plugin_renderer_base extends renderer_base {
416

417
    /**
418
419
     * @var renderer_base|core_renderer A reference to the current renderer.
     * The renderer provided here will be determined by the page but will in 90%
420
     * of cases by the {@link core_renderer}
421
422
423
424
     */
    protected $output;

    /**
Petr Skoda's avatar
Petr Skoda committed
425
     * Constructor method, calls the parent constructor
426
     *
427
     * @param moodle_page $page
428
     * @param string $target one of rendering target constants
429
     */
430
    public function __construct(moodle_page $page, $target) {
431
432
433
434
435
436
        if (empty($target) && $page->pagelayout === 'maintenance') {
            // If the page is using the maintenance layout then we're going to force the target to maintenance.
            // This way we'll get a special maintenance renderer that is designed to block access to API's that are likely
            // unavailable for this page layout.
            $target = RENDERER_TARGET_MAINTENANCE;
        }
437
438
        $this->output = $page->get_renderer('core', null, $target);
        parent::__construct($page, $target);
439
    }
440

441
    /**
442
443
     * Renders the provided widget and returns the HTML to display it.
     *
Petr Skoda's avatar
Petr Skoda committed
444
     * @param renderable $widget instance with renderable interface
445
446
447
     * @return string
     */
    public function render(renderable $widget) {
448
449
450
        $classname = get_class($widget);
        // Strip namespaces.
        $classname = preg_replace('/^.*\\\/', '', $classname);
451
452
        // Keep a copy at this point, we may need to look for a deprecated method.
        $deprecatedmethod = 'render_'.$classname;
453
        // Remove _renderable suffixes
454
        $classname = preg_replace('/_renderable$/', '', $classname);
455
456

        $rendermethod = 'render_'.$classname;
457
458
459
        if (method_exists($this, $rendermethod)) {
            return $this->$rendermethod($widget);
        }
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
        if ($rendermethod !== $deprecatedmethod && method_exists($this, $deprecatedmethod)) {
            // This is exactly where we don't want to be.
            // If you have arrived here you have a renderable component within your plugin that has the name
            // blah_renderable, and you have a render method render_blah_renderable on your plugin.
            // In 2.8 we revamped output, as part of this change we changed slightly how renderables got rendered
            // and the _renderable suffix now gets removed when looking for a render method.
            // You need to change your renderers render_blah_renderable to render_blah.
            // Until you do this it will not be possible for a theme to override the renderer to override your method.
            // Please do it ASAP.
            static $debugged = array();
            if (!isset($debugged[$deprecatedmethod])) {
                debugging(sprintf('Deprecated call. Please rename your renderables render method from %s to %s.',
                    $deprecatedmethod, $rendermethod), DEBUG_DEVELOPER);
                $debugged[$deprecatedmethod] = true;
            }
            return $this->$deprecatedmethod($widget);
        }
477
        // pass to core renderer if method not found here
478
        return $this->output->render($widget);
479
480
    }

481
482
    /**
     * Magic method used to pass calls otherwise meant for the standard renderer
Petr Skoda's avatar
Petr Skoda committed
483
     * to it to ensure we don't go causing unnecessary grief.
484
485
486
487
488
489
     *
     * @param string $method
     * @param array $arguments
     * @return mixed
     */
    public function __call($method, $arguments) {
490
        if (method_exists('renderer_base', $method)) {
491
            throw new coding_exception('Protected method called against '.get_class($this).' :: '.$method);
492
        }
493
494
495
        if (method_exists($this->output, $method)) {
            return call_user_func_array(array($this->output, $method), $arguments);
        } else {
496
            throw new coding_exception('Unknown method called against '.get_class($this).' :: '.$method);
497
498
        }
    }
499
}
500

501

502
/**
503
 * The standard implementation of the core_renderer interface.
504
505
 *
 * @copyright 2009 Tim Hunt
506
507
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 * @since Moodle 2.0
508
 * @package core
509
 * @category output
510
 */
511
class core_renderer extends renderer_base {
512
513
514
515
    /**
     * Do NOT use, please use <?php echo $OUTPUT->main_content() ?>
     * in layout files instead.
     * @deprecated
516
     * @var string used in {@link core_renderer::header()}.
517
     */
518
    const MAIN_CONTENT_TOKEN = '[MAIN CONTENT GOES HERE]';
519
520

    /**
521
522
     * @var string Used to pass information from {@link core_renderer::doctype()} to
     * {@link core_renderer::standard_head_html()}.
523
     */
524
    protected $contenttype;
525
526

    /**
527
528
     * @var string Used by {@link core_renderer::redirect_message()} method to communicate
     * with {@link core_renderer::header()}.
529
     */
530
    protected $metarefreshtag = '';
531
532

    /**
533
     * @var string Unique token for the closing HTML
534
     */
535
    protected $unique_end_html_token;
536
537

    /**
538
     * @var string Unique token for performance information
539
     */
540
    protected $unique_performance_info_token;
541
542

    /**
543
     * @var string Unique token for the main content.
544
     */
545
546
    protected $unique_main_content_token;

547
548
549
    /** @var custom_menu_item language The language menu if created */
    protected $language = null;

550
551
    /**
     * Constructor
552
     *
553
554
555
556
557
558
559
560
561
562
563
564
     * @param moodle_page $page the page we are doing output for.
     * @param string $target one of rendering target constants
     */
    public function __construct(moodle_page $page, $target) {
        $this->opencontainers = $page->opencontainers;
        $this->page = $page;
        $this->target = $target;

        $this->unique_end_html_token = '%%ENDHTML-'.sesskey().'%%';
        $this->unique_performance_info_token = '%%PERFORMANCEINFO-'.sesskey().'%%';
        $this->unique_main_content_token = '[MAIN CONTENT GOES HERE - '.sesskey().']';
    }
565
566
567
568

    /**
     * Get the DOCTYPE declaration that should be used with this page. Designed to
     * be called in theme layout.php files.
569
     *
570
     * @return string the DOCTYPE declaration that should be used.
571
572
     */
    public function doctype() {
573
574
575
        if ($this->page->theme->doctype === 'html5') {
            $this->contenttype = 'text/html; charset=utf-8';
            return "<!DOCTYPE html>\n";
576

577
        } else if ($this->page->theme->doctype === 'xhtml5') {
578
            $this->contenttype = 'application/xhtml+xml; charset=utf-8';
579
            return "<!DOCTYPE html>\n";
580
581

        } else {
582
583
584
            // legacy xhtml 1.0
            $this->contenttype = 'text/html; charset=utf-8';
            return ('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' . "\n");
585
586
587
588
589
590
        }
    }

    /**
     * The attributes that should be added to the <html> tag. Designed to
     * be called in theme layout.php files.
591
     *
592
593
594
     * @return string HTML fragment.
     */
    public function htmlattributes() {
595
        $return = get_html_lang(true);
596
        $attributes = array();
597
        if ($this->page->theme->doctype !== 'html5') {
598
            $attributes['xmlns'] = 'http://www.w3.org/1999/xhtml';
599
        }
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619

        // Give plugins an opportunity to add things like xml namespaces to the html element.
        // This function should return an array of html attribute names => values.
        $pluginswithfunction = get_plugins_with_function('add_htmlattributes', 'lib.php');
        foreach ($pluginswithfunction as $plugins) {
            foreach ($plugins as $function) {
                $newattrs = $function();
                unset($newattrs['dir']);
                unset($newattrs['lang']);
                unset($newattrs['xmlns']);
                unset($newattrs['xml:lang']);
                $attributes += $newattrs;
            }
        }

        foreach ($attributes as $key => $val) {
            $val = s($val);
            $return .= " $key=\"$val\"";
        }

620
        return $return;
621
622
623
624
625
626
    }

    /**
     * The standard tags (meta tags, links to stylesheets and JavaScript, etc.)
     * that should be included in the <head> tag. Designed to be called in theme
     * layout.php files.
627
     *
628
629
630
     * @return string HTML fragment.
     */
    public function standard_head_html() {
631
        global $CFG, $SESSION, $SITE, $PAGE;
632
633
634
635
636
637
638
639
640
641

        // Before we output any content, we need to ensure that certain
        // page components are set up.

        // Blocks must be set up early as they may require javascript which
        // has to be included in the page header before output is created.
        foreach ($this->page->blocks->get_regions() as $region) {
            $this->page->blocks->ensure_content_created($region, $this);
        }

642
        $output = '';
643

644
645
646
647
648
649
650
651
652
        // Give plugins an opportunity to add any head elements. The callback
        // must always return a string containing valid html head content.
        $pluginswithfunction = get_plugins_with_function('before_standard_html_head', 'lib.php');
        foreach ($pluginswithfunction as $plugins) {
            foreach ($plugins as $function) {
                $output .= $function();
            }
        }

653
654
655
656
657
658
        // Allow a url_rewrite plugin to setup any dynamic head content.
        if (isset($CFG->urlrewriteclass) && !isset($CFG->upgraderunning)) {
            $class = $CFG->urlrewriteclass;
            $output .= $class::html_head_setup();
        }

659
660
        $output .= '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />' . "\n";
        $output .= '<meta name="keywords" content="moodle, ' . $this->page->title . '" />' . "\n";
661
        // This is only set by the {@link redirect()} method
662
663
664
665
666
667
668
669
        $output .= $this->metarefreshtag;

        // Check if a periodic refresh delay has been set and make sure we arn't
        // already meta refreshing
        if ($this->metarefreshtag=='' && $this->page->periodicrefreshdelay!==null) {
            $output .= '<meta http-equiv="refresh" content="'.$this->page->periodicrefreshdelay.';url='.$this->page->url->out().'" />';
        }

670
        // Set up help link popups for all links with the helptooltip class
671
672
        $this->page->requires->js_init_call('M.util.help_popups.setup');

673
674
675
676
677
        $focus = $this->page->focuscontrol;
        if (!empty($focus)) {
            if (preg_match("#forms\['([a-zA-Z0-9]+)'\].elements\['([a-zA-Z0-9]+)'\]#", $focus, $matches)) {
                // This is a horrifically bad way to handle focus but it is passed in
                // through messy formslib::moodleform
678
                $this->page->requires->js_function_call('old_onload_focus', array($matches[1], $matches[2]));
679
680
681
682
683
684
            } else if (strpos($focus, '.')!==false) {
                // Old style of focus, bad way to do it
                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);
                $this->page->requires->js_function_call('old_onload_focus', explode('.', $focus, 2));
            } else {
                // Focus element with given id
685
                $this->page->requires->js_function_call('focuscontrol', array($focus));
686
687
688
            }
        }

689
690
        // Get the theme stylesheet - this has to be always first CSS, this loads also styles.css from all plugins;
        // any other custom CSS can not be overridden via themes and is highly discouraged
691
        $urls = $this->page->theme->css_urls($this->page);
692
        foreach ($urls as $url) {
693
            $this->page->requires->css_theme($url);
694
695
        }

696
        // Get the theme javascript head and footer
697
698
699
700
701
702
        if ($jsurl = $this->page->theme->javascript_url(true)) {
            $this->page->requires->js($jsurl, true);
        }
        if ($jsurl = $this->page->theme->javascript_url(false)) {
            $this->page->requires->js($jsurl);
        }
703

704
        // Get any HTML from the page_requirements_manager.
705
        $output .= $this->page->requires->get_head_code($this->page, $this);
706
707
708

        // List alternate versions.
        foreach ($this->page->alternateversions as $type => $alt) {
709
            $output .= html_writer::empty_tag('link', array('rel' => 'alternate',
710
711
                    'type' => $type, 'title' => $alt->title, 'href' => $alt->url));
        }
Petr Skoda's avatar
Petr Skoda committed
712

713
714
715
716
717
718
719
720
721
722
        // Add noindex tag if relevant page and setting applied.
        $allowindexing = isset($CFG->allowindexing) ? $CFG->allowindexing : 0;
        $loginpages = array('login-index', 'login-signup');
        if ($allowindexing == 2 || ($allowindexing == 0 && in_array($this->page->pagetype, $loginpages))) {
            if (!isset($CFG->additionalhtmlhead)) {
                $CFG->additionalhtmlhead = '';
            }
            $CFG->additionalhtmlhead .= '<meta name="robots" content="noindex" />';
        }

723
724
725
        if (!empty($CFG->additionalhtmlhead)) {
            $output .= "\n".$CFG->additionalhtmlhead;
        }
726

727
728
729
730
731
732
733
        if ($PAGE->pagelayout == 'frontpage') {
            $summary = s(strip_tags(format_text($SITE->summary, FORMAT_HTML)));
            if (!empty($summary)) {
                $output .= "<meta name=\"description\" content=\"$summary\" />\n";
            }
        }

734
735
736
737
738
739
        return $output;
    }

    /**
     * The standard tags (typically skip links) that should be output just inside
     * the start of the <body> tag. Designed to be called in theme layout.php files.
740
     *
741
742
743
     * @return string HTML fragment.
     */
    public function standard_top_of_body_html() {
744
        global $CFG;
745
        $output = $this->page->requires->get_top_of_body_code($this);
746
        if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmltopofbody)) {
747
748
            $output .= "\n".$CFG->additionalhtmltopofbody;
        }
749

750
751
752
753
754
755
756
757
        // Give subsystems an opportunity to inject extra html content. The callback
        // must always return a string containing valid html.
        foreach (\core_component::get_core_subsystems() as $name => $path) {
            if ($path) {
                $output .= component_callback($name, 'before_standard_top_of_body_html', [], '');
            }
        }

758
759
760
761
762
763
764
765
766
        // Give plugins an opportunity to inject extra html content. The callback
        // must always return a string containing valid html.
        $pluginswithfunction = get_plugins_with_function('before_standard_top_of_body_html', 'lib.php');
        foreach ($pluginswithfunction as $plugins) {
            foreach ($plugins as $function) {
                $output .= $function();
            }
        }

767
        $output .= $this->maintenance_warning();
768

769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
        return $output;
    }

    /**
     * Scheduled maintenance warning message.
     *
     * Note: This is a nasty hack to display maintenance notice, this should be moved
     *       to some general notification area once we have it.
     *
     * @return string
     */
    public function maintenance_warning() {
        global $CFG;

        $output = '';
        if (isset($CFG->maintenance_later) and $CFG->maintenance_later > time()) {
785
786
            $timeleft = $CFG->maintenance_later - time();
            // If timeleft less than 30 sec, set the class on block to error to highlight.
787
788
            $errorclass = ($timeleft < 30) ? 'alert-error alert-danger' : 'alert-warning';
            $output .= $this->box_start($errorclass . ' moodle-has-zindex maintenancewarning m-a-1 alert');
789
            $a = new stdClass();
790
791
            $a->hour = (int)($timeleft / 3600);
            $a->min = (int)(($timeleft / 60) % 60);
792
            $a->sec = (int)($timeleft % 60);
793
794
795
796
797
798
            if ($a->hour > 0) {
                $output .= get_string('maintenancemodeisscheduledlong', 'admin', $a);
            } else {
                $output .= get_string('maintenancemodeisscheduled', 'admin', $a);
            }

799
            $output .= $this->box_end();
800
801
802
            $this->page->requires->yui_module('moodle-core-maintenancemodetimer', 'M.core.maintenancemodetimer',
                    array(array('timeleftinsec' => $timeleft)));
            $this->page->requires->strings_for_js(
803
                    array('maintenancemodeisscheduled', 'maintenancemodeisscheduledlong', 'sitemaintenance'),
804
                    'admin');
805
        }
806
        return $output;
807
808
809
810
811
812
    }

    /**
     * The standard tags (typically performance information and validation links,
     * if we are in developer debug mode) that should be output in the footer area
     * of the page. Designed to be called in theme layout.php files.
813
     *
814
815
816
     * @return string HTML fragment.
     */
    public function standard_footer_html() {
817
        global $CFG, $SCRIPT;
818

819
        $output = '';
820
821
822
        if (during_initial_install()) {
            // Debugging info can not work before install is finished,
            // in any case we do not want any links during installation!
823
824
825
826
827
828
829
830
831
832
            return $output;
        }

        // Give plugins an opportunity to add any footer elements.
        // The callback must always return a string containing valid html footer content.
        $pluginswithfunction = get_plugins_with_function('standard_footer_html', 'lib.php');
        foreach ($pluginswithfunction as $plugins) {
            foreach ($plugins as $function) {
                $output .= $function();
            }
833
834
        }

835
        // This function is normally called from a layout.php file in {@link core_renderer::header()}
836
        // but some of the content won't be known until later, so we return a placeholder
837
        // for now. This will be replaced with the real content in {@link core_renderer::footer()}.
838
        $output .= $this->unique_performance_info_token;
839
        if ($this->page->devicetypeinuse == 'legacy') {
840
841
842
            // The legacy theme is in use print the notification
            $output .= html_writer::tag('div', get_string('legacythemeinuse'), array('class'=>'legacythemeinuse'));
        }
843

844
        // Get links to switch device types (only shown for users not on a default device)
845
846
        $output .= $this->theme_switch_links();

847
        if (!empty($CFG->debugpageinfo)) {
848
849
            $output .= '<div class="performanceinfo pageinfo">' . get_string('pageinfodebugsummary', 'core_admin',
                $this->page->debug_summary()) . '</div>';
850
        }
851
        if (debugging(null, DEBUG_DEVELOPER) and has_capability('moodle/site:config', context_system::instance())) {  // Only in developer mode
852
853
854
855
            // Add link to profiling report if necessary
            if (function_exists('profiling_is_running') && profiling_is_running()) {
                $txt = get_string('profiledscript', 'admin');
                $title = get_string('profiledscriptview', 'admin');
856
                $url = $CFG->wwwroot . '/admin/tool/profiling/index.php?script=' . urlencode($SCRIPT);
857
858
859
                $link= '<a title="' . $title . '" href="' . $url . '">' . $txt . '</a>';
                $output .= '<div class="profilingfooter">' . $link . '</div>';
            }
Tim Hunt's avatar
Tim Hunt committed
860
861
862
863
            $purgeurl = new moodle_url('/admin/purgecaches.php', array('confirm' => 1,
                'sesskey' => sesskey(), 'returnurl' => $this->page->url->out_as_local_url(false)));
            $output .= '<div class="purgecaches">' .
                    html_writer::link($purgeurl, get_string('purgecaches', 'admin')) . '</div>';
864
        }
865
        if (!empty($CFG->debugvalidators)) {
866
            // NOTE: this is not a nice hack, $PAGE->url is not always accurate and $FULLME neither, it is not a bug if it fails. --skodak
867
            $output .= '<div class="validators"><ul class="list-unstyled ml-1">
868
869
870
871
872
873
874
875
              <li><a href="http://validator.w3.org/check?verbose=1&amp;ss=1&amp;uri=' . urlencode(qualified_me()) . '">Validate HTML</a></li>
              <li><a href="http://www.contentquality.com/mynewtester/cynthia.exe?rptmode=-1&amp;url1=' . urlencode(qualified_me()) . '">Section 508 Check</a></li>
              <li><a href="http://www.contentquality.com/mynewtester/cynthia.exe?rptmode=0&amp;warnp2n3e=1&amp;url1=' . urlencode(qualified_me()) . '">WCAG 1 (2,3) Check</a></li>
            </ul></div>';
        }
        return $output;
    }

876
877
878
    /**
     * Returns standard main content placeholder.
     * Designed to be called in theme layout.php files.
879
     *
880
881
882
     * @return string HTML fragment.
     */
    public function main_content() {
883
884
885
886
887
888
889
        // This is here because it is the only place we can inject the "main" role over the entire main content area
        // without requiring all theme's to manually do it, and without creating yet another thing people need to
        // remember in the theme.
        // This is an unfortunate hack. DO NO EVER add anything more here.
        // DO NOT add classes.
        // DO NOT add an id.
        return '<div role="main">'.$this->unique_main_content_token.'</div>';
890
891
    }

892
893
894
895
896
897
898
899
    /**
     * Returns standard navigation between activities in a course.
     *
     * @return string the navigation HTML.
     */
    public function activity_navigation() {
        // First we should check if we want to add navigation.
        $context = $this->page->context;
900
901
        if (($this->page->pagelayout !== 'incourse' && $this->page->pagelayout !== 'frametop')
            || $context->contextlevel != CONTEXT_MODULE) {
902
903
904
905
906
907
908
909
910
911
912
913
914
915
            return '';
        }

        // If the activity is in stealth mode, show no links.
        if ($this->page->cm->is_stealth()) {
            return '';
        }

        // Get a list of all the activities in the course.
        $course = $this->page->cm->get_course();
        $modules = get_fast_modinfo($course->id)->get_cms();

        // Put the modules into an array in order by the position they are shown in the course.
        $mods = [];
916
        $activitylist = [];
917
918
919
920
921
922
        foreach ($modules as $module) {
            // Only add activities the user can access, aren't in stealth mode and have a url (eg. mod_label does not).
            if (!$module->uservisible || $module->is_stealth() || empty($module->url)) {
                continue;
            }
            $mods[$module->id] = $module;
923

924
925
926
927
            // No need to add the current module to the list for the activity dropdown menu.
            if ($module->id == $this->page->cm->id) {
                continue;
            }
928
            // Module name.
929
            $modname = $module->get_formatted_name();
930
931
932
933
934
935
936
937
            // Display the hidden text if necessary.
            if (!$module->visible) {
                $modname .= ' ' . get_string('hiddenwithbrackets');
            }
            // Module URL.
            $linkurl = new moodle_url($module->url, array('forceview' => 1));
            // Add module URL (as key) and name (as value) to the activity list array.
            $activitylist[$linkurl->out(false)] = $modname;
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
        }

        $nummods = count($mods);

        // If there is only one mod then do nothing.
        if ($nummods == 1) {
            return '';
        }

        // Get an array of just the course module ids used to get the cmid value based on their position in the course.
        $modids = array_keys($mods);

        // Get the position in the array of the course module we are viewing.
        $position = array_search($this->page->cm->id, $modids);

        $prevmod = null;
        $nextmod = null;

        // Check if we have a previous mod to show.
        if ($position > 0) {
            $prevmod = $mods[$modids[$position - 1]];
        }

        // Check if we have a next mod to show.
        if ($position < ($nummods - 1)) {
            $nextmod = $mods[$modids[$position + 1]];
        }

966
        $activitynav = new \core_course\output\activity_navigation($prevmod, $nextmod, $activitylist);
967
968
969
970
        $renderer = $this->page->get_renderer('core', 'course');
        return $renderer->render($activitynav);
    }

971
972
    /**
     * The standard tags (typically script tags that are not needed earlier) that
973
     * should be output after everything else. Designed to be called in theme layout.php files.
974
     *
975
976
977
     * @return string HTML fragment.
     */
    public function standard_end_of_body_html() {
978
979
        global $CFG;

980
        // This function is normally called from a layout.php file in {@link core_renderer::header()}
981
        // but some of the content won't be known until later, so we return a placeholder
982
        // for now. This will be replaced with the real content in {@link core_renderer::footer()}.
983
        $output = '';
984
        if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmlfooter)) {
985
986
987
988
            $output .= "\n".$CFG->additionalhtmlfooter;
        }
        $output .= $this->unique_end_html_token;
        return $output;
989
990
    }

991
992
993
994
995
996
997
998
999
1000
    /**
     * The standard HTML that should be output just before the <footer> tag.
     * Designed to be called in theme layout.php files.
     *
     * @return string HTML fragment.
     */
    public function standard_after_main_region_html() {
        global $CFG;
        $output = '';
        if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmlbottomofbody)) {