outputrenderers.php 164 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

    /**
     * Constructor
71
     *
72
     * The constructor takes two arguments. The first is the page that the renderer
73
74
75
76
     * 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.
     *
77
     * @param moodle_page $page the page we are doing output for.
78
     * @param string $target one of rendering target constants
79
     */
80
    public function __construct(moodle_page $page, $target) {
81
82
        $this->opencontainers = $page->opencontainers;
        $this->page = $page;
83
        $this->target = $target;
84
85
86
    }

    /**
87
     * Returns rendered widget.
88
89
90
     *
     * The provided widget needs to be an object that extends the renderable
     * interface.
91
     * If will then be rendered by a method based upon the classname for the widget.
92
93
94
     * 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
95
     * @param renderable $widget instance with renderable interface
96
     * @return string
97
     */
98
    public function render(renderable $widget) {
99
100
101
102
103
104
105
        $classname = get_class($widget);
        // Strip namespaces.
        $classname = preg_replace('/^.*\\\/', '', $classname);
        // Remove _renderable suffixes
        $classname = preg_replace('/_renderable$/', '', $classname);

        $rendermethod = 'render_'.$classname;
106
107
108
109
        if (method_exists($this, $rendermethod)) {
            return $this->$rendermethod($widget);
        }
        throw new coding_exception('Can not render widget, renderer method ('.$rendermethod.') not found.');
110
111
112
    }

    /**
113
114
115
116
     * 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.
117
     * If no id has been provided then a new ID is generated by {@link html_writer::random_id()}
118
     *
119
     * @param component_action $action
120
121
     * @param string $id
     * @return string id of element, either original submitted or random new if not supplied
122
     */
123
    public function add_action_handler(component_action $action, $id = null) {
124
125
126
        if (!$id) {
            $id = html_writer::random_id($action->event);
        }
127
        $this->page->requires->event_handler("#$id", $action->event, $action->jsfunction, $action->jsfunctionargs);
128
        return $id;
129
130
131
    }

    /**
132
133
     * Returns true is output has already started, and false if not.
     *
134
     * @return boolean true if the header has been printed.
135
     */
136
137
    public function has_started() {
        return $this->page->state >= moodle_page::STATE_IN_BODY;
138
139
140
141
    }

    /**
     * Given an array or space-separated list of classes, prepares and returns the HTML class attribute value
142
     *
143
144
145
146
147
148
149
150
151
152
153
     * @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;
    }

    /**
Petr Skoda's avatar
Petr Skoda committed
154
     * Return the moodle_url for an image.
155
     *
Petr Skoda's avatar
Petr Skoda committed
156
157
158
159
160
161
162
     * 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.
163
     *
Petr Skoda's avatar
Petr Skoda committed
164
165
166
167
168
169
170
171
172
173
174
     * 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/,
     *                    example: pix_url('comment', 'mod_glossary')
     *
     * @param string $imagename the pathname of the image
     * @param string $component full plugin name (aka component) or 'theme'
175
     * @return moodle_url
176
     */
177
    public function pix_url($imagename, $component = 'moodle') {
178
        return $this->page->theme->pix_url($imagename, $component);
179
180
181
    }
}

182

183
184
185
/**
 * Basis for all plugin renderers.
 *
186
187
188
189
 * @copyright Petr Skoda (skodak)
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 * @since Moodle 2.0
 * @package core
190
 * @category output
191
192
 */
class plugin_renderer_base extends renderer_base {
193

194
    /**
195
196
     * @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%
197
     * of cases by the {@link core_renderer}
198
199
200
201
     */
    protected $output;

    /**
Petr Skoda's avatar
Petr Skoda committed
202
     * Constructor method, calls the parent constructor
203
     *
204
     * @param moodle_page $page
205
     * @param string $target one of rendering target constants
206
     */
207
    public function __construct(moodle_page $page, $target) {
208
209
210
211
212
213
        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;
        }
214
215
        $this->output = $page->get_renderer('core', null, $target);
        parent::__construct($page, $target);
216
    }
