outputrenderers.php 196 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
84
85
86
    /**
     * @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) {
            $themename = $this->page->theme->name;
            $themerev = theme_get_revision();

87
88
89
            $cachedir = make_localcache_directory("mustache/$themerev/$themename");

            $loader = new \core\output\mustache_filesystem_loader();
90
            $stringhelper = new \core\output\mustache_string_helper();
91
            $quotehelper = new \core\output\mustache_quote_helper();
92
            $jshelper = new \core\output\mustache_javascript_helper($this->page);
93
            $pixhelper = new \core\output\mustache_pix_helper($this);
94
            $shortentexthelper = new \core\output\mustache_shorten_text_helper();
95
            $userdatehelper = new \core\output\mustache_user_date_helper();
96
97
98
99
100
101

            // 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'),
102
                             'quote' => array($quotehelper, 'quote'),
103
                             'js' => array($jshelper, 'help'),
104
                             'pix' => array($pixhelper, 'pix'),
105
106
107
                             'shortentext' => array($shortentexthelper, 'shorten'),
                             'userdate' => array($userdatehelper, 'transform'),
                         );
108
109
110
111
112

            $this->mustache = new Mustache_Engine(array(
                'cache' => $cachedir,
                'escape' => 's',
                'loader' => $loader,
113
114
                'helpers' => $helpers,
                'pragmas' => [Mustache_Engine::PRAGMA_BLOCKS]));
115
116
117
118
119
120
121

        }

        return $this->mustache;
    }


122
123
    /**
     * Constructor
124
     *
125
     * The constructor takes two arguments. The first is the page that the renderer
126
127
128
129
     * 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.
     *
130
     * @param moodle_page $page the page we are doing output for.
131
     * @param string $target one of rendering target constants
132
     */
133
    public function __construct(moodle_page $page, $target) {
134
135
        $this->opencontainers = $page->opencontainers;
        $this->page = $page;
136
        $this->target = $target;
137
138
    }

139
140
141
142
143
144
145
146
147
148
149
150
151
152
    /**
     * 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();

153
154
        try {
            // Grab a copy of the existing helper to be restored later.
155
            $uniqidhelper = $mustache->getHelper('uniqid');
156
157
        } catch (Mustache_Exception_UnknownHelperException $e) {
            // Helper doesn't exist.
158
            $uniqidhelper = null;
159
160
        }

161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
        // 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);
            }
        }
176

177
        $renderedtemplate = trim($template->render($context));
178
179
180

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

185
        return $renderedtemplate;
186
187
188
    }


189
    /**
190
     * Returns rendered widget.
191
192
193
     *
     * The provided widget needs to be an object that extends the renderable
     * interface.
194
     * If will then be rendered by a method based upon the classname for the widget.
195
196
197
     * For instance a widget of class `crazywidget` will be rendered by a protected
     * render_crazywidget method of this renderer.
     *
Petr Skoda's avatar
Petr Skoda committed
198
     * @param renderable $widget instance with renderable interface
199
     * @return string
200
     */
201
    public function render(renderable $widget) {
202
203
204
205
206
207
208
        $classname = get_class($widget);
        // Strip namespaces.
        $classname = preg_replace('/^.*\\\/', '', $classname);
        // Remove _renderable suffixes
        $classname = preg_replace('/_renderable$/', '', $classname);

        $rendermethod = 'render_'.$classname;
209
210
211
212
        if (method_exists($this, $rendermethod)) {
            return $this->$rendermethod($widget);
        }
        throw new coding_exception('Can not render widget, renderer method ('.$rendermethod.') not found.');
213
214
215
    }

    /**
216
217
218
219
     * 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.
220
     * If no id has been provided then a new ID is generated by {@link html_writer::random_id()}
221
     *
222
     * @param component_action $action
223
224
     * @param string $id
     * @return string id of element, either original submitted or random new if not supplied
225
     */
