MDL-19674 - err - slighly more sensible version of the previous commit.
[moodle.git] / lib / outputlib.php
CommitLineData
571fa828 1<?php
2
3// This file is part of Moodle - http://moodle.org/
4//
5// Moodle is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// Moodle is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17
18
19/**
20 * Functions for generating the HTML that Moodle should output.
21 *
22 * Please see http://docs.moodle.org/en/Developement:How_Moodle_outputs_HTML
23 * for an overview.
24 *
25 * @package moodlecore
26 * @copyright 2009 Tim Hunt
27 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later (5)
28 */
29
30
c84a2dbe 31function initialise_theme_and_output() {
32 global $CFG, $OUTPUT, $PAGE, $THEME;
33 if (!($OUTPUT instanceof bootstrap_renderer)) {
34 return; // Already done.
35 }
36 if (!isset($CFG->theme) || empty($PAGE)) {
37 // Too soon to do anything.
38 return;
39 }
40 theme_setup();
34a2777c 41 if (CLI_SCRIPT) {
c84a2dbe 42 $rendererfactory = new cli_renderer_factory($THEME, $PAGE);
34a2777c 43 } else {
c84a2dbe 44 $classname = $THEME->rendererfactory;
45 $rendererfactory = new $classname($THEME, $PAGE);
34a2777c 46 }
c84a2dbe 47 $OUTPUT = $rendererfactory->get_renderer('core');
34a2777c 48}
49
50
571fa828 51/**
52 * A renderer factory is just responsible for creating an appropriate renderer
53 * for any given part of Moodle.
54 *
55 * Which renderer factory to use is chose by the current theme, and an instance
56 * if created automatically when the theme is set up.
57 *
58 * A renderer factory must also have a constructor that takes a theme object and
59 * a moodle_page object. (See {@link renderer_factory_base::__construct} for an example.)
60 *
61 * @copyright 2009 Tim Hunt
62 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
63 * @since Moodle 2.0
64 */
65interface renderer_factory {
66 /**
67 * Return the renderer for a particular part of Moodle.
68 *
69 * The renderer interfaces are defined by classes called moodle_..._renderer
70 * where ... is the name of the module, which, will be defined in this file
71 * for core parts of Moodle, and in a file called renderer.php for plugins.
72 *
73 * There is no separate interface definintion for renderers. Instead we
74 * take advantage of PHP being a dynamic languages. The renderer returned
75 * does not need to be a subclass of the moodle_..._renderer base class, it
76 * just needs to impmenent the same interface. This is sometimes called
77 * 'Duck typing'. For a tricky example, see {@link template_renderer} below.
78 * renderer ob
79 *
80 * @param $module the name of part of moodle. E.g. 'core', 'quiz', 'qtype_multichoice'.
81 * @return object an object implementing the requested renderer interface.
82 */
83 public function get_renderer($module);
84}
85
86
87/**
88 * This is a base class to help you implement the renderer_factory interface.
89 *
90 * It keeps a cache of renderers that have been constructed, so you only need
91 * to construct each one once in you subclass.
92 *
93 * It also has a method to get the name of, and include the renderer.php with
94 * the definition of, the standard renderer class for a given module.
95 *
96 * @copyright 2009 Tim Hunt
97 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
98 * @since Moodle 2.0
99 */
100abstract class renderer_factory_base implements renderer_factory {
101 /** The theme we are rendering for. */
102 protected $theme;
103
104 /** The page we are doing output for. */
105 protected $page;
106
107 /** Used to cache renderers as they are created. */
108 protected $renderers = array();
109
34a2777c 110 protected $opencontainers;
111
571fa828 112 /**
113 * Constructor.
114 * @param object $theme the theme we are rendering for.
115 * @param moodle_page $page the page we are doing output for.
116 */
117 public function __construct($theme, $page) {
118 $this->theme = $theme;
119 $this->page = $page;
34a2777c 120 $this->opencontainers = new xhtml_container_stack();
571fa828 121 }
122
123 /* Implement the interface method. */
124 public function get_renderer($module) {
125 // Cache the renderers by module name, and delegate the actual
126 // construction to the create_renderer method.
127 if (!array_key_exists($module, $this->renderers)) {
128 $this->renderers[$module] = $this->create_renderer($module);
129 }
130
131 return $this->renderers[$module];
132 }
133
134 /**
135 * Subclasses should override this method to actually create an instance of
136 * the appropriate renderer class, based on the module name. That is,
137 * this method should implement the same contract as
138 * {@link renderer_factory::get_renderer}.
139 *
140 * @param $module the name of part of moodle. E.g. 'core', 'quiz', 'qtype_multichoice'.
141 * @return object an object implementing the requested renderer interface.
142 */
143 abstract public function create_renderer($module);
144
145 /**
146 * For a given module name, return the name of the standard renderer class
147 * that defines the renderer interface for that module.
148 *
149 * Also, if it exists, include the renderer.php file for that module, so
150 * the class definition of the default renderer has been loaded.
151 *
152 * @param string $module the name of part of moodle. E.g. 'core', 'quiz', 'qtype_multichoice'.
153 * @return string the name of the standard renderer class for that module.
154 */
155 protected function standard_renderer_class_for_module($module) {
156 $pluginrenderer = get_plugin_dir($module) . '/renderer.php';
157 if (file_exists($pluginrenderer)) {
158 include_once($pluginrenderer);
159 }
160 $class = 'moodle_' . $module . '_renderer';
161 if (!class_exists($class)) {
162 throw new coding_exception('Request for an unknown renderer class ' . $class);
163 }
164 return $class;
165 }
166}
167
168
169/**
170 * This is the default renderer factory for Moodle. It simply returns an instance
171 * of the appropriate standard renderer class.
172 *
173 * @copyright 2009 Tim Hunt
174 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
175 * @since Moodle 2.0
176 */
177class standard_renderer_factory extends renderer_factory_base {
178 /**
179 * Constructor.
180 * @param object $theme the theme we are rendering for.
181 * @param moodle_page $page the page we are doing output for.
182 */
183 public function __construct($theme, $page) {
184 parent::__construct($theme, $page);
185 }
186
187 /* Implement the subclass method. */
188 public function create_renderer($module) {
189 if ($module == 'core') {
c84a2dbe 190 return new moodle_core_renderer($this->opencontainers, $this->page, $this);
571fa828 191 } else {
192 $class = $this->standard_renderer_class_for_module($module);
34a2777c 193 return new $class($this->opencontainers, $this->get_renderer('core'), $this->page);
571fa828 194 }
195 }
196}
197
198
c84a2dbe 199/**
200 * This is a slight variation on the standard_renderer_factory used by CLI scripts.
201 *
202 * @copyright 2009 Tim Hunt
203 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
204 * @since Moodle 2.0
205 */
206class cli_renderer_factory extends standard_renderer_factory {
207 /**
208 * Constructor.
209 * @param object $theme the theme we are rendering for.
210 * @param moodle_page $page the page we are doing output for.
211 */
212 public function __construct($theme, $page) {
213 parent::__construct($theme, $page);
214 $this->renderers = array('core' => new cli_core_renderer($this->opencontainers, $this->page, $this));
571fa828 215 }
216}
217
218
219/**
220 * This is renderer factory allows themes to override the standard renderers using
221 * php code.
a5cb8d69 222 *
571fa828 223 * It will load any code from theme/mytheme/renderers.php and
224 * theme/parenttheme/renderers.php, if then exist. Then whenever you ask for
225 * a renderer for 'component', it will create a mytheme_component_renderer or a
226 * parenttheme_component_renderer, instead of a moodle_component_renderer,
227 * if either of those classes exist.
228 *
229 * This generates the slightly different HTML that the custom_corners theme expects.
230 *
231 * @copyright 2009 Tim Hunt
232 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
233 * @since Moodle 2.0
234 */
235class theme_overridden_renderer_factory extends standard_renderer_factory {
236 protected $prefixes = array();
237
238 /**
239 * Constructor.
240 * @param object $theme the theme we are rendering for.
241 * @param moodle_page $page the page we are doing output for.
242 */
243 public function __construct($theme, $page) {
244 global $CFG;
245 parent::__construct($theme, $page);
246
247 // Initialise $this->prefixes.
248 $renderersfile = $theme->dir . '/renderers.php';
249 if (is_readable($renderersfile)) {
250 include_once($renderersfile);
251 $this->prefixes[] = $theme->name . '_';
252 }
253 if (!empty($theme->parent)) {
254 $renderersfile = $CFG->themedir .'/'. $theme->parent . '/renderers.php';
255 if (is_readable($renderersfile)) {
256 include_once($renderersfile);
257 $this->prefixes[] = $theme->parent . '_';
258 }
259 }
260 }
261
262 /* Implement the subclass method. */
263 public function create_renderer($module) {
264 foreach ($this->prefixes as $prefix) {
265 $classname = $prefix . $module . '_renderer';
266 if (class_exists($classname)) {
267 if ($module == 'core') {
c84a2dbe 268 return new $classname($this->opencontainers, $this->page, $this);
571fa828 269 } else {
34a2777c 270 return new $classname($this->opencontainers, $this->get_renderer('core'), $this->page);
571fa828 271 }
272 }
273 }
274 return parent::create_renderer($module);
275 }
276}
277
278
279/**
280 * This is renderer factory that allows you to create templated themes.
281 *
282 * This should be considered an experimental proof of concept. In particular,
283 * the performance is probably not very good. Do not try to use in on a busy site
284 * without doing careful load testing first!
285 *
286 * This renderer factory returns instances of {@link template_renderer} class
287 * which which implement the corresponding renderer interface in terms of
288 * templates. To use this your theme must have a templates folder inside it.
289 * Then suppose the method moodle_core_renderer::greeting($name = 'world');
290 * exists. Then, a call to $OUTPUT->greeting() will cause the template
291 * /theme/yourtheme/templates/core/greeting.php to be rendered, with the variable
292 * $name available. The greeting.php template might contain
a5cb8d69 293 *
571fa828 294 * <pre>
295 * <h1>Hello <?php echo $name ?>!</h1>
296 * </pre>
297 *
298 * @copyright 2009 Tim Hunt
299 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
300 * @since Moodle 2.0
301 */
302class template_renderer_factory extends renderer_factory_base {
303 /**
304 * An array of paths of where to search for templates. Normally this theme,
305 * the parent theme then the standardtemplate theme. (If some of these do
306 * not exist, or are the same as each other, then the list will be shorter.
307 */
308 protected $searchpaths = array();
309
310 /**
311 * Constructor.
312 * @param object $theme the theme we are rendering for.
313 * @param moodle_page $page the page we are doing output for.
314 */
315 public function __construct($theme, $page) {
316 global $CFG;
317 parent::__construct($theme, $page);
318
319 // Initialise $this->searchpaths.
320 if ($theme->name != 'standardtemplate') {
321 $templatesdir = $theme->dir . '/templates';
322 if (is_dir($templatesdir)) {
323 $this->searchpaths[] = $templatesdir;
324 }
325 }
326 if (!empty($theme->parent)) {
327 $templatesdir = $CFG->themedir .'/'. $theme->parent . '/templates';
328 if (is_dir($templatesdir)) {
329 $this->searchpaths[] = $templatesdir;
330 }
331 }
332 $this->searchpaths[] = $CFG->themedir .'/standardtemplate/templates';
333 }
334
335 /* Implement the subclass method. */
336 public function create_renderer($module) {
337 // Refine the list of search paths for this module.
338 $searchpaths = array();
339 foreach ($this->searchpaths as $rootpath) {
340 $path = $rootpath . '/' . $module;
341 if (is_dir($path)) {
342 $searchpaths[] = $path;
343 }
344 }
345
346 // Create a template_renderer that copies the API of the standard renderer.
347 $copiedclass = $this->standard_renderer_class_for_module($module);
c84a2dbe 348 return new template_renderer($copiedclass, $searchpaths, $this->opencontainers, $this->page, $this);
571fa828 349 }
350}
351
352
353/**
354 * Simple base class for Moodle renderers.
355 *
356 * Tracks the xhtml_container_stack to use, which is passed in in the constructor.
357 *
8954245a 358 * Also has methods to facilitate generating HTML output.
359 *
571fa828 360 * @copyright 2009 Tim Hunt
361 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
362 * @since Moodle 2.0
363 */
364class moodle_renderer_base {
365 /** @var xhtml_container_stack the xhtml_container_stack to use. */
34a2777c 366 protected $opencontainers;
367 /** @var moodle_page the page we are rendering for. */
368 protected $page;
571fa828 369
370 /**
371 * Constructor
34a2777c 372 * @param $opencontainers the xhtml_container_stack to use.
373 * @param moodle_page $page the page we are doing output for.
571fa828 374 */
34a2777c 375 public function __construct($opencontainers, $page) {
376 $this->opencontainers = $opencontainers;
377 $this->page = $page;
571fa828 378 }
8954245a 379
380 protected function output_tag($tagname, $attributes, $contents) {
381 return $this->output_start_tag($tagname, $attributes) . $contents .
382 $this->output_end_tag($tagname);
383 }
384 protected function output_start_tag($tagname, $attributes) {
385 return '<' . $tagname . $this->output_attributes($attributes) . '>';
386 }
387 protected function output_end_tag($tagname) {
388 return '</' . $tagname . '>';
389 }
390 protected function output_empty_tag($tagname, $attributes) {
391 return '<' . $tagname . $this->output_attributes($attributes) . ' />';
392 }
393
394 protected function output_attribute($name, $value) {
34a2777c 395 $value = trim($value);
8954245a 396 if ($value || is_numeric($value)) { // We want 0 to be output.
397 return ' ' . $name . '="' . $value . '"';
398 }
399 }
400 protected function output_attributes($attributes) {
a5cb8d69 401 if (empty($attributes)) {
402 $attributes = array();
403 }
8954245a 404 $output = '';
405 foreach ($attributes as $name => $value) {
406 $output .= $this->output_attribute($name, $value);
407 }
408 return $output;
409 }
34a2777c 410 public static function prepare_classes($classes) {
411 if (is_array($classes)) {
412 return implode(' ', array_unique($classes));
413 }
414 return $classes;
8954245a 415 }
571fa828 416}
417
418
419/**
420 * This is the templated renderer which copies the API of another class, replacing
421 * all methods calls with instantiation of a template.
422 *
423 * When the method method_name is called, this class will search for a template
424 * called method_name.php in the folders in $searchpaths, taking the first one
425 * that it finds. Then it will set up variables for each of the arguments of that
426 * method, and render the template. This is implemented in the {@link __call()}
427 * PHP magic method.
428 *
429 * Methods like print_box_start and print_box_end are handles specially, and
430 * implemented in terms of the print_box.php method.
431 *
432 * @copyright 2009 Tim Hunt
433 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
434 * @since Moodle 2.0
435 */
436class template_renderer extends moodle_renderer_base {
437 /** @var ReflectionClass information about the class whose API we are copying. */
438 protected $copiedclass;
439 /** @var array of places to search for templates. */
440 protected $searchpaths;
c84a2dbe 441 protected $rendererfactory;
571fa828 442
443 /**
444 * Magic word used when breaking apart container templates to implement
445 * _start and _end methods.
446 */
447 const contentstoken = '-@#-Contents-go-here-#@-';
448
449 /**
450 * Constructor
451 * @param string $copiedclass the name of a class whose API we should be copying.
452 * @param $searchpaths a list of folders to search for templates in.
34a2777c 453 * @param $opencontainers the xhtml_container_stack to use.
454 * @param moodle_page $page the page we are doing output for.
c84a2dbe 455 * @param renderer_factory $rendererfactory the renderer factory that created us.
571fa828 456 */
c84a2dbe 457 public function __construct($copiedclass, $searchpaths, $opencontainers, $page, $rendererfactory) {
34a2777c 458 parent::__construct($opencontainers, $page);
571fa828 459 $this->copiedclass = new ReflectionClass($copiedclass);
460 $this->searchpaths = $searchpaths;
c84a2dbe 461 $this->rendererfactory = $rendererfactory;
462 }
463
464 /**
465 * Get a renderer for another part of Moodle.
466 * @param $module the name of part of moodle. E.g. 'core', 'quiz', 'qtype_multichoice'.
467 * @return object an object implementing the requested renderer interface.
468 */
469 public function get_other_renderer($module) {
470 $this->rendererfactory->get_renderer($module);
571fa828 471 }
472
473 /* PHP magic method implementation. */
474 public function __call($method, $arguments) {
475 if (substr($method, -6) == '_start') {
476 return $this->process_start(substr($method, 0, -6), $arguments);
477 } else if (substr($method, -4) == '_end') {
478 return $this->process_end(substr($method, 0, -4), $arguments);
479 } else {
480 return $this->process_template($method, $arguments);
481 }
482 }
483
484 /**
485 * Render the template for a given method of the renderer class we are copying,
486 * using the arguments passed.
487 * @param string $method the method that was called.
488 * @param array $arguments the arguments that were passed to it.
489 * @return string the HTML to be output.
490 */
491 protected function process_template($method, $arguments) {
492 if (!$this->copiedclass->hasMethod($method) ||
493 !$this->copiedclass->getMethod($method)->isPublic()) {
494 throw new coding_exception('Unknown method ' . $method);
495 }
496
497 // Find the template file for this method.
498 $template = $this->find_template($method);
499
500 // Use the reflection API to find out what variable names the arguments
501 // should be stored in, and fill in any missing ones with the defaults.
502 $namedarguments = array();
503 $expectedparams = $this->copiedclass->getMethod($method)->getParameters();
504 foreach ($expectedparams as $param) {
505 $paramname = $param->getName();
506 if (!empty($arguments)) {
507 $namedarguments[$paramname] = array_shift($arguments);
508 } else if ($param->isDefaultValueAvailable()) {
509 $namedarguments[$paramname] = $param->getDefaultValue();
510 } else {
511 throw new coding_exception('Missing required argument ' . $paramname);
512 }
513 }
514
515 // Actually render the template.
516 return $this->render_template($template, $namedarguments);
517 }
518
519 /**
520 * Actually do the work of rendering the template.
521 * @param $_template the full path to the template file.
522 * @param $_namedarguments an array variable name => value, the variables
523 * that should be available to the template.
524 * @return string the HTML to be output.
525 */
526 protected function render_template($_template, $_namedarguments) {
527 // Note, we intentionally break the coding guidelines with regards to
528 // local variable names used in this function, so that they do not clash
529 // with the names of any variables being passed to the template.
530
6e37fa50 531 global $CFG, $SITE, $THEME, $USER;
532 // The next lines are a bit tricky. The point is, here we are in a method
533 // of a renderer class, and this object may, or may not, be the the same as
534 // the global $OUTPUT object. When rendering the template, we want to use
535 // this object. However, people writing Moodle code expect the current
536 // rederer to be called $OUTPUT, not $this, so define a variable called
537 // $OUTPUT pointing at $this. The same comment applies to $PAGE and $COURSE.
34a2777c 538 $OUTPUT = $this;
6e37fa50 539 $PAGE = $this->page;
34a2777c 540 $COURSE = $this->page->course;
571fa828 541
542 // And the parameters from the function call.
543 extract($_namedarguments);
544
545 // Include the template, capturing the output.
546 ob_start();
547 include($_template);
548 $_result = ob_get_contents();
549 ob_end_clean();
550
551 return $_result;
552 }
553
554 /**
555 * Searches the folders in {@link $searchpaths} to try to find a template for
556 * this method name. Throws an exception if one cannot be found.
557 * @param string $method the method name.
558 * @return string the full path of the template to use.
559 */
560 protected function find_template($method) {
561 foreach ($this->searchpaths as $path) {
562 $filename = $path . '/' . $method . '.php';
563 if (file_exists($filename)) {
564 return $filename;
565 }
566 }
567 throw new coding_exception('Cannot find template for ' . $this->copiedclass->getName() . '::' . $method);
568 }
569
570 /**
571 * Handle methods like print_box_start by using the print_box template,
572 * splitting the result, pusing the end onto the stack, then returning the start.
573 * @param string $method the method that was called, with _start stripped off.
574 * @param array $arguments the arguments that were passed to it.
575 * @return string the HTML to be output.
576 */
577 protected function process_start($template, $arguments) {
578 array_unshift($arguments, self::contentstoken);
579 $html = $this->process_template($template, $arguments);
580 list($start, $end) = explode(self::contentstoken, $html, 2);
34a2777c 581 $this->opencontainers->push($template, $end);
571fa828 582 return $start;
583 }
584
585 /**
586 * Handle methods like print_box_end, we just need to pop the end HTML from
587 * the stack.
588 * @param string $method the method that was called, with _end stripped off.
589 * @param array $arguments not used. Assumed to be irrelevant.
590 * @return string the HTML to be output.
591 */
592 protected function process_end($template, $arguments) {
34a2777c 593 return $this->opencontainers->pop($template);
571fa828 594 }
595
596 /**
597 * @return array the list of paths where this class searches for templates.
598 */
599 public function get_search_paths() {
600 return $this->searchpaths;
601 }
602
603 /**
604 * @return string the name of the class whose API we are copying.
605 */
606 public function get_copied_class() {
607 return $this->copiedclass->getName();
608 }
609}
610
611
612/**
613 * This class keeps track of which HTML tags are currently open.
614 *
615 * This makes it much easier to always generate well formed XHTML output, even
616 * if execution terminates abruptly. Any time you output some opening HTML
617 * without the matching closing HTML, you should push the neccessary close tags
618 * onto the stack.
619 *
620 * @copyright 2009 Tim Hunt
621 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
622 * @since Moodle 2.0
623 */
624class xhtml_container_stack {
625 /** @var array stores the list of open containers. */
34a2777c 626 protected $opencontainers = array();
571fa828 627
628 /**
629 * Push the close HTML for a recently opened container onto the stack.
630 * @param string $type The type of container. This is checked when {@link pop()}
631 * is called and must match, otherwise a developer debug warning is output.
632 * @param string $closehtml The HTML required to close the container.
633 */
634 public function push($type, $closehtml) {
635 $container = new stdClass;
636 $container->type = $type;
637 $container->closehtml = $closehtml;
34a2777c 638 array_push($this->opencontainers, $container);
571fa828 639 }
640
641 /**
642 * Pop the HTML for the next closing container from the stack. The $type
643 * must match the type passed when the container was opened, otherwise a
644 * warning will be output.
645 * @param string $type The type of container.
646 * @return string the HTML requried to close the container.
647 */
648 public function pop($type) {
34a2777c 649 if (empty($this->opencontainers)) {
571fa828 650 debugging('There are no more open containers. This suggests there is a nesting problem.', DEBUG_DEVELOPER);
651 return;
652 }
653
34a2777c 654 $container = array_pop($this->opencontainers);
571fa828 655 if ($container->type != $type) {
656 debugging('The type of container to be closed (' . $container->type .
657 ') does not match the type of the next open container (' . $type .
658 '). This suggests there is a nesting problem.', DEBUG_DEVELOPER);
659 }
660 return $container->closehtml;
661 }
662
34a2777c 663 /**
664 * Return how many containers are currently open.
665 * @return integer how many containers are currently open.
666 */
667 public function count() {
668 return count($this->opencontainers);
669 }
670
571fa828 671 /**
672 * Close all but the last open container. This is useful in places like error
673 * handling, where you want to close all the open containers (apart from <body>)
674 * before outputting the error message.
675 * @return string the HTML requried to close any open containers inside <body>.
676 */
677 public function pop_all_but_last() {
678 $output = '';
34a2777c 679 while (count($this->opencontainers) > 1) {
680 $container = array_pop($this->opencontainers);
571fa828 681 $output .= $container->closehtml;
682 }
683 return $output;
684 }
685
686 /**
687 * You can call this function if you want to throw away an instance of this
688 * class without properly emptying the stack (for example, in a unit test).
689 * Calling this method stops the destruct method from outputting a developer
690 * debug warning. After calling this method, the instance can no longer be used.
691 */
692 public function discard() {
34a2777c 693 $this->opencontainers = null;
571fa828 694 }
695
696 /**
697 * Emergency fallback. If we get to the end of processing and not all
698 * containers have been closed, output the rest with a developer debug warning.
699 */
700 public function __destruct() {
34a2777c 701 if (empty($this->opencontainers)) {
571fa828 702 return;
703 }
704
705 debugging('Some containers were left open. This suggests there is a nesting problem.', DEBUG_DEVELOPER);
706 echo $this->pop_all_but_last();
34a2777c 707 $container = array_pop($this->opencontainers);
571fa828 708 echo $container->closehtml;
709 }
710}
711
712
713/**
714 * The standard implementation of the moodle_core_renderer interface.
715 *
716 * @copyright 2009 Tim Hunt
717 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
718 * @since Moodle 2.0
719 */
720class moodle_core_renderer extends moodle_renderer_base {
34a2777c 721 const PERFORMANCE_INFO_TOKEN = '%%PERFORMANCEINFO%%';
722 const END_HTML_TOKEN = '%%ENDHTML%%';
723 const MAIN_CONTENT_TOKEN = '[MAIN CONTENT GOES HERE]';
724 protected $contenttype;
c84a2dbe 725 protected $rendererfactory;
e8775320 726 protected $metarefreshtag = '';
c84a2dbe 727 /**
728 * Constructor
729 * @param $opencontainers the xhtml_container_stack to use.
730 * @param moodle_page $page the page we are doing output for.
731 * @param renderer_factory $rendererfactory the renderer factory that created us.
732 */
733 public function __construct($opencontainers, $page, $rendererfactory) {
734 parent::__construct($opencontainers, $page);
735 $this->rendererfactory = $rendererfactory;
736 }
737
738 /**
739 * Get a renderer for another part of Moodle.
740 * @param $module the name of part of moodle. E.g. 'core', 'quiz', 'qtype_multichoice'.
741 * @return object an object implementing the requested renderer interface.
742 */
743 public function get_other_renderer($module) {
744 $this->rendererfactory->get_renderer($module);
745 }
34a2777c 746
747 public function doctype() {
748 global $CFG;
749
750 $doctype = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' . "\n";
751 $this->contenttype = 'text/html; charset=utf-8';
752
753 if (empty($CFG->xmlstrictheaders)) {
754 return $doctype;
755 }
756
757 // We want to serve the page with an XML content type, to force well-formedness errors to be reported.
758 $prolog = '<?xml version="1.0" encoding="utf-8"?>' . "\n";
759 if (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/xhtml+xml') !== false) {
760 // Firefox and other browsers that can cope natively with XHTML.
761 $this->contenttype = 'application/xhtml+xml; charset=utf-8';
762
763 } else if (preg_match('/MSIE.*Windows NT/', $_SERVER['HTTP_USER_AGENT'])) {
764 // IE can't cope with application/xhtml+xml, but it will cope if we send application/xml with an XSL stylesheet.
765 $this->contenttype = 'application/xml; charset=utf-8';
766 $prolog .= '<?xml-stylesheet type="text/xsl" href="' . $CFG->httpswwwroot . '/lib/xhtml.xsl"?>' . "\n";
767
768 } else {
769 $prolog = '';
770 }
771
772 return $prolog . $doctype;
773 }
774
775 public function htmlattributes() {
776 return get_html_lang(true) . ' xmlns="http://www.w3.org/1999/xhtml"';
777 }
778
779 public function standard_head_html() {
780 global $CFG, $THEME;
781 $output = '';
782 $output .= '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />' . "\n";
783 $output .= '<meta name="keywords" content="moodle, ' . $this->page->title . '" />' . "\n";
784 if (!$this->page->cacheable) {
785 $output .= '<meta http-equiv="pragma" content="no-cache" />' . "\n";
786 $output .= '<meta http-equiv="expires" content="0" />' . "\n";
787 }
e8775320 788 // This is only set by the {@link redirect()} method
789 $output .= $this->metarefreshtag;
17a6649b 790
791 // Check if a periodic refresh delay has been set and make sure we arn't
792 // already meta refreshing
793 if ($this->metarefreshtag=='' && $this->page->periodicrefreshdelay!==null) {
794 $metarefesh = '<meta http-equiv="refresh" content="%d;url=%s" />';
795 $output .= sprintf($metarefesh, $this->page->periodicrefreshdelay, $this->page->url->out());
796 }
797
34a2777c 798 ob_start();
799 include($CFG->javascript);
800 $output .= ob_get_contents();
801 ob_end_clean();
802 $output .= $this->page->requires->get_head_code();
a5cb8d69 803
34a2777c 804 foreach ($this->page->alternateversions as $type => $alt) {
805 $output .= $this->output_empty_tag('link', array('rel' => 'alternate',
806 'type' => $type, 'title' => $alt->title, 'href' => $alt->url));
807 }
808
809 // Add the meta page from the themes if any were requested
810 // TODO kill this.
811 $PAGE = $this->page;
812 $metapage = '';
813 if (!isset($THEME->standardmetainclude) || $THEME->standardmetainclude) {
814 ob_start();
815 include_once($CFG->dirroot.'/theme/standard/meta.php');
816 $output .= ob_get_contents();
817 ob_end_clean();
818 }
819 if ($THEME->parent && (!isset($THEME->parentmetainclude) || $THEME->parentmetainclude)) {
820 if (file_exists($CFG->dirroot.'/theme/'.$THEME->parent.'/meta.php')) {
821 ob_start();
822 include_once($CFG->dirroot.'/theme/'.$THEME->parent.'/meta.php');
823 $output .= ob_get_contents();
824 ob_end_clean();
825 }
826 }
827 if (!isset($THEME->metainclude) || $THEME->metainclude) {
828 if (file_exists($CFG->dirroot.'/theme/'.current_theme().'/meta.php')) {
829 ob_start();
830 include_once($CFG->dirroot.'/theme/'.current_theme().'/meta.php');
831 $output .= ob_get_contents();
832 ob_end_clean();
833 }
834 }
835
836 return $output;
837 }
838
839 public function standard_top_of_body_html() {
840 return $this->page->requires->get_top_of_body_code();
841 }
842
843 public function standard_footer_html() {
844 $output = self::PERFORMANCE_INFO_TOKEN;
845 if (debugging()) {
846 $output .= '<div class="validators"><ul>
847 <li><a href="http://validator.w3.org/check?verbose=1&amp;ss=1&amp;uri=' . urlencode(qualified_me()) . '">Validate HTML</a></li>
848 <li><a href="http://www.contentquality.com/mynewtester/cynthia.exe?rptmode=-1&amp;url1=' . urlencode(qualified_me()) . '">Section 508 Check</a></li>
849 <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>
850 </ul></div>';
851 }
852 return $output;
853 }
854
855 public function standard_end_of_body_html() {
856 echo self::END_HTML_TOKEN;
857 }
858
859 public function login_info() {
860 global $USER;
861 return user_login_string($this->page->course, $USER);
862 }
863
864 public function home_link() {
865 global $CFG, $SITE;
866
867 if ($this->page->pagetype == 'site-index') {
868 // Special case for site home page - please do not remove
869 return '<div class="sitelink">' .
870 '<a title="Moodle ' . $CFG->release . '" href="http://moodle.org/">' .
871 '<img style="width:100px;height:30px" src="' . $CFG->httpswwwroot . '/pix/moodlelogo.gif" alt="moodlelogo" /></a></div>';
872
873 } else if (!empty($CFG->target_release) && $CFG->target_release != $CFG->release) {
874 // Special case for during install/upgrade.
875 return '<div class="sitelink">'.
876 '<a title="Moodle ' . $CFG->target_release . '" href="http://docs.moodle.org/en/Administrator_documentation" onclick="this.target=\'_blank\'">' .
877 '<img style="width:100px;height:30px" src="' . $CFG->httpswwwroot . '/pix/moodlelogo.gif" alt="moodlelogo" /></a></div>';
878
879 } else if ($this->page->course->id == $SITE->id || strpos($this->page->pagetype, 'course-view') === 0) {
880 return '<div class="homelink"><a href="' . $CFG->wwwroot . '/">' .
881 get_string('home') . '</a></div>';
882
883 } else {
884 return '<div class="homelink"><a href="' . $CFG->wwwroot . '/course/view.php?id=' . $this->page->course->id . '">' .
885 format_string($this->page->course->shortname) . '</a></div>';
886 }
887 }
888
afa2dcad 889 /**
890 * Checks if we are in the body yet or not and returns true if we are in
891 * the body, false if we havn't reached it yet
892 *
893 * @uses moodle_page::STATE_IN_BODY
894 * @return bool True for in body, false if before
895 */
e8775320 896 public function has_started() {
afa2dcad 897 return ($this->page->state >= moodle_page::STATE_IN_BODY);
e8775320 898 }
899
afa2dcad 900 /**
901 * Redirects the user by any means possible given the current state
902 *
903 * This function should not be called directly, it should always be called using
904 * the redirect function in lib/weblib.php
905 *
906 * The redirect function should really only be called before page output has started
907 * however it will allow itself to be called during the state STATE_IN_BODY
908 *
909 * @global object
910 * @uses DEBUG_DEVELOPER
911 * @uses DEBUG_ALL
912 * @uses moodle_page::STATE_BEFORE_HEADER
913 * @uses moodle_page::STATE_PRINTING_HEADER
914 * @uses moodle_page::STATE_IN_BODY
915 * @uses moodle_page::STATE_DONE
916 * @param string $encodedurl The URL to send to encoded if required
917 * @param string $message The message to display to the user if any
918 * @param int $delay The delay before redirecting a user, if $message has been
919 * set this is a requirement and defaults to 3, set to 0 no delay
920 * @param string $messageclass The css class to put on the message that is
921 * being displayed to the user
922 * @return string The HTML to display to the user before dying, may contain
923 * meta refresh, javascript refresh, and may have set header redirects
924 */
e8775320 925 public function redirect($encodedurl, $message, $delay, $messageclass='notifyproblem') {
926 global $CFG;
927 $url = str_replace('&amp;', '&', $encodedurl);
928
929 $disableredirect = false;
930
931 if ($delay!=0) {
932 /// At developer debug level. Don't redirect if errors have been printed on screen.
933 /// Currenly only works in PHP 5.2+; we do not want strict PHP5 errors
934 $lasterror = error_get_last();
935 $error = defined('DEBUGGING_PRINTED') or (!empty($lasterror) && ($lasterror['type'] & DEBUG_DEVELOPER));
936 $errorprinted = debugging('', DEBUG_ALL) && $CFG->debugdisplay && $error;
937 if ($errorprinted) {
938 $disableredirect= true;
939 $message = "<strong>Error output, so disabling automatic redirect.</strong></p><p>" . $message;
940 }
941 }
942
943 switch ($this->page->state) {
944 case moodle_page::STATE_BEFORE_HEADER :
945 // No output yet it is safe to delivery the full arsenol of redirect methods
946 if (!$disableredirect) {
947 @header($_SERVER['SERVER_PROTOCOL'] . ' 303 See Other'); //302 might not work for POST requests, 303 is ignored by obsolete clients
948 @header('Location: '.$url);
949 $this->metarefreshtag = '<meta http-equiv="refresh" content="'. $delay .'; url='. $encodedurl .'" />'."\n";
950 $this->page->requires->js_function_call('document.location.replace', array($url))->after_delay($delay+3);
951 }
17a6649b 952 $this->page->set_generaltype('popup');
953 $this->page->set_title('redirect');
e8775320 954 $output = $this->header();
955 $output .= $this->notification($message, $messageclass);
956 $output .= $this->footer();
957 break;
958 case moodle_page::STATE_PRINTING_HEADER :
959 // We should hopefully never get here
960 throw new coding_exception('You cannot redirect while printing the page header');
961 break;
962 case moodle_page::STATE_IN_BODY :
963 // We really shouldn't be here but we can deal with this
964 debugging("You should really redirect before you start page output");
965 if (!$disableredirect) {
966 $this->page->requires->js_function_call('document.location.replace', array($url))->after_delay($delay+3);
967 }
968 $output = $this->opencontainers->pop_all_but_last();
969 $output .= $this->notification($message, $messageclass);
970 $output .= $this->footer();
971 break;
972 case moodle_page::STATE_DONE :
973 // Too late to be calling redirect now
974 throw new coding_exception('You cannot redirect after the entire page has been generated');
975 break;
976 }
977 return $output;
978 }
979
34a2777c 980 // TODO remove $navigation and $menu arguments - replace with $PAGE->navigation
981 public function header($navigation = '', $menu='') {
982 global $USER, $CFG;
983
984 output_starting_hook();
985 $this->page->set_state(moodle_page::STATE_PRINTING_HEADER);
986
987 // Add any stylesheets required using the horrible legacy mechanism. TODO kill this.
988 foreach ($CFG->stylesheets as $stylesheet) {
989 $this->page->requires->css($stylesheet, true);
990 }
991
992 // Find the appropriate page template, based on $this->page->generaltype.
993 $templatefile = $this->find_page_template();
994 if ($templatefile) {
995 // Render the template.
996 $template = $this->render_page_template($templatefile, $menu, $navigation);
997 } else {
998 // New style template not found, fall back to using header.html and footer.html.
999 $template = $this->handle_legacy_theme($navigation, $menu);
1000 }
1001
1002 // Slice the template output into header and footer.
1003 $cutpos = strpos($template, self::MAIN_CONTENT_TOKEN);
1004 if ($cutpos === false) {
1005 throw new coding_exception('Layout template ' . $templatefile .
1006 ' does not contain the string "' . self::MAIN_CONTENT_TOKEN . '".');
1007 }
1008 $header = substr($template, 0, $cutpos);
1009 $footer = substr($template, $cutpos + strlen(self::MAIN_CONTENT_TOKEN));
1010
1011 send_headers($this->contenttype, $this->page->cacheable);
1012 $this->opencontainers->push('header/footer', $footer);
1013 $this->page->set_state(moodle_page::STATE_IN_BODY);
1014 return $header . $this->skip_link_target();
1015 }
1016
1017 protected function find_page_template() {
1018 global $THEME;
1019
1020 // If this is a particular page type, look for a specific template.
1021 $type = $this->page->generaltype;
1022 if ($type != 'normal') {
1023 $templatefile = $THEME->dir . '/layout-' . $type . '.php';
1024 if (is_readable($templatefile)) {
1025 return $templatefile;
1026 }
1027 }
1028
1029 // Otherwise look for the general template.
1030 $templatefile = $THEME->dir . '/layout.php';
1031 if (is_readable($templatefile)) {
1032 return $templatefile;
1033 }
1034
1035 return false;
1036 }
1037
1038 protected function render_page_template($templatefile, $menu, $navigation) {
1039 global $CFG, $SITE, $THEME, $USER;
6e37fa50 1040 // The next lines are a bit tricky. The point is, here we are in a method
1041 // of a renderer class, and this object may, or may not, be the the same as
1042 // the global $OUTPUT object. When rendering the template, we want to use
1043 // this object. However, people writing Moodle code expect the current
1044 // rederer to be called $OUTPUT, not $this, so define a variable called
1045 // $OUTPUT pointing at $this. The same comment applies to $PAGE and $COURSE.
34a2777c 1046 $OUTPUT = $this;
1047 $PAGE = $this->page;
1048 $COURSE = $this->page->course;
1049
1050 ob_start();
1051 include($templatefile);
1052 $template = ob_get_contents();
1053 ob_end_clean();
1054 return $template;
1055 }
1056
1057 protected function handle_legacy_theme($navigation, $menu) {
1058 global $CFG, $SITE, $THEME, $USER;
1059 // Set a pretend global from the properties of this class.
6e37fa50 1060 // See the comment in render_page_template for a fuller explanation.
34a2777c 1061 $COURSE = $this->page->course;
1062
1063 // Set up local variables that header.html expects.
1064 $direction = $this->htmlattributes();
1065 $title = $this->page->title;
1066 $heading = $this->page->heading;
1067 $focus = $this->page->focuscontrol;
1068 $button = $this->page->button;
1069 $pageid = $this->page->pagetype;
1070 $pageclass = $this->page->bodyclasses;
1071 $bodytags = ' class="' . $pageclass . '" id="' . $pageid . '"';
1072 $home = $this->page->generaltype == 'home';
1073
1074 $meta = $this->standard_head_html();
1075 // The next line is a nasty hack. having set $meta to standard_head_html, we have already
1076 // got the contents of include($CFG->javascript). However, legacy themes are going to
1077 // include($CFG->javascript) again. We want to make sure that when they do, nothing is output.
1078 $CFG->javascript = $CFG->libdir . '/emptyfile.php';
1079
1080 // Set up local variables that footer.html expects.
1081 $homelink = $this->home_link();
1082 $loggedinas = $this->login_info();
1083 $course = $this->page->course;
1084 $performanceinfo = self::PERFORMANCE_INFO_TOKEN;
1085
1086 if (!$menu && $navigation) {
1087 $menu = $loggedinas;
1088 }
1089
1090 ob_start();
1091 include($THEME->dir . '/header.html');
1092 $this->page->requires->get_top_of_body_code();
1093 echo self::MAIN_CONTENT_TOKEN;
1094
1095 $menu = str_replace('navmenu', 'navmenufooter', $menu);
1096 include($THEME->dir . '/footer.html');
1097
1098 $output = ob_get_contents();
1099 ob_end_clean();
1100
1101 $output = str_replace('</body>', self::END_HTML_TOKEN . '</body>', $output);
1102
1103 return $output;
1104 }
1105
1106 public function footer() {
1107 $output = '';
1108 if ($this->opencontainers->count() != 1) {
1109 debugging('Some HTML tags were opened in the body of the page but not closed.', DEBUG_DEVELOPER);
1110 $output .= $this->opencontainers->pop_all_but_last();
1111 }
1112
1113 $footer = $this->opencontainers->pop('header/footer');
1114
1115 // Provide some performance info if required
1116 $performanceinfo = '';
1117 if (defined('MDL_PERF') || (!empty($CFG->perfdebug) and $CFG->perfdebug > 7)) {
1118 $perf = get_performance_info();
1119 if (defined('MDL_PERFTOLOG') && !function_exists('register_shutdown_function')) {
1120 error_log("PERF: " . $perf['txt']);
1121 }
1122 if (defined('MDL_PERFTOFOOT') || debugging() || $CFG->perfdebug > 7) {
1123 $performanceinfo = $perf['html'];
1124 }
1125 }
1126 $footer = str_replace(self::PERFORMANCE_INFO_TOKEN, $performanceinfo, $footer);
1127
1128 $footer = str_replace(self::END_HTML_TOKEN, $this->page->requires->get_end_code(), $footer);
1129
1130 $this->page->set_state(moodle_page::STATE_DONE);
1131
1132 return $output . $footer;
1133 }
1134
a5cb8d69 1135 /**
1136 * Prints a nice side block with an optional header.
1137 *
1138 * The content is described
1139 * by a {@link block_contents} object.
1140 *
1141 * @param block $content HTML for the content
1142 * @return string the HTML to be output.
1143 */
1144 function block($bc) {
1145 $bc = clone($bc);
1146 $bc->prepare();
1147
1148 $title = strip_tags($bc->title);
1149 if (empty($title)) {
1150 $output = '';
1151 $skipdest = '';
1152 } else {
1153 $output = $this->output_tag('a', array('href' => '#sb-' . $bc->skipid, 'class' => 'skip-block'),
1154 get_string('skipa', 'access', $title));
1155 $skipdest = $this->output_tag('span', array('id' => 'sb-' . $bc->skipid, 'class' => 'skip-block-to'), '');
1156 }
1157
1158 $bc->attributes['id'] = $bc->id;
1159 $bc->attributes['class'] = $bc->get_classes_string();
1160 $output .= $this->output_start_tag('div', $bc->attributes);
1161
1162 if ($bc->heading) {
1163 // Some callers pass in complete html for the heading, which may include
1164 // complicated things such as the 'hide block' button; some just pass in
1165 // text. If they only pass in plain text i.e. it doesn't include a
1166 // <div>, then we add in standard tags that make it look like a normal
1167 // page block including the h2 for accessibility
1168 if (strpos($bc->heading, '</div>') === false) {
1169 $bc->heading = $this->output_tag('div', array('class' => 'title'),
1170 $this->output_tag('h2', null, $bc->heading));
1171 }
1172
1173 $output .= $this->output_tag('div', array('class' => 'header'), $bc->heading);
1174 }
1175
1176 $output .= $this->output_start_tag('div', array('class' => 'content'));
1177
1178 if ($bc->content) {
1179 $output .= $bc->content;
1180
1181 } else if ($bc->list) {
1182 $row = 0;
a5cb8d69 1183 $items = array();
1184 foreach ($bc->list as $key => $string) {
1185 $item = $this->output_start_tag('li', array('class' => 'r' . $row));
1186 if ($bc->icons) {
1187 $item .= $this->output_tag('div', array('class' => 'icon column c0'), $bc->icons[$key]);
1188 }
1189 $item .= $this->output_tag('div', array('class' => 'column c1'), $string);
1190 $item .= $this->output_end_tag('li');
1191 $items[] = $item;
1192 $row = 1 - $row; // Flip even/odd.
1193 }
1194 $output .= $this->output_tag('ul', array('class' => 'list'), implode("\n", $items));
1195 }
1196
1197 if ($bc->footer) {
1198 $output .= $this->output_tag('div', array('class' => 'footer'), $bc->footer);
1199 }
1200
1201 $output .= $this->output_end_tag('div');
1202 $output .= $this->output_end_tag('div');
1203 $output .= $skipdest;
1204
1205 if (!empty($CFG->allowuserblockhiding) && isset($attributes['id'])) {
1206 $strshow = addslashes_js(get_string('showblocka', 'access', $title));
1207 $strhide = addslashes_js(get_string('hideblocka', 'access', $title));
1208 $output .= $this->page->requires->js_function_call('elementCookieHide', array(
1209 $bc->id, $strshow, $strhide))->asap();
1210 }
1211
1212 return $output;
1213 }
1214
8954245a 1215 public function link_to_popup_window() {
a5cb8d69 1216
8954245a 1217 }
571fa828 1218
8954245a 1219 public function button_to_popup_window() {
a5cb8d69 1220
8954245a 1221 }
1222
1223 public function close_window_button($buttontext = null, $reloadopener = false) {
1224 if (empty($buttontext)) {
1225 $buttontext = get_string('closewindow');
1226 }
1227 // TODO
1228 }
1229
1230 public function close_window($delay = 0, $reloadopener = false) {
1231 // TODO
1232 }
1233
1234 /**
1235 * Output a <select> menu.
1236 *
1237 * You can either call this function with a single moodle_select_menu argument
1238 * or, with a list of parameters, in which case those parameters are sent to
1239 * the moodle_select_menu constructor.
1240 *
1241 * @param moodle_select_menu $selectmenu a moodle_select_menu that describes
1242 * the select menu you want output.
1243 * @return string the HTML for the <select>
1244 */
1245 public function select_menu($selectmenu) {
1246 $selectmenu = clone($selectmenu);
1247 $selectmenu->prepare();
1248
1249 if ($selectmenu->nothinglabel) {
1250 $selectmenu->options = array($selectmenu->nothingvalue => $selectmenu->nothinglabel) +
1251 $selectmenu->options;
1252 }
1253
1254 if (empty($selectmenu->id)) {
1255 $selectmenu->id = 'menu' . str_replace(array('[', ']'), '', $selectmenu->name);
1256 }
1257
1258 $attributes = array(
1259 'name' => $selectmenu->name,
1260 'id' => $selectmenu->id,
1261 'class' => $selectmenu->get_classes_string(),
1262 'onchange' => $selectmenu->script,
1263 );
1264 if ($selectmenu->disabled) {
1265 $attributes['disabled'] = 'disabled';
1266 }
1267 if ($selectmenu->tabindex) {
1268 $attributes['tabindex'] = $tabindex;
1269 }
1270
1271 if ($selectmenu->listbox) {
1272 if (is_integer($selectmenu->listbox)) {
1273 $size = $selectmenu->listbox;
1274 } else {
1275 $size = min($selectmenu->maxautosize, count($selectmenu->options));
1276 }
1277 $attributes['size'] = $size;
1278 if ($selectmenu->multiple) {
1279 $attributes['multiple'] = 'multiple';
1280 }
1281 }
1282
1283 $html = $this->output_start_tag('select', $attributes) . "\n";
1284 foreach ($selectmenu->options as $value => $label) {
1285 $attributes = array('value' => $value);
1286 if ((string)$value == (string)$selectmenu->selectedvalue ||
1287 (is_array($selectmenu->selectedvalue) && in_array($value, $selectmenu->selectedvalue))) {
1288 $attributes['selected'] = 'selected';
1289 }
1290 $html .= ' ' . $this->output_tag('option', $attributes, s($label)) . "\n";
1291 }
1292 $html .= $this->output_end_tag('select') . "\n";
1293
1294 return $html;
1295 }
1296
1297 // TODO choose_from_menu_nested
1298
1299 // TODO choose_from_radio
1300
1301 /**
1302 * Output an error message. By default wraps the error message in <span class="error">.
1303 * If the error message is blank, nothing is output.
1304 * @param $message the error message.
1305 * @return string the HTML to output.
1306 */
1307 public function error_text($message) {
1308 if (empty($message)) {
1309 return '';
1310 }
1311 return $this->output_tag('span', array('class' => 'error'), $message);
1312 }
34a2777c 1313
1314 /**
1315 * Do not call this function directly.
1316 *
1317 * To terminate the current script with a fatal error, call the {@link print_error}
1318 * function, or throw an exception. Doing either of those things will then call this
1319 * funciton to display the error, before terminating the exection.
1320 *
1321 * @param string $message
1322 * @param string $moreinfourl
1323 * @param string $link
1324 * @param array $backtrace
1325 * @param string $debuginfo
1326 * @param bool $showerrordebugwarning
1327 * @return string the HTML to output.
1328 */
1329 public function fatal_error($message, $moreinfourl, $link, $backtrace,
1330 $debuginfo = null, $showerrordebugwarning = false) {
1331
1332 $output = '';
1333
e8775320 1334 if ($this->has_started()) {
1335 $output .= $this->opencontainers->pop_all_but_last();
1336 } else {
34a2777c 1337 // Header not yet printed
1338 @header('HTTP/1.0 404 Not Found');
c84a2dbe 1339 $this->page->set_title(get_string('error'));
1340 $output .= $this->header();
34a2777c 1341 }
1342
1343 $message = '<p class="errormessage">' . $message . '</p>'.
1344 '<p class="errorcode"><a href="' . $moreinfourl . '">' .
1345 get_string('moreinformation') . '</a></p>';
1346 $output .= $this->box($message, 'errorbox');
1347
1348 if (debugging('', DEBUG_DEVELOPER)) {
1349 if ($showerrordebugwarning) {
1350 $output .= $this->notification('error() is a deprecated function. ' .
1351 'Please call print_error() instead of error()', 'notifytiny');
1352 }
1353 if (!empty($debuginfo)) {
1354 $output .= $this->notification($debuginfo, 'notifytiny');
1355 }
1356 if (!empty($backtrace)) {
1357 $output .= $this->notification('Stack trace: ' .
1358 format_backtrace($backtrace, true), 'notifytiny');
1359 }
1360 }
1361
1362 if (!empty($link)) {
1363 $output .= $this->continue_button($link);
1364 }
1365
c84a2dbe 1366 $output .= $this->footer();
34a2777c 1367
1368 // Padding to encourage IE to display our error page, rather than its own.
1369 $output .= str_repeat(' ', 512);
1370
1371 return $output;
1372 }
1373
1374 /**
1375 * Output a notification (that is, a status message about something that has
1376 * just happened).
1377 *
1378 * @param string $message the message to print out
1379 * @param string $classes normally 'notifyproblem' or 'notifysuccess'.
1380 * @return string the HTML to output.
1381 */
1382 public function notification($message, $classes = 'notifyproblem') {
1383 return $this->output_tag('div', array('class' =>
1384 moodle_renderer_base::prepare_classes($classes)), clean_text($message));
1385 }
1386
1387 /**
1388 * Print a continue button that goes to a particular URL.
1389 *
1390 * @param string|moodle_url $link The url the button goes to.
1391 * @return string the HTML to output.
1392 */
1393 public function continue_button($link) {
1394 if (!is_a($link, 'moodle_url')) {
1395 $link = new moodle_url($link);
1396 }
1397 return $this->output_tag('div', array('class' => 'continuebutton'),
1398 print_single_button($link->out(true), $link->params(), get_string('continue'), 'get', '', true));
1399 }
1400
1401 /**
1402 * Output the place a skip link goes to.
1403 * @param $id The target name from the corresponding $PAGE->requires->skip_link_to($target) call.
1404 * @return string the HTML to output.
1405 */
1406 public function skip_link_target($id = 'maincontent') {
1407 return $this->output_tag('span', array('id' => $id), '');
1408 }
1409
1410 public function heading($text, $level, $classes = 'main', $id = '') {
1411 $level = (integer) $level;
1412 if ($level < 1 or $level > 6) {
1413 throw new coding_exception('Heading level must be an integer between 1 and 6.');
1414 }
1415 return $this->output_tag('h' . $level,
1416 array('id' => $id, 'class' => moodle_renderer_base::prepare_classes($classes)), $text);
1417 }
1418
1419 public function box($contents, $classes = 'generalbox', $id = '') {
1420 return $this->box_start($classes, $id) . $contents . $this->box_end();
1421 }
1422
1423 public function box_start($classes = 'generalbox', $id = '') {
1424 $this->opencontainers->push('box', $this->output_end_tag('div'));
1425 return $this->output_start_tag('div', array('id' => $id,
1426 'class' => 'box ' . moodle_renderer_base::prepare_classes($classes)));
1427 }
1428
1429 public function box_end() {
1430 return $this->opencontainers->pop('box');
1431 }
1432
1433 public function container($contents, $classes = '', $id = '') {
1434 return $this->container_start($classes, $id) . $contents . $this->container_end();
1435 }
1436
1437 public function container_start($classes = '', $id = '') {
1438 $this->opencontainers->push('container', $this->output_end_tag('div'));
1439 return $this->output_start_tag('div', array('id' => $id,
1440 'class' => moodle_renderer_base::prepare_classes($classes)));
1441 }
1442
1443 public function container_end() {
1444 return $this->opencontainers->pop('container');
1445 }
a5cb8d69 1446
1447 /**
1448 * At the moment we frequently have a problem with $CFG->pixpath not being
1449 * initialised when it is needed. Unfortunately, there is no nice way to handle
1450 * this. I think we need to replace $CFG->pixpath with something like $OUTPUT->icon(...).
1451 * However, until then, we need a way to force $CFG->pixpath to be initialised,
1452 * to fix the error messages, and that is what this function if for.
1453 */
1454 public function initialise_deprecated_cfg_pixpath() {
1455 // Actually, we don't have to do anything here. Just calling any method
1456 // of $OBJECT is enough. However, if the only reason you are calling
1457 // an $OUTPUT method is to get $CFG->pixpath initialised, please use this
1458 // method, so we can find them and clean them up later once we have
1459 // found a better replacement for $CFG->pixpath.
1460 }
8954245a 1461}
1462
1463
1464/**
1465 * Base class for classes representing HTML elements, like moodle_select_menu.
1466 *
1467 * Handles the id and class attribues.
1468 *
1469 * @copyright 2009 Tim Hunt
1470 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1471 * @since Moodle 2.0
1472 */
1473class moodle_html_component {
1474 /**
1475 * @var string value to use for the id attribute of this HTML tag.
1476 */
1477 public $id = '';
1478 /**
1479 * @var array class names to add to this HTML element.
1480 */
1481 public $classes = array();
1482
1483 /**
1484 * Ensure some class names are an array.
1485 * @param mixed $classes either an array of class names or a space-separated
1486 * string containing class names.
1487 * @return array the class names as an array.
1488 */
1489 public static function clean_clases($classes) {
1490 if (is_array($classes)) {
1491 return $classes;
1492 } else {
a5cb8d69 1493 return explode(' ', trim($classes));
8954245a 1494 }
1495 }
1496
1497 /**
1498 * Set the class name array.
1499 * @param mixed $classes either an array of class names or a space-separated
1500 * string containing class names.
1501 */
1502 public function set_classes($classes) {
1503 $this->classes = self::clean_clases($classes);
1504 }
1505
1506 /**
1507 * Add a class name to the class names array.
1508 * @param string $class the new class name to add.
1509 */
1510 public function add_class($class) {
1511 $this->classes[] = $class;
1512 }
1513
1514 /**
1515 * Add a whole lot of class names to the class names array.
1516 * @param mixed $classes either an array of class names or a space-separated
1517 * string containing class names.
1518 */
1519 public function add_classes($classes) {
1520 $this->classes += self::clean_clases($classes);
1521 }
1522
1523 /**
1524 * Get the class names as a string.
1525 * @return string the class names as a space-separated string. Ready to be put in the class="" attribute.
1526 */
1527 public function get_classes_string() {
1528 return implode(' ', $this->classes);
1529 }
1530
1531 /**
1532 * Perform any cleanup or final processing that should be done before an
1533 * instance of this class is output.
1534 */
1535 public function prepare() {
1536 $this->classes = array_unique(self::clean_clases($this->classes));
1537 }
1538}
1539
1540
1541/**
1542 * This class hold all the information required to describe a <select> menu that
34a2777c 1543 * will be printed by {@link moodle_core_renderer::select_menu()}. (Or by an overridden
8954245a 1544 * version of that method in a subclass.)
1545 *
1546 * All the fields that are not set by the constructor have sensible defaults, so
1547 * you only need to set the properties where you want non-default behaviour.
1548 *
1549 * @copyright 2009 Tim Hunt
1550 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1551 * @since Moodle 2.0
1552 */
1553class moodle_select_menu extends moodle_html_component {
1554 /**
1555 * @var array the choices to show in the menu. An array $value => $label.
1556 */
1557 public $options;
1558 /**
1559 * @var string the name of this form control. That is, the name of the GET/POST
1560 * variable that will be set if this select is submmitted as part of a form.
1561 */
1562 public $name;
1563 /**
1564 * @var string the option to select initially. Should match one
1565 * of the $options array keys. Default none.
1566 */
1567 public $selectedvalue;
1568 /**
1569 * @var string The label for the 'nothing is selected' option.
1570 * Defaults to get_string('choosedots').
1571 * Set this to '' if you do not want a 'nothing is selected' option.
1572 */
1573 public $nothinglabel = null;
1574 /**
1575 * @var string The value returned by the 'nothing is selected' option. Defaults to 0.
1576 */
1577 public $nothingvalue = 0;
1578 /**
1579 * @var boolean set this to true if you want the control to appear disabled.
1580 */
1581 public $disabled = false;
1582 /**
1583 * @var integer if non-zero, sets the tabindex attribute on the <select> element. Default 0.
1584 */
1585 public $tabindex = 0;
1586 /**
1587 * @var mixed Defaults to false, which means display the select as a dropdown menu.
1588 * If true, display this select as a list box whose size is chosen automatically.
1589 * If an integer, display as list box of that size.
1590 */
1591 public $listbox = false;
1592 /**
1593 * @var integer if you are using $listbox === true to get an automatically
1594 * sized list box, the size of the list box will be the number of options,
1595 * or this number, whichever is smaller.
1596 */
1597 public $maxautosize = 10;
1598 /**
1599 * @var boolean if true, allow multiple selection. Only used if $listbox is true.
1600 */
1601 public $multiple = false;
1602 /**
1603 * @deprecated
1604 * @var string JavaScript to add as an onchange attribute. Do not use this.
1605 * Use the YUI even library instead.
1606 */
1607 public $script = '';
1608
1609 /* @see lib/moodle_html_component#prepare() */
1610 public function prepare() {
1611 if (empty($this->id)) {
1612 $this->id = 'menu' . str_replace(array('[', ']'), '', $this->name);
1613 }
1614 if (empty($this->classes)) {
1615 $this->set_classes(array('menu' . str_replace(array('[', ']'), '', $this->name)));
1616 }
1617 $this->add_class('select');
1618 parent::prepare();
1619 }
1620
1621 /**
1622 * This is a shortcut for making a simple select menu. It lets you specify
1623 * the options, name and selected option in one line of code.
1624 * @param array $options used to initialise {@link $options}.
1625 * @param string $name used to initialise {@link $name}.
1626 * @param string $selected used to initialise {@link $selected}.
1627 * @return moodle_select_menu A moodle_select_menu object with the three common fields initialised.
1628 */
1629 public static function make($options, $name, $selected = '') {
1630 $menu = new moodle_select_menu();
1631 $menu->options = $options;
1632 $menu->name = $name;
1633 $menu->selectedvalue = $selected;
1634 return $menu;
1635 }
1636
1637 /**
1638 * This is a shortcut for making a yes/no select menu.
1639 * @param string $name used to initialise {@link $name}.
1640 * @param string $selected used to initialise {@link $selected}.
1641 * @return moodle_select_menu A menu initialised with yes/no options.
1642 */
1643 public static function make_yes_no($name, $selected) {
1644 return self::make(array(0 => get_string('no'), 1 => get_string('yes')), $name, $selected);
1645 }
571fa828 1646}
1647
1648
a5cb8d69 1649/**
1650 * This class hold all the information required to describe a Moodle block.
1651 *
1652 * That is, it holds all the different bits of HTML content that need to be put into the block.
1653 *
1654 * @copyright 2009 Tim Hunt
1655 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1656 * @since Moodle 2.0
1657 */
1658class block_contents extends moodle_html_component {
1659 protected static $idcounter = 1;
1660 /**
1661 * @param string $heading HTML for the heading. Can include full HTML or just
1662 * plain text - plain text will automatically be enclosed in the appropriate
1663 * heading tags.
1664 */
1665 public $heading = '';
1666 /**
1667 * @param string $title Plain text title, as embedded in the $heading.
1668 */
1669 public $title = '';
1670 /**
1671 * @param string $content HTML for the content
1672 */
1673 public $content = '';
1674 /**
1675 * @param array $list an alternative to $content, it you want a list of things with optional icons.
1676 */
1677 public $list = array();
1678 /**
1679 * @param array $icons optional icons for the things in $list.
1680 */
1681 public $icons = array();
1682 /**
1683 * @param string $footer Extra HTML content that gets output at the end, inside a &lt;div class="footer">
1684 */
1685 public $footer = '';
1686 /**
1687 * @param array $attributes an array of attribute => value pairs that are put on the
1688 * outer div of this block. {@link $id} and {@link $classes} attributes should be set separately.
1689 */
1690 public $attributes = array();
1691 /**
1692 * @param integer $skipid do not set this manually. It is set automatically be the {@link prepare()} method.
1693 */
1694 public $skipid;
1695
1696 /* @see lib/moodle_html_component#prepare() */
1697 public function prepare() {
1698 $this->skipid = self::$idcounter;
1699 self::$idcounter += 1;
1700 $this->add_class('sideblock');
1701 parent::prepare();
1702 }
1703}
1704
1705
34a2777c 1706/**
1707 * A renderer that generates output for commandlines scripts.
1708 *
1709 * The implementation of this renderer is probably incomplete.
1710 *
1711 * @copyright 2009 Tim Hunt
1712 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1713 * @since Moodle 2.0
1714 */
1715class cli_core_renderer extends moodle_core_renderer {
1716 public function header() {
1717 output_starting_hook();
1718 return $this->page->heading . "\n";
1719 }
1720
1721 public function heading($text, $level, $classes = 'main', $id = '') {
1722 $text .= "\n";
1723 switch ($level) {
1724 case 1:
1725 return '=>' . $text;
1726 case 2:
1727 return '-->' . $text;
1728 default:
1729 return $text;
1730 }
1731 }
1732
1733 public function fatal_error($errorcode, $module, $a, $link, $backtrace,
1734 $debuginfo = null, $showerrordebugwarning = false) {
1735 $output = "!!! $message !!!\n";
1736
1737 if (debugging('', DEBUG_DEVELOPER)) {
1738 if (!empty($debuginfo)) {
1739 $this->notification($debuginfo, 'notifytiny');
1740 }
1741 if (!empty($backtrace)) {
1742 $this->notification('Stack trace: ' . format_backtrace($backtrace, true), 'notifytiny');
1743 }
1744 }
1745 }
1746
1747 public function notification($message, $classes = 'notifyproblem') {
1748 $message = clean_text($message);
1749 if ($style === 'notifysuccess') {
1750 return "++ $message ++\n";
1751 }
1752 return "!! $message !!\n";
1753 }
1754}
1755