217

218
    /**
219
220
     * Renders the provided widget and returns the HTML to display it.
     *
Petr Skoda's avatar
Petr Skoda committed
221
     * @param renderable $widget instance with renderable interface
222
223
224
     * @return string
     */
    public function render(renderable $widget) {
225
226
227
        $classname = get_class($widget);
        // Strip namespaces.
        $classname = preg_replace('/^.*\\\/', '', $classname);
228
229
        // Keep a copy at this point, we may need to look for a deprecated method.
        $deprecatedmethod = 'render_'.$classname;
230
        // Remove _renderable suffixes
231
        $classname = preg_replace('/_renderable$/', '', $classname);
232
233

        $rendermethod = 'render_'.$classname;
234
235
236
        if (method_exists($this, $rendermethod)) {
            return $this->$rendermethod($widget);
        }
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
        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);
        }
254
        // pass to core renderer if method not found here
255
        return $this->output->render($widget);
256
257
    }

258
259
    /**
     * Magic method used to pass calls otherwise meant for the standard renderer
Petr Skoda's avatar
Petr Skoda committed
260
     * to it to ensure we don't go causing unnecessary grief.
261
262
263
264
265
266
     *
     * @param string $method
     * @param array $arguments
     * @return mixed
     */
    public function __call($method, $arguments) {
267
        if (method_exists('renderer_base', $method)) {
268
            throw new coding_exception('Protected method called against '.get_class($this).' :: '.$method);
269
        }
270
271
272
        if (method_exists($this->output, $method)) {
            return call_user_func_array(array($this->output, $method), $arguments);
        } else {
273
            throw new coding_exception('Unknown method called against '.get_class($this).' :: '.$method);
274
275
        }
    }
276
}
277

278

279
/**
280
 * The standard implementation of the core_renderer interface.
281
282
 *
 * @copyright 2009 Tim Hunt
283
284
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 * @since Moodle 2.0
285
 * @package core
286
 * @category output
287
 */
288
class core_renderer extends renderer_base {
289
290
291
292
    /**
     * Do NOT use, please use <?php echo $OUTPUT->main_content() ?>
     * in layout files instead.
     * @deprecated
293
     * @var string used in {@link core_renderer::header()}.
294
     */
295
    const MAIN_CONTENT_TOKEN = '[MAIN CONTENT GOES HERE]';
296
297

    /**
298
299
     * @var string Used to pass information from {@link core_renderer::doctype()} to
     * {@link core_renderer::standard_head_html()}.
300
     */
301
    protected $contenttype;
302
303

    /**
304
305
     * @var string Used by {@link core_renderer::redirect_message()} method to communicate
     * with {@link core_renderer::header()}.
306
     */
307
    protected $metarefreshtag = '';
308
309

    /**
310
     * @var string Unique token for the closing HTML
311
     */
312
    protected $unique_end_html_token;
313
314

    /**
315
     * @var string Unique token for performance information
316
     */
317
    protected $unique_performance_info_token;
318
319

    /**
320
     * @var string Unique token for the main content.
321
     */
322
323
324
325
    protected $unique_main_content_token;

    /**
     * Constructor
326
     *
327
328
329
330
331
332
333
334
335
336
337
338
     * @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().']';
    }
339
340
341
342

    /**
     * Get the DOCTYPE declaration that should be used with this page. Designed to
     * be called in theme layout.php files.
343
     *
344
     * @return string the DOCTYPE declaration that should be used.
345
346
     */
    public function doctype() {
347
348
349
        if ($this->page->theme->doctype === 'html5') {
            $this->contenttype = 'text/html; charset=utf-8';
            return "<!DOCTYPE html>\n";
350

351
        } else if ($this->page->theme->doctype === 'xhtml5') {
352
            $this->contenttype = 'application/xhtml+xml; charset=utf-8';
353
            return "<!DOCTYPE html>\n";
354
355

        } else {
356
357
358
            // 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");
359
360
361
362
363
364
        }
    }