226
    public function add_action_handler(component_action $action, $id = null) {
227
228
229
        if (!$id) {
            $id = html_writer::random_id($action->event);
        }
230
        $this->page->requires->event_handler("#$id", $action->event, $action->jsfunction, $action->jsfunctionargs);
231
        return $id;
232
233
234
    }

    /**
235
236
     * Returns true is output has already started, and false if not.
     *
237
     * @return boolean true if the header has been printed.
238
     */
239
240
    public function has_started() {
        return $this->page->state >= moodle_page::STATE_IN_BODY;
241
242
243
244
    }

    /**
     * Given an array or space-separated list of classes, prepares and returns the HTML class attribute value
245
     *
246
247
248
249
250
251
252
253
254
255
     * @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;
    }

256
257
258
259
260
261
262
263
264
265
    /**
     * 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
     */
266
    public function pix_url($imagename, $component = 'moodle') {
267
        debugging('pix_url is deprecated. Use image_url for images and pix_icon for icons.', DEBUG_DEVELOPER);
268
269
270
        return $this->page->theme->image_url($imagename, $component);
    }

271
    /**
Petr Skoda's avatar
Petr Skoda committed
272
     * Return the moodle_url for an image.
273
     *
Petr Skoda's avatar
Petr Skoda committed
274
275
276
277
278
279
280
     * 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.
281
     *
Petr Skoda's avatar
Petr Skoda committed
282
283
284
285
286
287
288
     * 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/,
289
     *                    example: image_url('comment', 'mod_glossary')
Petr Skoda's avatar
Petr Skoda committed
290
291
292
     *
     * @param string $imagename the pathname of the image
     * @param string $component full plugin name (aka component) or 'theme'
293
     * @return moodle_url
294
     */
295
296
    public function image_url($imagename, $component = 'moodle') {
        return $this->page->theme->image_url($imagename, $component);
297
    }
298
299
300
301
302
303
304
305

    /**
     * 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
     */
306
    public function get_logo_url($maxwidth = null, $maxheight = 200) {
307
308
309
310
311
312
        global $CFG;
        $logo = get_config('core_admin', 'logo');
        if (empty($logo)) {
            return false;
        }

313
314
315
        // 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.

316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
        // 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);
    }

346
347
}

348

349
350
351
/**
 * Basis for all plugin renderers.
 *
352
353
354
355
 * @copyright Petr Skoda (skodak)
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 * @since Moodle 2.0
 * @package core
356
 * @category output
357
358
 */
class plugin_renderer_base extends renderer_base {
359

360
    /**
361
362
     * @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%
363
     * of cases by the {@link core_renderer}
364
365
366
367
     */
    protected $output;

    /**
Petr Skoda's avatar
Petr Skoda committed
368
     * Constructor method, calls the parent constructor
369
     *
370
     * @param moodle_page $page
371
     * @param string $target one of rendering target constants
372
     */
373
    public function __construct(moodle_page $page, $target) {
374
375
376
377
378
379
        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;
        }
380
381
        $this->output = $page->get_renderer('core', null, $target);
        parent::__construct($page, $target);
382
    }
383

384
    /**
385
386
     * Renders the provided widget and returns the HTML to display it.
     *
Petr Skoda's avatar
Petr Skoda committed
387
     * @param renderable $widget instance with renderable interface
388
389
390
     * @return string
     */
    public function render(renderable $widget) {
391
392
393
        $classname = get_class($widget);
        // Strip namespaces.
        $classname = preg_replace('/^.*\\\/', '', $classname);
394
395
        // Keep a copy at this point, we may need to look for a deprecated method.
        $deprecatedmethod = 'render_'.$classname;
396
        // Remove _renderable suffixes
397
        $classname = preg_replace('/_renderable$/', '', $classname);
398
399

        $rendermethod = 'render_'.$classname;
400
401
402
        if (method_exists($this, $rendermethod)) {
            return $this->$rendermethod($widget);
        }
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
        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);
        }