    /**
     * The attributes that should be added to the <html> tag. Designed to
     * be called in theme layout.php files.
365
     *
366
367
368
     * @return string HTML fragment.
     */
    public function htmlattributes() {
369
370
371
372
373
        $return = get_html_lang(true);
        if ($this->page->theme->doctype !== 'html5') {
            $return .= ' xmlns="http://www.w3.org/1999/xhtml"';
        }
        return $return;
374
375
376
377
378
379
    }

    /**
     * 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.
380
     *
381
382
383
     * @return string HTML fragment.
     */
    public function standard_head_html() {
384
        global $CFG, $SESSION;
385
386
387
388
389
390
391
392
393
394

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

395
396
397
        $output = '';
        $output .= '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />' . "\n";
        $output .= '<meta name="keywords" content="moodle, ' . $this->page->title . '" />' . "\n";
398
        // This is only set by the {@link redirect()} method
399
400
401
402
403
404
405
406
        $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().'" />';
        }

407
408
409
        // flow player embedding support
        $this->page->requires->js_function_call('M.util.load_flowplayer');

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

413
414
415
416
417
418
419
        // Setup help icon overlays.
        $this->page->requires->yui_module('moodle-core-popuphelp', 'M.core.init_popuphelp');
        $this->page->requires->strings_for_js(array(
            'morehelp',
            'loadinghelp',
        ), 'moodle');

420
        $this->page->requires->js_function_call('setTimeout', array('fix_column_widths()', 20));
421
422
423
424
425
426

        $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
427
                $this->page->requires->js_function_call('old_onload_focus', array($matches[1], $matches[2]));
428
429
430
431
432
433
            } 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
434
                $this->page->requires->js_function_call('focuscontrol', array($focus));
435
436
437
            }
        }

438
439
        // 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
440
        $urls = $this->page->theme->css_urls($this->page);
441
        foreach ($urls as $url) {
442
            $this->page->requires->css_theme($url);
443
444
        }

445
        // Get the theme javascript head and footer
446
447
448
449
450
451
        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);
        }
452

453
        // Get any HTML from the page_requirements_manager.
454
        $output .= $this->page->requires->get_head_code($this->page, $this);
455
456
457

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

462
463
464
        if (!empty($CFG->additionalhtmlhead)) {
            $output .= "\n".$CFG->additionalhtmlhead;
        }
465
466
467
468
469
470
471

        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.
472
     *
473
474
475
     * @return string HTML fragment.
     */
    public function standard_top_of_body_html() {
476
477
478
479
480
        global $CFG;
        $output = $this->page->requires->get_top_of_body_code();
        if (!empty($CFG->additionalhtmltopofbody)) {
            $output .= "\n".$CFG->additionalhtmltopofbody;
        }
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
        $output .= $this->maintenance_warning();
        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()) {
498
499
500
501
502
503
504
505
            $timeleft = $CFG->maintenance_later - time();
            // If timeleft less than 30 sec, set the class on block to error to highlight.
            $errorclass = ($timeleft < 30) ? 'error' : 'warning';
            $output .= $this->box_start($errorclass . ' moodle-has-zindex maintenancewarning');
            $a = new stdClass();
            $a->min = (int)($timeleft/60);
            $a->sec = (int)($timeleft % 60);
            $output .= get_string('maintenancemodeisscheduled', 'admin', $a) ;
506
            $output .= $this->box_end();
507
508
509
510
511
            $this->page->requires->yui_module('moodle-core-maintenancemodetimer', 'M.core.maintenancemodetimer',
                    array(array('timeleftinsec' => $timeleft)));
            $this->page->requires->strings_for_js(
                    array('maintenancemodeisscheduled', 'sitemaintenance'),
                    'admin');
512
        }
513
        return $output;
514
515
516
517
518
519
    }

    /**
     * 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.
520
     *
521
522
523
     * @return string HTML fragment.
     */
    public function standard_footer_html() {
524
        global $CFG, $SCRIPT;
525

526
527
528
529
530
531
        if (during_initial_install()) {
            // Debugging info can not work before install is finished,
            // in any case we do not want any links during installation!
            return '';
        }

532
        // This function is normally called from a layout.php file in {@link core_renderer::header()}
533
        // but some of the content won't be known until later, so we return a placeholder
534
        // for now. This will be replaced with the real content in {@link core_renderer::footer()}.
535
        $output = $this->unique_performance_info_token;
536
        if ($this->page->devicetypeinuse == 'legacy') {
537
538
539
            // The legacy theme is in use print the notification
            $output .= html_writer::tag('div', get_string('legacythemeinuse'), array('class'=>'legacythemeinuse'));
        }
540

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

544
        if (!empty($CFG->debugpageinfo)) {
545
            $output .= '<div class="performanceinfo pageinfo">This page is: ' . $this->page->debug_summary() . '</div>';
546
        }
547
        if (debugging(null, DEBUG_DEVELOPER) and has_capability('moodle/site:config', context_system::instance())) {  // Only in developer mode
548
549
550
551
            // 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');
552
                $url = $CFG->wwwroot . '/admin/tool/profiling/index.php?script=' . urlencode($SCRIPT);
553
554
555
                $link= '<a title="' . $title . '" href="' . $url . '">' . $txt . '</a>';
                $output .= '<div class="profilingfooter">' . $link . '</div>';
            }
Tim Hunt's avatar
Tim Hunt committed
556
557
558
559
            $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>';
560
        }
561
        if (!empty($CFG->debugvalidators)) {
562
            // 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
563
564
565
566
567
568
569
570
571
            $output .= '<div class="validators"><ul>
              <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;
    }

572
573
574
    /**
     * Returns standard main content placeholder.
     * Designed to be called in theme layout.php files.
575
     *
576
577
578
     * @return string HTML fragment.
     */
    public function main_content() {
579
580
581
582
583
584
585
        // 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>';
586
587
    }

588
589
    /**
     * The standard tags (typically script tags that are not needed earlier) that
590
     * should be output after everything else. Designed to be called in theme layout.php files.
591
     *
592
593
594
     * @return string HTML fragment.
     */
    public function standard_end_of_body_html() {
595
596
        global $CFG;

597
        // This function is normally called from a layout.php file in {@link core_renderer::header()}
598
        // but some of the content won't be known until later, so we return a placeholder
599
        // for now. This will be replaced with the real content in {@link core_renderer::footer()}.
600
601
602
603
604
605
        $output = '';
        if (!empty($CFG->additionalhtmlfooter)) {
            $output .= "\n".$CFG->additionalhtmlfooter;
        }
        $output .= $this->unique_end_html_token;
        return $output;
606
607
608
609
610
    }

    /**
     * Return the standard string that says whether you are logged in (and switched
     * roles/logged in as another user).
611
612
613
     * @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.
614
615
     * @return string HTML fragment.
     */