420
        // pass to core renderer if method not found here
421
        return $this->output->render($widget);
422
423
    }

424
425
    /**
     * Magic method used to pass calls otherwise meant for the standard renderer
Petr Skoda's avatar
Petr Skoda committed
426
     * to it to ensure we don't go causing unnecessary grief.
427
428
429
430
431
432
     *
     * @param string $method
     * @param array $arguments
     * @return mixed
     */
    public function __call($method, $arguments) {
433
        if (method_exists('renderer_base', $method)) {
434
            throw new coding_exception('Protected method called against '.get_class($this).' :: '.$method);
435
        }
436
437
438
        if (method_exists($this->output, $method)) {
            return call_user_func_array(array($this->output, $method), $arguments);
        } else {
439
            throw new coding_exception('Unknown method called against '.get_class($this).' :: '.$method);
440
441
        }
    }
442
}
443

444

445
/**
446
 * The standard implementation of the core_renderer interface.
447
448
 *
 * @copyright 2009 Tim Hunt
449
450
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 * @since Moodle 2.0
451
 * @package core
452
 * @category output
453
 */
454
class core_renderer extends renderer_base {
455
456
457
458
    /**
     * Do NOT use, please use <?php echo $OUTPUT->main_content() ?>
     * in layout files instead.
     * @deprecated
459
     * @var string used in {@link core_renderer::header()}.
460
     */
461
    const MAIN_CONTENT_TOKEN = '[MAIN CONTENT GOES HERE]';
462
463

    /**
464
465
     * @var string Used to pass information from {@link core_renderer::doctype()} to
     * {@link core_renderer::standard_head_html()}.
466
     */
467
    protected $contenttype;
468
469

    /**
470
471
     * @var string Used by {@link core_renderer::redirect_message()} method to communicate
     * with {@link core_renderer::header()}.
472
     */
473
    protected $metarefreshtag = '';
474
475

    /**
476
     * @var string Unique token for the closing HTML
477
     */
478
    protected $unique_end_html_token;
479
480

    /**
481
     * @var string Unique token for performance information
482
     */
483
    protected $unique_performance_info_token;
484
485

    /**
486
     * @var string Unique token for the main content.
487
     */
488
489
490
491
    protected $unique_main_content_token;

    /**
     * Constructor
492
     *
493
494
495
496
497
498
499
500
501
502
503
504
     * @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().']';
    }
505
506
507
508

    /**
     * Get the DOCTYPE declaration that should be used with this page. Designed to
     * be called in theme layout.php files.
509
     *
510
     * @return string the DOCTYPE declaration that should be used.
511
512
     */
    public function doctype() {
513
514
515
        if ($this->page->theme->doctype === 'html5') {
            $this->contenttype = 'text/html; charset=utf-8';
            return "<!DOCTYPE html>\n";
516

517
        } else if ($this->page->theme->doctype === 'xhtml5') {
518
            $this->contenttype = 'application/xhtml+xml; charset=utf-8';
519
            return "<!DOCTYPE html>\n";
520
521

        } else {
522
523
524
            // 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");
525
526
527
528
529
530
        }
    }

    /**
     * The attributes that should be added to the <html> tag. Designed to
     * be called in theme layout.php files.
531
     *
532
533
534
     * @return string HTML fragment.
     */
    public function htmlattributes() {
535
        $return = get_html_lang(true);
536
        $attributes = array();
537
        if ($this->page->theme->doctype !== 'html5') {
538
            $attributes['xmlns'] = 'http://www.w3.org/1999/xhtml';
539
        }
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559

        // 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\"";
        }

560
        return $return;
561
562
563
564
565
566
    }

    /**
     * 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.
567
     *
568
569
570
     * @return string HTML fragment.
     */
    public function standard_head_html() {
571
        global $CFG, $SESSION;
572
573
574
575
576
577
578
579
580
581

        // 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);
        }