616
    public function login_info($withlinks = null) {
617
        global $USER, $CFG, $DB, $SESSION;
618

619
620
621
        if (during_initial_install()) {
            return '';
        }
622

623
624
625
626
        if (is_null($withlinks)) {
            $withlinks = empty($this->page->layout_options['nologinlinks']);
        }

627
        $loginpage = ((string)$this->page->url === get_login_url());
628
        $course = $this->page->course;
629
630
        if (\core\session\manager::is_loggedinas()) {
            $realuser = \core\session\manager::get_realuser();
631
            $fullname = fullname($realuser, true);
632
            if ($withlinks) {
633
634
635
                $loginastitle = get_string('loginas');
                $realuserinfo = " [<a href=\"$CFG->wwwroot/course/loginas.php?id=$course->id&amp;sesskey=".sesskey()."\"";
                $realuserinfo .= "title =\"".$loginastitle."\">$fullname</a>] ";
636
637
638
            } else {
                $realuserinfo = " [$fullname] ";
            }
639
640
641
        } else {
            $realuserinfo = '';
        }
642

643
        $loginurl = get_login_url();
644

645
646
647
        if (empty($course->id)) {
            // $course->id is not defined during installation
            return '';
648
        } else if (isloggedin()) {
649
            $context = context_course::instance($course->id);
650

651
            $fullname = fullname($USER, true);
652
            // Since Moodle 2.0 this link always goes to the public profile page (not the course profile page)
653
            if ($withlinks) {
654
655
                $linktitle = get_string('viewprofile');
                $username = "<a href=\"$CFG->wwwroot/user/profile.php?id=$USER->id\" title=\"$linktitle\">$fullname</a>";
656
657
658
            } else {
                $username = $fullname;
            }
659
            if (is_mnet_remote_user($USER) and $idprovider = $DB->get_record('mnet_host', array('id'=>$USER->mnethostid))) {
660
661
662
663
664
                if ($withlinks) {
                    $username .= " from <a href=\"{$idprovider->wwwroot}\">{$idprovider->name}</a>";
                } else {
                    $username .= " from {$idprovider->name}";
                }
665
            }
666
            if (isguestuser()) {
667
                $loggedinas = $realuserinfo.get_string('loggedinasguest');
668
                if (!$loginpage && $withlinks) {
669
670
                    $loggedinas .= " (<a href=\"$loginurl\">".get_string('login').'</a>)';
                }
671
            } else if (is_role_switched($course->id)) { // Has switched roles
672
673
                $rolename = '';
                if ($role = $DB->get_record('role', array('id'=>$USER->access['rsw'][$context->path]))) {
674
                    $rolename = ': '.role_get_name($role, $context);
675
                }
676
677
                $loggedinas = get_string('loggedinas', 'moodle', $username).$rolename;
                if ($withlinks) {
678
                    $url = new moodle_url('/course/switchrole.php', array('id'=>$course->id,'sesskey'=>sesskey(), 'switchrole'=>0, 'returnurl'=>$this->page->url->out_as_local_url(false)));
679
                    $loggedinas .= ' ('.html_writer::tag('a', get_string('switchrolereturn'), array('href' => $url)).')';
680
                }
681
            } else {
682
683
684
685
                $loggedinas = $realuserinfo.get_string('loggedinas', 'moodle', $username);
                if ($withlinks) {
                    $loggedinas .= " (<a href=\"$CFG->wwwroot/login/logout.php?sesskey=".sesskey()."\">".get_string('logout').'</a>)';
                }
686
687
            }
        } else {
688
            $loggedinas = get_string('loggedinnot', 'moodle');
689
            if (!$loginpage && $withlinks) {
690
691
                $loggedinas .= " (<a href=\"$loginurl\">".get_string('login').'</a>)';
            }
692
        }
693

694
        $loggedinas = '<div class="logininfo">'.$loggedinas.'</div>';
695

696
697
698
        if (isset($SESSION->justloggedin)) {
            unset($SESSION->justloggedin);
            if (!empty($CFG->displayloginfailures)) {
699
                if (!isguestuser()) {
700
701
702
                    // Include this file only when required.
                    require_once($CFG->dirroot . '/user/lib.php');
                    if ($count = user_count_login_failures($USER)) {
703
                        $loggedinas .= '<div class="loginfailures">';
704
705
706
                        $a = new stdClass();
                        $a->attempts = $count;
                        $loggedinas .= get_string('failedloginattempts', '', $a);
707
                        if (file_exists("$CFG->dirroot/report/log/index.php") and has_capability('report/log:view', context_system::instance())) {
708
709
                            $loggedinas .= ' ('.html_writer::link(new moodle_url('/report/log/index.php', array('chooselog' => 1,
                                    'id' => 0 , 'modid' => 'site_errors')), get_string('logs')).')';
710
711
712
713
714
715
                        }
                        $loggedinas .= '</div>';
                    }
                }
            }
        }
716

717
        return $loggedinas;
718
719
720
721
    }

    /**
     * Return the 'back' link that normally appears in the footer.
722
     *
723
724
725
726
727
728
729
730
     * @return string HTML fragment.
     */
    public function home_link() {
        global $CFG, $SITE;

        if ($this->page->pagetype == 'site-index') {
            // Special case for site home page - please do not remove
            return '<div class="sitelink">' .
731
                   '<a title="Moodle" href="http://moodle.org/">' .
732
                   '<img src="' . $this->pix_url('moodlelogo') . '" alt="moodlelogo" /></a></div>';
733
734
735
736

        } else if (!empty($CFG->target_release) && $CFG->target_release != $CFG->release) {
            // Special case for during install/upgrade.
            return '<div class="sitelink">'.
737
                   '<a title="Moodle" href="http://docs.moodle.org/en/Administrator_documentation" onclick="this.target=\'_blank\'">' .
738
                   '<img src="' . $this->pix_url('moodlelogo') . '" alt="moodlelogo" /></a></div>';
739
740
741
742
743
744
745

        } else if ($this->page->course->id == $SITE->id || strpos($this->page->pagetype, 'course-view') === 0) {
            return '<div class="homelink"><a href="' . $CFG->wwwroot . '/">' .
                    get_string('home') . '</a></div>';

        } else {
            return '<div class="homelink"><a href="' . $CFG->wwwroot . '/course/view.php?id=' . $this->page->course->id . '">' .
746
                    format_string($this->page->course->shortname, true, array('context' => $this->page->context)) . '</a></div>';
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
        }
    }

    /**
     * Redirects the user by any means possible given the current state
     *
     * This function should not be called directly, it should always be called using
     * the redirect function in lib/weblib.php
     *
     * The redirect function should really only be called before page output has started
     * however it will allow itself to be called during the state STATE_IN_BODY
     *
     * @param string $encodedurl The URL to send to encoded if required
     * @param string $message The message to display to the user if any
     * @param int $delay The delay before redirecting a user, if $message has been
     *         set this is a requirement and defaults to 3, set to 0 no delay
     * @param boolean $debugdisableredirect this redirect has been disabled for
     *         debugging purposes. Display a message that explains, and don't
     *         trigger the redirect.
     * @return string The HTML to display to the user before dying, may contain
     *         meta refresh, javascript refresh, and may have set header redirects
     */
    public function redirect_message($encodedurl, $message, $delay, $debugdisableredirect) {
        global $CFG;
        $url = str_replace('&amp;', '&', $encodedurl);

        switch ($this->page->state) {
            case moodle_page::STATE_BEFORE_HEADER :
                // No output yet it is safe to delivery the full arsenal of redirect methods
                if (!$debugdisableredirect) {
                    // Don't use exactly the same time here, it can cause problems when both redirects fire at the same time.
                    $this->metarefreshtag = '<meta http-equiv="refresh" content="'. $delay .'; url='. $encodedurl .'" />'."\n";
779
                    $this->page->requires->js_function_call('document.location.replace', array($url), false, ($delay + 3));
780
781
782
783
784
785
786
787
788
789
790
                }
                $output = $this->header();
                break;
            case moodle_page::STATE_PRINTING_HEADER :
                // We should hopefully never get here
                throw new coding_exception('You cannot redirect while printing the page header');
                break;
            case moodle_page::STATE_IN_BODY :
                // We really shouldn't be here but we can deal with this
                debugging("You should really redirect before you start page output");
                if (!$debugdisableredirect) {
791
                    $this->page->requires->js_function_call('document.location.replace', array($url), false, $delay);
792
793
794
795
796
797
798
799
800
                }
                $output = $this->opencontainers->pop_all_but_last();
                break;
            case moodle_page::STATE_DONE :
                // Too late to be calling redirect now
                throw new coding_exception('You cannot redirect after the entire page has been generated');
                break;
        }
        $output .= $this->notification($message, 'redirectmessage');
801
        $output .= '<div class="continuebutton">(<a href="'. $encodedurl .'">'. get_string('continue') .'</a>)</div>';
802
803
804
805
806
807
808
809
810
811
812
813
        if ($debugdisableredirect) {
            $output .= '<p><strong>Error output, so disabling automatic redirect.</strong></p>';
        }
        $output .= $this->footer();
        return $output;
    }

    /**
     * Start output by sending the HTTP headers, and printing the HTML <head>
     * and the start of the <body>.
     *
     * To control what is printed, you should set properties on $PAGE. If you
814
     * are familiar with the old {@link print_header()} function from Moodle 1.9
815
816
817
818
819
820
821
822
     * you will find that there are properties on $PAGE that correspond to most
     * of the old parameters to could be passed to print_header.
     *
     * Not that, in due course, the remaining $navigation, $menu parameters here
     * will be replaced by more properties of $PAGE, but that is still to do.
     *
     * @return string HTML that you must output this, preferably immediately.
     */
823
    public function header() {
824
825
        global $USER, $CFG;

826
        if (\core\session\manager::is_loggedinas()) {
827
828
829
            $this->page->add_body_class('userloggedinas');
        }

830
831
832
833
834
835
836
        // If the user is logged in, and we're not in initial install,
        // check to see if the user is role-switched and add the appropriate
        // CSS class to the body element.
        if (!during_initial_install() && isloggedin() && is_role_switched($this->page->course->id)) {
            $this->page->add_body_class('userswitchedrole');
        }

Petr Škoda's avatar
Petr Škoda committed
837
838
839
        // Give themes a chance to init/alter the page object.
        $this->page->theme->init_page($this->page);

840
841
        $this->page->set_state(moodle_page::STATE_PRINTING_HEADER);

842
843
844
845
        // Find the appropriate page layout file, based on $this->page->pagelayout.
        $layoutfile = $this->page->theme->layout_file($this->page->pagelayout);
        // Render the layout using the layout file.
        $rendered = $this->render_page_layout($layoutfile);
846

847
        // Slice the rendered output into header and footer.
848
849
850
851
852
853
854
855
        $cutpos = strpos($rendered, $this->unique_main_content_token);
        if ($cutpos === false) {
            $cutpos = strpos($rendered, self::MAIN_CONTENT_TOKEN);
            $token = self::MAIN_CONTENT_TOKEN;
        } else {
            $token = $this->unique_main_content_token;
        }

856
        if ($cutpos === false) {
857
            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.');
858
        }
859
        $header = substr($rendered, 0, $cutpos);
860
        $footer = substr($rendered, $cutpos + strlen($token));
861
862

        if (empty($this->contenttype)) {
863
            debugging('The page layout file did not call $OUTPUT->doctype()');
864
            $header = $this->doctype() . $header;
865
866
        }

867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
        // If this theme version is below 2.4 release and this is a course view page
        if ((!isset($this->page->theme->settings->version) || $this->page->theme->settings->version < 2012101500) &&
                $this->page->pagelayout === 'course' && $this->page->url->compare(new moodle_url('/course/view.php'), URL_MATCH_BASE)) {
            // check if course content header/footer have not been output during render of theme layout
            $coursecontentheader = $this->course_content_header(true);
            $coursecontentfooter = $this->course_content_footer(true);
            if (!empty($coursecontentheader)) {
                // display debug message and add header and footer right above and below main content
                // Please note that course header and footer (to be displayed above and below the whole page)
                // are not displayed in this case at all.
                // Besides the content header and footer are not displayed on any other course page
                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);
                $header .= $coursecontentheader;
                $footer = $coursecontentfooter. $footer;
            }
        }