582
        $output = '';
583

584
585
586
587
588
589
590
591
592
        // 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();
            }
        }

593
594
595
596
597
598
        // 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();
        }

599
600
        $output .= '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />' . "\n";
        $output .= '<meta name="keywords" content="moodle, ' . $this->page->title . '" />' . "\n";
601
        // This is only set by the {@link redirect()} method
602
603
604
605
606
607
608
609
        $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().'" />';
        }

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

613
614
615
616
617
        $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
618
                $this->page->requires->js_function_call('old_onload_focus', array($matches[1], $matches[2]));
619
620
621
622
623
624
            } 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
625
                $this->page->requires->js_function_call('focuscontrol', array($focus));
626
627
628
            }
        }

629
630
        // 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
631
        $urls = $this->page->theme->css_urls($this->page);
632
        foreach ($urls as $url) {
633
            $this->page->requires->css_theme($url);
634
635
        }

636
        // Get the theme javascript head and footer
637
638
639
640
641
642
        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);
        }
643

644
        // Get any HTML from the page_requirements_manager.
645
        $output .= $this->page->requires->get_head_code($this->page, $this);
646
647
648

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

653
654
655
656
657
658
659
660
661
662
        // 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" />';
        }

663
664
665
        if (!empty($CFG->additionalhtmlhead)) {
            $output .= "\n".$CFG->additionalhtmlhead;
        }
666
667
668
669
670
671
672

        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.
673
     *
674
675
676
     * @return string HTML fragment.
     */
    public function standard_top_of_body_html() {
677
        global $CFG;
678
        $output = $this->page->requires->get_top_of_body_code($this);
679
        if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmltopofbody)) {
680
681
            $output .= "\n".$CFG->additionalhtmltopofbody;
        }
682
683
684
685
686
687
688
689
690
691

        // 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();
            }
        }

692
        $output .= $this->maintenance_warning();
693

694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
        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()) {
710
711
            $timeleft = $CFG->maintenance_later - time();
            // If timeleft less than 30 sec, set the class on block to error to highlight.
712
713
            $errorclass = ($timeleft < 30) ? 'alert-error alert-danger' : 'alert-warning';
            $output .= $this->box_start($errorclass . ' moodle-has-zindex maintenancewarning m-a-1 alert');
714
            $a = new stdClass();
715
716
            $a->hour = (int)($timeleft / 3600);
            $a->min = (int)(($timeleft / 60) % 60);
717
            $a->sec = (int)($timeleft % 60);
718
719
720
721
722
723
            if ($a->hour > 0) {
                $output .= get_string('maintenancemodeisscheduledlong', 'admin', $a);
            } else {
                $output .= get_string('maintenancemodeisscheduled', 'admin', $a);
            }

724
            $output .= $this->box_end();
725
726
727
            $this->page->requires->yui_module('moodle-core-maintenancemodetimer', 'M.core.maintenancemodetimer',
                    array(array('timeleftinsec' => $timeleft)));
            $this->page->requires->strings_for_js(
728
                    array('maintenancemodeisscheduled', 'maintenancemodeisscheduledlong', 'sitemaintenance'),
729
                    'admin');
730
        }
731
        return $output;
732
733
734
735
736
737
    }

    /**
     * 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.
738
     *
739
740
741
     * @return string HTML fragment.
     */
    public function standard_footer_html() {
742
        global $CFG, $SCRIPT;
743

744
        $output = '';
745
746
747
        if (during_initial_install()) {
            // Debugging info can not work before install is finished,
            // in any case we do not want any links during installation!
748
749
750
751
752
753
754
755
756
757
            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();
            }
758
759
        }

760
        // This function is normally called from a layout.php file in {@link core_renderer::header()}
761
        // but some of the content won't be known until later, so we return a placeholder
762
        // for now. This will be replaced with the real content in {@link core_renderer::footer()}.