884
        send_headers($this->contenttype, $this->page->cacheable);
885

886
887
        $this->opencontainers->push('header/footer', $footer);
        $this->page->set_state(moodle_page::STATE_IN_BODY);
888

889
        return $header . $this->skip_link_target('maincontent');
890
891
892
    }

    /**
893
     * Renders and outputs the page layout file.
894
895
896
897
898
     *
     * This is done by preparing the normal globals available to a script, and
     * then including the layout file provided by the current theme for the
     * requested layout.
     *
899
     * @param string $layoutfile The name of the layout file
900
901
     * @return string HTML code
     */
902
    protected function render_page_layout($layoutfile) {
903
        global $CFG, $SITE, $USER;
904
905
        // The next lines are a bit tricky. The point is, here we are in a method
        // of a renderer class, and this object may, or may not, be the same as
906
        // the global $OUTPUT object. When rendering the page layout file, we want to use
907
908
909
910
911
912
913
914
        // this object. However, people writing Moodle code expect the current
        // renderer to be called $OUTPUT, not $this, so define a variable called
        // $OUTPUT pointing at $this. The same comment applies to $PAGE and $COURSE.
        $OUTPUT = $this;
        $PAGE = $this->page;
        $COURSE = $this->page->course;

        ob_start();
915
916
        include($layoutfile);
        $rendered = ob_get_contents();
917
        ob_end_clean();
918
        return $rendered;
919
920
921
922
    }

    /**
     * Outputs the page's footer
923
     *
924
925
926
     * @return string HTML fragment
     */
    public function footer() {
927
        global $CFG, $DB;
928

929
        $output = $this->container_end_all(true);
930

931
932
        $footer = $this->opencontainers->pop('header/footer');

933
        if (debugging() and $DB and $DB->is_transaction_started()) {
934
            // TODO: MDL-20625 print warning - transaction will be rolled back
935
936
        }

937
938
939
940
941
942
943
944
        // Provide some performance info if required
        $performanceinfo = '';
        if (defined('MDL_PERF') || (!empty($CFG->perfdebug) and $CFG->perfdebug > 7)) {
            $perf = get_performance_info();
            if (defined('MDL_PERFTOFOOT') || debugging() || $CFG->perfdebug > 7) {
                $performanceinfo = $perf['html'];
            }
        }
945
946
947
948
949

        // We always want performance data when running a performance test, even if the user is redirected to another page.
        if (MDL_PERF_TEST && strpos($footer, $this->unique_performance_info_token) === false) {
            $footer = $this->unique_performance_info_token . $footer;
        }
950
        $footer = str_replace($this->unique_performance_info_token, $performanceinfo, $footer);
951

952
        $footer = str_replace($this->unique_end_html_token, $this->page->requires->get_end_code(), $footer);
953
954
955
956
957
958

        $this->page->set_state(moodle_page::STATE_DONE);

        return $output . $footer;
    }