763
        $output .= $this->unique_performance_info_token;
764
        if ($this->page->devicetypeinuse == 'legacy') {
765
766
767
            // The legacy theme is in use print the notification
            $output .= html_writer::tag('div', get_string('legacythemeinuse'), array('class'=>'legacythemeinuse'));
        }
768

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

772
        if (!empty($CFG->debugpageinfo)) {
773
            $output .= '<div class="performanceinfo pageinfo">This page is: ' . $this->page->debug_summary() . '</div>';
774
        }
775
        if (debugging(null, DEBUG_DEVELOPER) and has_capability('moodle/site:config', context_system::instance())) {  // Only in developer mode
776
777
778
779
            // 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');
780
                $url = $CFG->wwwroot . '/admin/tool/profiling/index.php?script=' . urlencode($SCRIPT);
781
782
783
                $link= '<a title="' . $title . '" href="' . $url . '">' . $txt . '</a>';
                $output .= '<div class="profilingfooter">' . $link . '</div>';
            }
Tim Hunt's avatar
Tim Hunt committed
784
785
786
787
            $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>';
788
        }
789
        if (!empty($CFG->debugvalidators)) {
790
            // 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
791
            $output .= '<div class="validators"><ul class="list-unstyled m-l-1">
792
793
794
795
796
797
798
799
              <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;
    }

800
801
802
    /**
     * Returns standard main content placeholder.
     * Designed to be called in theme layout.php files.
803
     *
804
805
806
     * @return string HTML fragment.
     */
    public function main_content() {
807
808
809
810
811
812
813
        // 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>';
814
815
    }

816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
    /**
     * 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;
        if ($this->page->pagelayout !== 'incourse' || $context->contextlevel != CONTEXT_MODULE) {
            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 = [];
        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;
        }

        $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]];
        }

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

878
879
    /**
     * The standard tags (typically script tags that are not needed earlier) that
880
     * should be output after everything else. Designed to be called in theme layout.php files.
881
     *
882
883
884
     * @return string HTML fragment.
     */
    public function standard_end_of_body_html() {
885
886
        global $CFG;

887
        // This function is normally called from a layout.php file in {@link core_renderer::header()}
888
        // but some of the content won't be known until later, so we return a placeholder
889
        // for now. This will be replaced with the real content in {@link core_renderer::footer()}.
890
        $output = '';
891
        if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmlfooter)) {
892
893
894
895
            $output .= "\n".$CFG->additionalhtmlfooter;
        }
        $output .= $this->unique_end_html_token;
        return $output;
896
897
898
899
900
    }

    /**
     * Return the standard string that says whether you are logged in (and switched
     * roles/logged in as another user).
901
902
903
     * @param bool $withlinks if false, then don't include any links in the HTML produced.
     * If not set, the default is the nologinlinks option from the theme config.php file,
     * and if that is not set, then links are included.
904
905
     * @return string HTML fragment.
     */