959
960
961
962
    /**
     * Close all but the last open container. This is useful in places like error
     * handling, where you want to close all the open containers (apart from <body>)
     * before outputting the error message.
963
     *
964
965
966
967
968
969
970
971
     * @param bool $shouldbenone assert that the stack should be empty now - causes a
     *      developer debug warning if it isn't.
     * @return string the HTML required to close any open containers inside <body>.
     */
    public function container_end_all($shouldbenone = false) {
        return $this->opencontainers->pop_all_but_last($shouldbenone);
    }

972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
    /**
     * Returns course-specific information to be output immediately above content on any course page
     * (for the current course)
     *
     * @param bool $onlyifnotcalledbefore output content only if it has not been output before
     * @return string
     */
    public function course_content_header($onlyifnotcalledbefore = false) {
        global $CFG;
        if ($this->page->course->id == SITEID) {
            // return immediately and do not include /course/lib.php if not necessary
            return '';
        }
        static $functioncalled = false;
        if ($functioncalled && $onlyifnotcalledbefore) {
            // we have already output the content header
            return '';
        }
        require_once($CFG->dirroot.'/course/lib.php');
        $functioncalled = true;
        $courseformat = course_get_format($this->page->course);
        if (($obj = $courseformat->course_content_header()) !== null) {
994
            return html_writer::div($courseformat->get_renderer($this->page)->render($obj), 'course-content-header');
995
996
997
998
999
1000
        }
        return '';
    }

    /**
     * Returns course-specific information to be output immediately below content on any course page
For faster browsing, not all history is shown. View entire blame