906
    public function login_info($withlinks = null) {
907
        global $USER, $CFG, $DB, $SESSION;
908

909
910
911
        if (during_initial_install()) {
            return '';
        }
912

913
914
915
916
        if (is_null($withlinks)) {
            $withlinks = empty($this->page->layout_options['nologinlinks']);
        }

917
        $course = $this->page->course;
918
919
        if (\core\session\manager::is_loggedinas()) {
            $realuser = \core\session\manager::get_realuser();
920
            $fullname = fullname($realuser, true);
921
            if ($withlinks) {
922
923
924
                $loginastitle = get_string('loginas');
                $realuserinfo = " [<a href=\"$CFG->wwwroot/course/loginas.php?id=$course->id&amp;sesskey=".sesskey()."\"";
                $realuserinfo .= "title =\"".$loginastitle."\">$fullname</a>] ";
925
926
927
            } else {
                $realuserinfo = " [$fullname] ";
            }
928
929
930
        } else {
            $realuserinfo = '';
        }
931

932
        $loginpage = $this->is_login_page();
933
        $loginurl = get_login_url();
934

935
936
937
        if (empty($course->id)) {
            // $course->id is not defined during installation
            return '';
938
        } else if (isloggedin()) {
939
            $context = context_course::instance($course->id);
940

941
            $fullname = fullname($USER, true);
942
            // Since Moodle 2.0 this link always goes to the public profile page (not the course profile page)
943
            if ($withlinks) {
944
945
                $linktitle = get_string('viewprofile');
                $username = "<a href=\"$CFG->wwwroot/user/profile.php?id=$USER->id\" title=\"$linktitle\">$fullname</a>";
946
947
948
            } else {
                $username = $fullname;
            }
949
            if (is_mnet_remote_user($USER) and $idprovider = $DB->get_record('mnet_host', array('id'=>$USER->mnethostid))) {
950
951
952
953
954
                if ($withlinks) {
                    $username .= " from <a href=\"{$idprovider->wwwroot}\">{$idprovider->name}</a>";
                } else {
                    $username .= " from {$idprovider->name}";
                }
955
            }
956
            if (isguestuser()) {
957
                $loggedinas = $realuserinfo.get_string('loggedinasguest');
958
                if (!$loginpage && $withlinks) {
959
960
                    $loggedinas .= " (<a href=\"$loginurl\">".get_string('login').'</a>)';
                }
961
            } else if (is_role_switched($course->id)) { // Has switched roles
962
963
                $rolename = '';
                if ($role = $DB->get_record('role', array('id'=>$USER->access['rsw'][$context->path]))) {
964
                    $rolename = ': '.role_get_name($role, $context);
965
                }
966
967
                $loggedinas = get_string('loggedinas', 'moodle', $username).$rolename;
                if ($withlinks) {
968
                    $url = new moodle_url('/course/switchrole.php', array('id'=>$course->id,'sesskey'=>sesskey(), 'switchrole'=>0, 'returnurl'=>$this->page->url->out_as_local_url(false)));
969
                    $loggedinas .= ' ('.html_writer::tag('a', get_string('switchrolereturn'), array('href' => $url)).')';
970
                }
971
            } else {
972
973
974
975
                $loggedinas = $realuserinfo.get_string('loggedinas', 'moodle', $username);
                if ($withlinks) {
                    $loggedinas .= " (<a href=\"$CFG->wwwroot/login/logout.php?sesskey=".sesskey()."\">".get_string('logout').'</a>)';
                }
976
977
            }
        } else {
978
            $loggedinas = get_string('loggedinnot', 'moodle');
979
            if (!$loginpage && $withlinks) {
980
981
                $loggedinas .= " (<a href=\"$loginurl\">".get_string('login').'</a>)';
            }
982
        }
983

984
        $loggedinas = '<div class="logininfo">'.$loggedinas.'</div>';
985

986
987
988
        if (isset($SESSION->justloggedin)) {
            unset($SESSION->justloggedin);
            if (!empty($CFG->displayloginfailures)) {
989
                if (!isguestuser()) {
990
991
992
                    // Include this file only when required.
                    require_once($CFG->dirroot . '/user/lib.php');
                    if ($count = user_count_login_failures($USER)) {
993
                        $loggedinas .= '<div class="loginfailures">';
994
995
996
                        $a = new stdClass();
                        $a->attempts = $count;
                        $loggedinas .= get_string('failedloginattempts', '', $a);
997
                        if (file_exists("$CFG->dirroot/report/log/index.php") and has_capability('report/log:view', context_system::instance())) {
998
999
                            $loggedinas .= ' ('.html_writer::link(new moodle_url('/report/log/index.php', array('chooselog' => 1,
                                    'id' => 0 , 'modid' => 'site_errors')), get_string('logs')).')';
1000
                        }
For faster browsing, not all history is shown. View entire blame