From ebebf55cadeb187b1ad912706f0c382bbfa2b36b Mon Sep 17 00:00:00 2001 From: tjhunt Date: Thu, 2 Jul 2009 08:49:25 +0000 Subject: [PATCH] output: MDL-19690 icon_finder classes and $OUTPUT->mod/old_icon_url This is ready to replace $CFG->pixpath and $CFG->modpixpath soon. --- lib/outputlib.php | 2834 ++++++++++++++++-------------- lib/setup.php | 2 +- lib/simpletest/testoutputlib.php | 93 +- 3 files changed, 1602 insertions(+), 1327 deletions(-) diff --git a/lib/outputlib.php b/lib/outputlib.php index 61f1b303ae..3b4a933670 100644 --- a/lib/outputlib.php +++ b/lib/outputlib.php @@ -35,8 +35,8 @@ * Which renderer factory to use is chose by the current theme, and an instance * if created automatically when the theme is set up. * - * A renderer factory must also have a constructor that takes a theme object and - * a moodle_page object. (See {@link renderer_factory_base::__construct} for an example.) + * A renderer factory must also have a constructor that takes a theme_config object. + * (See {@link renderer_factory_base::__construct} for an example.) * * @copyright 2009 Tim Hunt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later @@ -57,1647 +57,1875 @@ interface renderer_factory { * 'Duck typing'. For a tricky example, see {@link template_renderer} below. * renderer ob * - * @param $module the name of part of moodle. E.g. 'core', 'quiz', 'qtype_multichoice'. + * @param string $component name such as 'core', 'mod_forum' or 'qtype_multichoice'. + * @param moodle_page $page the page the renderer is outputting content for. * @return object an object implementing the requested renderer interface. */ - public function get_renderer($module, $page); + public function get_renderer($component, $page); } /** - * This is a base class to help you implement the renderer_factory interface. + * An icon finder is responsible for working out the correct URL for an icon. * - * It keeps a cache of renderers that have been constructed, so you only need - * to construct each one once in you subclass. + * A icon finder must also have a constructor that takes a theme object. + * (See {@link standard_icon_finder::__construct} for an example.) * - * It also has a method to get the name of, and include the renderer.php with - * the definition of, the standard renderer class for a given module. + * Note that we are planning to change the Moodle icon naming convention before + * the Moodle 2.0 relase. Therefore, this API will probably change. * * @copyright 2009 Tim Hunt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.0 */ -abstract class renderer_factory_base implements renderer_factory { - /** @var theme_config the theme we belong to. */ - protected $theme; - +interface icon_finder { /** - * Constructor. - * @param theme_config $theme the theme we belong to. + * Return the URL for an icon indentifed as in pre-Moodle 2.0 code. + * + * Suppose you have old code like $url = "$CFG->pixpath/i/course.gif"; + * then old_icon_url('i/course'); will return the equivalent URL that is correct now. + * + * @param $iconname the name of the icon. + * @return string the URL for that icon. */ - public function __construct($theme) { - $this->theme = $theme; - } + public function old_icon_url($iconname); + /** - * For a given module name, return the name of the standard renderer class - * that defines the renderer interface for that module. + * Return the URL for an icon indentifed as in pre-Moodle 2.0 code. * - * Also, if it exists, include the renderer.php file for that module, so - * the class definition of the default renderer has been loaded. + * Suppose you have old code like $url = "$CFG->modpixpath/$mod/icon.gif"; + * then mod_icon_url('icon', $mod); will return the equivalent URL that is correct now. * - * @param string $module the name of part of moodle. E.g. 'core', 'quiz', 'qtype_multichoice'. - * @return string the name of the standard renderer class for that module. + * @param $iconname the name of the icon. + * @param $module the module the icon belongs to. + * @return string the URL for that icon. */ - protected function standard_renderer_class_for_module($module) { - $pluginrenderer = get_plugin_dir($module) . '/renderer.php'; - if (file_exists($pluginrenderer)) { - include_once($pluginrenderer); - } - $class = 'moodle_' . $module . '_renderer'; - if (!class_exists($class)) { - throw new coding_exception('Request for an unknown renderer class ' . $class); - } - return $class; - } + public function mod_icon_url($iconname, $module); } /** - * This is the default renderer factory for Moodle. It simply returns an instance - * of the appropriate standard renderer class. + *This class represents the configuration variables of a Moodle theme. + * + * Normally, to create an instance of this class, you should use the + * {@link theme_config::load()} factory method to load a themes config.php file. * * @copyright 2009 Tim Hunt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.0 */ -class standard_renderer_factory extends renderer_factory_base { - /* Implement the subclass method. */ - public function get_renderer($module, $page) { - if ($module == 'core') { - return new moodle_core_renderer($page); - } else { - $class = $this->standard_renderer_class_for_module($module); - return new $class($page, $this->get_renderer('core', $page)); - } - } -} +class theme_config { + /** + * @var array The names of all the stylesheets from this theme that you would + * like included, in order. + */ + public $sheets = array('styles_layout', 'styles_fonts', 'styles_color'); + public $standardsheets = true; -/** - * This is a slight variation on the standard_renderer_factory used by CLI scripts. - * - * @copyright 2009 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.0 - */ -class cli_renderer_factory extends standard_renderer_factory { - /* Implement the subclass method. */ - public function get_renderer($module, $page) { - if ($module == 'core') { - return new cli_core_renderer($page); - } else { - parent::get_renderer($module, $page); - } - } -} +/// This variable can be set to an array containing +/// filenames from the *STANDARD* theme. If the +/// array exists, it will be used to choose the +/// files to include in the standard style sheet. +/// When false, then no files are used. +/// When true or NON-EXISTENT, then ALL standard files are used. +/// This parameter can be used, for example, to prevent +/// having to override too many classes. +/// Note that the trailing .css should not be included +/// eg $THEME->standardsheets = array('styles_layout','styles_fonts','styles_color'); +//////////////////////////////////////////////////////////////////////////////// -/** - * This is renderer factory allows themes to override the standard renderers using - * php code. - * - * It will load any code from theme/mytheme/renderers.php and - * theme/parenttheme/renderers.php, if then exist. Then whenever you ask for - * a renderer for 'component', it will create a mytheme_component_renderer or a - * parenttheme_component_renderer, instead of a moodle_component_renderer, - * if either of those classes exist. - * - * This generates the slightly different HTML that the custom_corners theme expects. - * - * @copyright 2009 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.0 - */ -class theme_overridden_renderer_factory extends standard_renderer_factory { - protected $prefixes = array(); + public $parent = null; - /** - * Constructor. - * @param object $theme the theme we are rendering for. - * @param moodle_page $page the page we are doing output for. - */ - public function __construct($theme) { - global $CFG; - parent::__construct($theme); +/// This variable can be set to the name of a parent theme +/// which you want to have included before the current theme. +/// This can make it easy to make modifications to another +/// theme without having to actually change the files +/// If this variable is empty or false then a parent theme +/// is not used. +//////////////////////////////////////////////////////////////////////////////// - // Initialise $this->prefixes. - $renderersfile = $theme->dir . '/renderers.php'; - if (is_readable($renderersfile)) { - include_once($renderersfile); - $this->prefixes[] = $theme->name . '_'; - } - if (!empty($theme->parent)) { - $renderersfile = $CFG->themedir .'/'. $theme->parent . '/renderers.php'; - if (is_readable($renderersfile)) { - include_once($renderersfile); - $this->prefixes[] = $theme->parent . '_'; - } - } - } - /* Implement the subclass method. */ - public function get_renderer($module, $page) { - foreach ($this->prefixes as $prefix) { - $classname = $prefix . $module . '_renderer'; - if (class_exists($classname)) { - if ($module == 'core') { - return new $classname($page); - } else { - return new $classname($page, $this->get_renderer('core', $page)); - } - } - } - return parent::get_renderer($module, $page); - } -} + public $parentsheets = false; +/// This variable can be set to an array containing +/// filenames from a chosen *PARENT* theme. If the +/// array exists, it will be used to choose the +/// files to include in the standard style sheet. +/// When false, then no files are used. +/// When true or NON-EXISTENT, then ALL standard files are used. +/// This parameter can be used, for example, to prevent +/// having to override too many classes. +/// Note that the trailing .css should not be included +/// eg $THEME->parentsheets = array('styles_layout','styles_fonts','styles_color'); +//////////////////////////////////////////////////////////////////////////////// -/** - * This is renderer factory that allows you to create templated themes. - * - * This should be considered an experimental proof of concept. In particular, - * the performance is probably not very good. Do not try to use in on a busy site - * without doing careful load testing first! - * - * This renderer factory returns instances of {@link template_renderer} class - * which which implement the corresponding renderer interface in terms of - * templates. To use this your theme must have a templates folder inside it. - * Then suppose the method moodle_core_renderer::greeting($name = 'world'); - * exists. Then, a call to $OUTPUT->greeting() will cause the template - * /theme/yourtheme/templates/core/greeting.php to be rendered, with the variable - * $name available. The greeting.php template might contain - * - *
- * 

Hello !

- *
- * - * @copyright 2009 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.0 - */ -class template_renderer_factory extends renderer_factory_base { - /** - * An array of paths of where to search for templates. Normally this theme, - * the parent theme then the standardtemplate theme. (If some of these do - * not exist, or are the same as each other, then the list will be shorter. - */ - protected $searchpaths = array(); - /** - * Constructor. - * @param object $theme the theme we are rendering for. - * @param moodle_page $page the page we are doing output for. - */ - public function __construct($theme) { - global $CFG; - parent::__construct($theme); + public $modsheets = true; - // Initialise $this->searchpaths. - if ($theme->name != 'standardtemplate') { - $templatesdir = $theme->dir . '/templates'; - if (is_dir($templatesdir)) { - $this->searchpaths[] = $templatesdir; - } - } - if (!empty($theme->parent)) { - $templatesdir = $CFG->themedir .'/'. $theme->parent . '/templates'; - if (is_dir($templatesdir)) { - $this->searchpaths[] = $templatesdir; - } - } - $this->searchpaths[] = $CFG->themedir .'/standardtemplate/templates'; - } +/// When this is enabled, then this theme will search for +/// files named "styles.php" inside all Activity modules and +/// include them. This allows modules to provide some basic +/// layouts so they work out of the box. +/// It is HIGHLY recommended to leave this enabled. - /* Implement the subclass method. */ - public function get_renderer($module, $page) { - // Refine the list of search paths for this module. - $searchpaths = array(); - foreach ($this->searchpaths as $rootpath) { - $path = $rootpath . '/' . $module; - if (is_dir($path)) { - $searchpaths[] = $path; - } - } - // Create a template_renderer that copies the API of the standard renderer. - $copiedclass = $this->standard_renderer_class_for_module($module); - return new template_renderer($copiedclass, $searchpaths, $page); - } -} + public $blocksheets = true; +/// When this is enabled, then this theme will search for +/// files named "styles.php" inside all Block modules and +/// include them. This allows Blocks to provide some basic +/// layouts so they work out of the box. +/// It is HIGHLY recommended to leave this enabled. -/** - * 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 - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.0 - */ -class moodle_renderer_base { - /** @var xhtml_container_stack the xhtml_container_stack to use. */ - protected $opencontainers; - /** @var moodle_page the page we are rendering for. */ - protected $page; - /** - * Constructor - * @param $opencontainers the xhtml_container_stack to use. - * @param moodle_page $page the page we are doing output for. - */ - public function __construct($page) { - $this->opencontainers = $page->opencontainers; - $this->page = $page; - } + public $langsheets = false; - /** - * Have we started output yet? - * @return boolean true if the header has been printed. - */ - public function has_started() { - return $this->page->state >= moodle_page::STATE_IN_BODY; - } +/// By setting this to true, then this theme will search for +/// a file named "styles.php" inside the current language +/// directory. This allows different languages to provide +/// different styles. - protected function output_tag($tagname, $attributes, $contents) { - return $this->output_start_tag($tagname, $attributes) . $contents . - $this->output_end_tag($tagname); - } - protected function output_start_tag($tagname, $attributes) { - return '<' . $tagname . $this->output_attributes($attributes) . '>'; - } - protected function output_end_tag($tagname) { - return ''; - } - protected function output_empty_tag($tagname, $attributes) { - return '<' . $tagname . $this->output_attributes($attributes) . ' />'; - } - protected function output_attribute($name, $value) { - $value = trim($value); - if ($value || is_numeric($value)) { // We want 0 to be output. - return ' ' . $name . '="' . $value . '"'; - } - } - protected function output_attributes($attributes) { - if (empty($attributes)) { - $attributes = array(); - } - $output = ''; - foreach ($attributes as $name => $value) { - $output .= $this->output_attribute($name, $value); - } - return $output; - } - public static function prepare_classes($classes) { - if (is_array($classes)) { - return implode(' ', array_unique($classes)); - } - return $classes; - } -} + public $courseformatsheets = true; +/// When this is enabled, this theme will search for files +/// named "styles.php" inside all course formats and +/// include them. This allows course formats to provide +/// their own default styles. -/** - * This is the templated renderer which copies the API of another class, replacing - * all methods calls with instantiation of a template. - * - * When the method method_name is called, this class will search for a template - * called method_name.php in the folders in $searchpaths, taking the first one - * that it finds. Then it will set up variables for each of the arguments of that - * method, and render the template. This is implemented in the {@link __call()} - * PHP magic method. - * - * Methods like print_box_start and print_box_end are handles specially, and - * implemented in terms of the print_box.php method. - * - * @copyright 2009 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.0 - */ -class template_renderer extends moodle_renderer_base { - /** @var ReflectionClass information about the class whose API we are copying. */ - protected $copiedclass; - /** @var array of places to search for templates. */ - protected $searchpaths; - protected $rendererfactory; - /** - * Magic word used when breaking apart container templates to implement - * _start and _end methods. - */ - const contentstoken = '-@#-Contents-go-here-#@-'; + public $metainclude = false; - /** - * Constructor - * @param string $copiedclass the name of a class whose API we should be copying. - * @param $searchpaths a list of folders to search for templates in. - * @param $opencontainers the xhtml_container_stack to use. - * @param moodle_page $page the page we are doing output for. - */ - public function __construct($copiedclass, $searchpaths, $page) { - parent::__construct($page); - $this->copiedclass = new ReflectionClass($copiedclass); - $this->searchpaths = $searchpaths; - } +/// When this is enabled (or not set!) then Moodle will try +/// to include a file meta.php from this theme into the +/// part of the page. - /* PHP magic method implementation. */ - public function __call($method, $arguments) { - if (substr($method, -6) == '_start') { - return $this->process_start(substr($method, 0, -6), $arguments); - } else if (substr($method, -4) == '_end') { - return $this->process_end(substr($method, 0, -4), $arguments); - } else { - return $this->process_template($method, $arguments); - } - } - /** - * Render the template for a given method of the renderer class we are copying, - * using the arguments passed. - * @param string $method the method that was called. - * @param array $arguments the arguments that were passed to it. - * @return string the HTML to be output. - */ - protected function process_template($method, $arguments) { - if (!$this->copiedclass->hasMethod($method) || - !$this->copiedclass->getMethod($method)->isPublic()) { - throw new coding_exception('Unknown method ' . $method); - } + public $standardmetainclude = true; - // Find the template file for this method. - $template = $this->find_template($method); - // Use the reflection API to find out what variable names the arguments - // should be stored in, and fill in any missing ones with the defaults. - $namedarguments = array(); - $expectedparams = $this->copiedclass->getMethod($method)->getParameters(); - foreach ($expectedparams as $param) { - $paramname = $param->getName(); - if (!empty($arguments)) { - $namedarguments[$paramname] = array_shift($arguments); - } else if ($param->isDefaultValueAvailable()) { - $namedarguments[$paramname] = $param->getDefaultValue(); - } else { - throw new coding_exception('Missing required argument ' . $paramname); - } - } +/// When this is enabled (or not set!) then Moodle will try +/// to include a file meta.php from the standard theme into the +/// part of the page. - // Actually render the template. - return $this->render_template($template, $namedarguments); - } - /** - * Actually do the work of rendering the template. - * @param $_template the full path to the template file. - * @param $_namedarguments an array variable name => value, the variables - * that should be available to the template. - * @return string the HTML to be output. - */ - protected function render_template($_template, $_namedarguments) { - // Note, we intentionally break the coding guidelines with regards to - // local variable names used in this function, so that they do not clash - // with the names of any variables being passed to the template. + public $parentmetainclude = false; - global $CFG, $SITE, $THEME, $USER; - // 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 the same as - // the global $OUTPUT object. When rendering the template, we want to use - // this object. However, people writing Moodle code expect the current - // rederer 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; +/// When this is enabled (or not set!) then Moodle will try +/// to include a file meta.php from the parent theme into the +/// part of the page. - // And the parameters from the function call. - extract($_namedarguments); - // Include the template, capturing the output. - ob_start(); - include($_template); - $_result = ob_get_contents(); - ob_end_clean(); + public $navmenuwidth = 50; - return $_result; - } +/// You can use this to control the cutoff point for strings +/// in the navmenus (list of activities in popup menu etc) +/// Default is 50 characters wide. - /** - * Searches the folders in {@link $searchpaths} to try to find a template for - * this method name. Throws an exception if one cannot be found. - * @param string $method the method name. - * @return string the full path of the template to use. - */ - protected function find_template($method) { - foreach ($this->searchpaths as $path) { - $filename = $path . '/' . $method . '.php'; - if (file_exists($filename)) { - return $filename; - } - } - throw new coding_exception('Cannot find template for ' . $this->copiedclass->getName() . '::' . $method); - } - /** - * Handle methods like print_box_start by using the print_box template, - * splitting the result, pusing the end onto the stack, then returning the start. - * @param string $method the method that was called, with _start stripped off. - * @param array $arguments the arguments that were passed to it. - * @return string the HTML to be output. - */ - protected function process_start($template, $arguments) { - array_unshift($arguments, self::contentstoken); - $html = $this->process_template($template, $arguments); - list($start, $end) = explode(self::contentstoken, $html, 2); - $this->opencontainers->push($template, $end); - return $start; - } + public $makenavmenulist = false; - /** - * Handle methods like print_box_end, we just need to pop the end HTML from - * the stack. - * @param string $method the method that was called, with _end stripped off. - * @param array $arguments not used. Assumed to be irrelevant. - * @return string the HTML to be output. - */ - protected function process_end($template, $arguments) { - return $this->opencontainers->pop($template); - } +/// By setting this to true, then you will have access to a +/// new variable in your header.html and footer.html called +/// $navmenulist ... this contains a simple XHTML menu of +/// all activities in the current course, mostly useful for +/// creating popup navigation menus and so on. - /** - * @return array the list of paths where this class searches for templates. - */ - public function get_search_paths() { - return $this->searchpaths; - } - /** - * @return string the name of the class whose API we are copying. - */ - public function get_copied_class() { - return $this->copiedclass->getName(); - } -} + public $resource_mp3player_colors = 'bgColour=000000&btnColour=ffffff&btnBorderColour=cccccc&iconColour=000000&iconOverColour=00cc00&trackColour=cccccc&handleColour=ffffff&loaderColour=ffffff&font=Arial&fontColour=3333FF&buffer=10&waitForPlay=no&autoPlay=yes'; -/** - * This class keeps track of which HTML tags are currently open. - * - * This makes it much easier to always generate well formed XHTML output, even - * if execution terminates abruptly. Any time you output some opening HTML - * without the matching closing HTML, you should push the neccessary close tags - * onto the stack. - * - * @copyright 2009 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.0 - */ -class xhtml_container_stack { - /** @var array stores the list of open containers. */ - protected $opencontainers = array(); +/// With this you can control the colours of the "big" MP3 player +/// that is used for MP3 resources. + + + public $filter_mediaplugin_colors = 'bgColour=000000&btnColour=ffffff&btnBorderColour=cccccc&iconColour=000000&iconOverColour=00cc00&trackColour=cccccc&handleColour=ffffff&loaderColour=ffffff&waitForPlay=yes'; + +/// ...And this controls the small embedded player + + + public $custompix = false; + +/// If true, then this theme must have a "pix" +/// subdirectory that contains copies of all +/// files from the moodle/pix directory, plus a +/// "pix/mod" directory containing all the icons +/// for all the activity modules. + + +///$THEME->rarrow = '►' //OR '→'; +///$THEME->larrow = '◄' //OR '←'; +///$CFG->block_search_button = link_arrow_right(get_string('search'), $url='', $accesshide=true); +/// +/// Accessibility: Right and left arrow-like characters are +/// used in the breadcrumb trail, course navigation menu +/// (previous/next activity), calendar, and search forum block. +/// +/// If the theme does not set characters, appropriate defaults +/// are set by (lib/weblib.php:check_theme_arrows). The suggestions +/// above are 'silent' in a screen-reader like JAWS. Please DO NOT +/// use < > » - these are confusing for blind users. +//////////////////////////////////////////////////////////////////////////////// + + + public $blockregions = array('side-pre', 'side-post'); + public $defaultblockregion = 'side-post'; +/// Areas where blocks may appear on any page that uses this theme. For each +/// region you list in $THEME->blockregions you must call blocks_print_group +/// with that region id somewhere in header.html or footer.html. +/// defaultblockregion is the region where new blocks will be added, and +/// where any blocks in unrecognised regions will be shown. (Suppose someone +/// added a block when anther theme was selected). +//////////////////////////////////////////////////////////////////////////////// + + /** @var string the name of this theme. Set automatically. */ + public $name; + /** @var string the folder where this themes fiels are stored. $CFG->themedir . '/' . $this->name */ + public $dir; + + /** @var string Name of the renderer factory class to use. */ + public $rendererfactory = 'standard_renderer_factory'; + /** @var renderer_factory Instance of the renderer_factory class. */ + protected $rf = null; + + /** @var string Name of the icon finder class to use. */ + public $iconfinder = 'pix_icon_finder'; + /** @var renderer_factory Instance of the renderer_factory class. */ + protected $if = null; /** - * Push the close HTML for a recently opened container onto the stack. - * @param string $type The type of container. This is checked when {@link pop()} - * is called and must match, otherwise a developer debug warning is output. - * @param string $closehtml The HTML required to close the container. + * If you want to do custom processing on the CSS before it is output (for + * example, to replace certain variable names with particular values) you can + * give the name of a function here. + * + * There are two functions avaiable that you may wish to use (defined in lib/outputlib.php): + * output_css_replacing_constants + * output_css_for_css_edit + * If you wish to write your own function, use those two as examples, and it + * should be clear what you have to do. + * + * @var string the name of a function. */ - public function push($type, $closehtml) { - $container = new stdClass; - $container->type = $type; - $container->closehtml = $closehtml; - array_push($this->opencontainers, $container); - } + public $customcssoutputfunction = null; /** - * Pop the HTML for the next closing container from the stack. The $type - * must match the type passed when the container was opened, otherwise a - * warning will be output. - * @param string $type The type of container. - * @return string the HTML requried to close the container. + * Load the config.php file for a particular theme, and return an instance + * of this class. (That is, this is a factory method.) + * + * @param string $themename the name of the theme. + * @return theme_config an instance of this class. */ - public function pop($type) { - if (empty($this->opencontainers)) { - debugging('There are no more open containers. This suggests there is a nesting problem.', DEBUG_DEVELOPER); - return; - } + public static function load($themename) { + global $CFG; - $container = array_pop($this->opencontainers); - if ($container->type != $type) { - debugging('The type of container to be closed (' . $container->type . - ') does not match the type of the next open container (' . $type . - '). This suggests there is a nesting problem.', DEBUG_DEVELOPER); + // We have to use the variable name $THEME (upper case) becuase that + // is what is used in theme config.php files. + + // Set some other standard properties of the theme. + $THEME = new theme_config; + $THEME->name = $themename; + $THEME->dir = $CFG->themedir . '/' . $themename; + + // Load up the theme config + $configfile = $THEME->dir . '/config.php'; + if (!is_readable($configfile)) { + throw new coding_exception('Cannot use theme ' . $themename . + '. The file ' . $configfile . ' does not exist or is not readable.'); } - return $container->closehtml; + include($configfile); + + $THEME->update_legacy_information(); + + return $THEME; } /** - * Return how many containers are currently open. - * @return integer how many containers are currently open. + * Get the renderer for a part of Moodle for this theme. + * @param string $module the name of part of moodle. E.g. 'core', 'quiz', 'qtype_multichoice'. + * @param moodle_page $page the page we are rendering + * @return moodle_renderer_base the requested renderer. */ - public function count() { - return count($this->opencontainers); + public function get_renderer($module, $page) { + if (is_null($this->rf)) { + if (CLI_SCRIPT) { + $classname = 'cli_renderer_factory'; + } else { + $classname = $this->rendererfactory; + } + $this->rf = new $classname($this); + } + + return $this->rf->get_renderer($module, $page); } /** - * 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 ) - * before outputting the error message. - * @return string the HTML requried to close any open containers inside . + * Get the renderer for a part of Moodle for this theme. + * @return moodle_renderer_base the requested renderer. */ - public function pop_all_but_last() { - $output = ''; - while (count($this->opencontainers) > 1) { - $container = array_pop($this->opencontainers); - $output .= $container->closehtml; + protected function get_icon_finder() { + if (is_null($this->if)) { + $classname = $this->iconfinder; + $this->if = new $classname($this); } - return $output; + return $this->if; } /** - * You can call this function if you want to throw away an instance of this - * class without properly emptying the stack (for example, in a unit test). - * Calling this method stops the destruct method from outputting a developer - * debug warning. After calling this method, the instance can no longer be used. + * Return the URL for an icon indentifed as in pre-Moodle 2.0 code. + * + * Suppose you have old code like $url = "$CFG->pixpath/i/course.gif"; + * then old_icon_url('i/course'); will return the equivalent URL that is correct now. + * + * @param $iconname the name of the icon. + * @return string the URL for that icon. */ - public function discard() { - $this->opencontainers = null; + public function old_icon_url($iconname) { + return $this->if->old_icon_url($iconname); } /** - * Emergency fallback. If we get to the end of processing and not all - * containers have been closed, output the rest with a developer debug warning. + * Return the URL for an icon indentifed as in pre-Moodle 2.0 code. + * + * Suppose you have old code like $url = "$CFG->modpixpath/$mod/icon.gif"; + * then mod_icon_url('icon', $mod); will return the equivalent URL that is correct now. + * + * @param $iconname the name of the icon. + * @param $module the module the icon belongs to. + * @return string the URL for that icon. */ - public function __destruct() { - if (empty($this->opencontainers)) { - return; - } - - debugging('Some containers were left open. This suggests there is a nesting problem.', DEBUG_DEVELOPER); - echo $this->pop_all_but_last(); - $container = array_pop($this->opencontainers); - echo $container->closehtml; + public function mod_icon_url($iconname, $module) { + return $this->if->mod_icon_url($iconname, $module); } -} - - -/** - * The standard implementation of the moodle_core_renderer interface. - * - * @copyright 2009 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.0 - */ -class moodle_core_renderer extends moodle_renderer_base { - const PERFORMANCE_INFO_TOKEN = '%%PERFORMANCEINFO%%'; - const END_HTML_TOKEN = '%%ENDHTML%%'; - const MAIN_CONTENT_TOKEN = '[MAIN CONTENT GOES HERE]'; - protected $contenttype; - protected $metarefreshtag = ''; - public function doctype() { + /** + * Get the list of stylesheet URLs that need to go in the header for this theme. + * @return array of URLs. + */ + public function get_stylesheet_urls() { global $CFG; - $doctype = '' . "\n"; - $this->contenttype = 'text/html; charset=utf-8'; + // Put together the parameters + $params = '?for=' . $this->name; - if (empty($CFG->xmlstrictheaders)) { - return $doctype; + // Stylesheets, in order (standard, parent, this - some of which may be the same). + $stylesheets = array(); + if ($this->name != 'standard' && $this->standardsheets) { + $stylesheets[] = $CFG->httpsthemewww . '/standard/styles.php' . $params; + } + if (!empty($this->parent)) { + $stylesheets[] = $CFG->httpsthemewww . '/' . $this->parent . '/styles.php' . $params; } - // We want to serve the page with an XML content type, to force well-formedness errors to be reported. - $prolog = '' . "\n"; - if (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/xhtml+xml') !== false) { - // Firefox and other browsers that can cope natively with XHTML. - $this->contenttype = 'application/xhtml+xml; charset=utf-8'; - - } else if (preg_match('/MSIE.*Windows NT/', $_SERVER['HTTP_USER_AGENT'])) { - // IE can't cope with application/xhtml+xml, but it will cope if we send application/xml with an XSL stylesheet. - $this->contenttype = 'application/xml; charset=utf-8'; - $prolog .= 'httpswwwroot . '/lib/xhtml.xsl"?>' . "\n"; - - } else { - $prolog = ''; + // Pass on the current language, if it will be needed. + if (!empty($this->langsheets)) { + $params .= '&lang=' . current_language(); } + $stylesheets[] = $CFG->httpsthemewww . '/' . $this->name . '/styles.php' . $params; - return $prolog . $doctype; - } + // Additional styles for right-to-left languages. + if (right_to_left()) { + $stylesheets[] = $CFG->httpsthemewww . '/standard/rtl.css'; - public function htmlattributes() { - return get_html_lang(true) . ' xmlns="http://www.w3.org/1999/xhtml"'; - } + if (!empty($this->parent) && file_exists($CFG->themedir . '/' . $this->parent . '/rtl.css')) { + $stylesheets[] = $CFG->httpsthemewww . '/' . $this->parent . '/rtl.css'; + } - public function standard_head_html() { - global $CFG, $THEME; - $output = ''; - $output .= '' . "\n"; - $output .= '' . "\n"; - if (!$this->page->cacheable) { - $output .= '' . "\n"; - $output .= '' . "\n"; + if (file_exists($this->dir . '/rtl.css')) { + $stylesheets[] = $CFG->httpsthemewww . '/' . $this->name . '/rtl.css'; + } } - // This is only set by the {@link redirect()} method - $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) { - $metarefesh = ''; - $output .= sprintf($metarefesh, $this->page->periodicrefreshdelay, $this->page->url->out()); - } + return $stylesheets; + } - // TODO get rid of $CFG->javascript. We should be able to do everything - // with $PAGE->requires. - ob_start(); - include($CFG->javascript); - $output .= ob_get_contents(); - ob_end_clean(); - $output .= $this->page->requires->get_head_code(); - - // List alternate versions. - foreach ($this->page->alternateversions as $type => $alt) { - $output .= $this->output_empty_tag('link', array('rel' => 'alternate', - 'type' => $type, 'title' => $alt->title, 'href' => $alt->url)); + /** + * This methon looks a the settings that have been loaded, to see whether + * any legacy things are being used, and outputs warning and tries to update + * things to use equivalent newer settings. + */ + protected function update_legacy_information() { + global $CFG; + if (!empty($this->customcorners)) { + // $THEME->customcorners is deprecated but we provide support for it via the + // custom_corners_renderer_factory class in lib/deprecatedlib.php + debugging('$THEME->customcorners is deprecated. Please use the new $THEME->rendererfactory ' . + 'to control HTML generation. Please use $this->rendererfactory = \'custom_corners_renderer_factory\'; ' . + 'in your config.php file instead.', DEBUG_DEVELOPER); + $this->rendererfactory = 'custom_corners_renderer_factory'; } - // Add the meta page from the themes if any were requested - // TODO See if we can get rid of this. - $PAGE = $this->page; - $metapage = ''; - if (!isset($THEME->standardmetainclude) || $THEME->standardmetainclude) { - ob_start(); - include_once($CFG->dirroot.'/theme/standard/meta.php'); - $output .= ob_get_contents(); - ob_end_clean(); - } - if ($THEME->parent && (!isset($THEME->parentmetainclude) || $THEME->parentmetainclude)) { - if (file_exists($CFG->dirroot.'/theme/'.$THEME->parent.'/meta.php')) { - ob_start(); - include_once($CFG->dirroot.'/theme/'.$THEME->parent.'/meta.php'); - $output .= ob_get_contents(); - ob_end_clean(); - } + if (!empty($this->cssconstants)) { + debugging('$THEME->cssconstants is deprecated. Please use ' . + '$THEME->customcssoutputfunction = \'output_css_replacing_constants\'; ' . + 'in your config.php file instead.', DEBUG_DEVELOPER); + $this->customcssoutputfunction = 'output_css_replacing_constants'; } - if (!isset($THEME->metainclude) || $THEME->metainclude) { - if (file_exists($CFG->dirroot.'/theme/'.current_theme().'/meta.php')) { - ob_start(); - include_once($CFG->dirroot.'/theme/'.current_theme().'/meta.php'); - $output .= ob_get_contents(); - ob_end_clean(); - } + + if (!empty($this->CSSEdit)) { + debugging('$THEME->CSSEdit is deprecated. Please use ' . + '$THEME->customcssoutputfunction = \'output_css_for_css_edit\'; ' . + 'in your config.php file instead.', DEBUG_DEVELOPER); + $this->customcssoutputfunction = 'output_css_for_css_edit'; } - return $output; + if ($CFG->smartpix) { + $this->iconfinder = 'smartpix_icon_finder'; + } else if ($this->custompix) { + $this->iconfinder = 'theme_icon_finder'; + } } - public function standard_top_of_body_html() { - return $this->page->requires->get_top_of_body_code(); - } + /** + * Set the variable $CFG->pixpath and $CFG->modpixpath to be the right + * ones for this theme. + */ + public function setup_cfg_paths() { + global $CFG; + if (!empty($CFG->smartpix)) { + if ($CFG->slasharguments) { + // Use this method if possible for better caching + $extra = ''; + } else { + $extra = '?file='; + } + $CFG->pixpath = $CFG->httpswwwroot . '/pix/smartpix.php' . $extra . '/' . $this->name; + $CFG->modpixpath = $CFG->httpswwwroot . '/pix/smartpix.php' . $extra . '/' . $this->name . '/mod'; - public function standard_footer_html() { - $output = self::PERFORMANCE_INFO_TOKEN; - if (debugging()) { - $output .= '
'; + } else if (empty($THEME->custompix)) { + $CFG->pixpath = $CFG->httpswwwroot . '/pix'; + $CFG->modpixpath = $CFG->httpswwwroot . '/mod'; + + } else { + $CFG->pixpath = $CFG->httpsthemewww . '/' . $this->name . '/pix'; + $CFG->modpixpath = $CFG->httpsthemewww . '/' . $this->name . '/pix/mod'; } - return $output; } +} - public function standard_end_of_body_html() { - echo self::END_HTML_TOKEN; + +/** + * This icon finder implements the old scheme that was used when themes that had + * $THEME->custompix = false. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 + */ +class pix_icon_finder implements icon_finder { + /** + * Constructor + * @param theme_config $theme the theme we are finding icons for (which is irrelevant). + */ + public function __construct($theme) { } - public function login_info() { - global $USER; - return user_login_string($this->page->course, $USER); + /* Implement interface method. */ + public function old_icon_url($iconname) { + global $CFG; + return $CFG->httpswwwroot . '/pix/' . $iconname . '.gif'; } - public function home_link() { - global $CFG, $SITE; + /* Implement interface method. */ + public function mod_icon_url($iconname, $module) { + global $CFG; + return $CFG->httpswwwroot . '/mod/' . $module . '/' . $iconname . '.gif'; + } +} - if ($this->page->pagetype == 'site-index') { - // Special case for site home page - please do not remove - return ''; - } else if (!empty($CFG->target_release) && $CFG->target_release != $CFG->release) { - // Special case for during install/upgrade. - return ''; +/** + * This icon finder implements the old scheme that was used for themes that had + * $THEME->custompix = true. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 + */ +class theme_icon_finder implements icon_finder { + protected $themename; + /** + * Constructor + * @param theme_config $theme the theme we are finding icons for. + */ + public function __construct($theme) { + $this->themename = $theme->name; + } - } else if ($this->page->course->id == $SITE->id || strpos($this->page->pagetype, 'course-view') === 0) { - return ''; + /* Implement interface method. */ + public function old_icon_url($iconname) { + global $CFG; + return $CFG->httpsthemewww . '/' . $this->themename . '/pix/' . $iconname . '.gif'; + } - } else { - return ''; - } + /* Implement interface method. */ + public function mod_icon_url($iconname, $module) { + global $CFG; + return $CFG->httpsthemewww . '/' . $this->themename . '/pix/mod/' . $module . '/' . $iconname . '.gif'; } +} + + +/** + * This icon finder implements the algorithm in pix/smartpix.php. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 + */ +class smartpix_icon_finder extends pix_icon_finder { + protected $places = array(); /** - * 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 string $messageclass The css class to put on the message that is - * being displayed to the user - * @return string The HTML to display to the user before dying, may contain - * meta refresh, javascript refresh, and may have set header redirects + * Constructor + * @param theme_config $theme the theme we are finding icons for. */ - public function redirect($encodedurl, $message, $delay, $messageclass='notifyproblem') { + public function __construct($theme) { global $CFG; - $url = str_replace('&', '&', $encodedurl); - - $disableredirect = false; + $this->places[$CFG->themedir . '/' . $theme->name . '/pix/'] = + $CFG->httpsthemewww . '/' . $theme->name . '/pix/'; + if (!empty($theme->parent)) { + $this->places[$CFG->themedir . '/' . $theme->parent . '/pix/'] = + $CFG->httpsthemewww . '/' . $theme->parent . '/pix/'; + } + } - if ($delay!=0) { - /// At developer debug level. Don't redirect if errors have been printed on screen. - /// Currenly only works in PHP 5.2+; we do not want strict PHP5 errors - $lasterror = error_get_last(); - $error = defined('DEBUGGING_PRINTED') or (!empty($lasterror) && ($lasterror['type'] & DEBUG_DEVELOPER)); - $errorprinted = debugging('', DEBUG_ALL) && $CFG->debugdisplay && $error; - if ($errorprinted) { - $disableredirect= true; - $message = "Error output, so disabling automatic redirect.

" . $message; + /* Implement interface method. */ + public function old_icon_url($iconname) { + foreach ($this->places as $dirroot => $urlroot) { + if (file_exists($dirroot . $iconname . '.gif')) { + return $iconname . $iconname . '.gif'; } } + return parent::old_icon_url($iconname); + } - switch ($this->page->state) { - case moodle_page::STATE_BEFORE_HEADER : - // No output yet it is safe to delivery the full arsenol of redirect methods - if (!$disableredirect) { - @header($_SERVER['SERVER_PROTOCOL'] . ' 303 See Other'); //302 might not work for POST requests, 303 is ignored by obsolete clients - @header('Location: '.$url); - $this->metarefreshtag = ''."\n"; - $this->page->requires->js_function_call('document.location.replace', array($url))->after_delay($delay+3); - } - $output = $this->header(); - $output .= $this->notification($message, $messageclass); - $output .= $this->footer(); - 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 (!$disableredirect) { - $this->page->requires->js_function_call('document.location.replace', array($url))->after_delay($delay+3); - } - $output = $this->opencontainers->pop_all_but_last(); - $output .= $this->notification($message, $messageclass); - $output .= $this->footer(); - 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; + /* Implement interface method. */ + public function mod_icon_url($iconname, $module) { + foreach ($this->places as $dirroot => $urlroot) { + if (file_exists($dirroot . 'mod/' . $iconname . '.gif')) { + return $iconname . 'mod/' . $iconname . '.gif'; + } } - return $output; + return parent::old_icon_url($iconname); } +} - // TODO remove $navigation and $menu arguments - replace with $PAGE->navigation - public function header($navigation = '', $menu='') { - global $USER, $CFG; - output_starting_hook(); - $this->page->set_state(moodle_page::STATE_PRINTING_HEADER); +/** + * This is a base class to help you implement the renderer_factory interface. + * + * It keeps a cache of renderers that have been constructed, so you only need + * to construct each one once in you subclass. + * + * It also has a method to get the name of, and include the renderer.php with + * the definition of, the standard renderer class for a given module. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 + */ +abstract class renderer_factory_base implements renderer_factory { + /** @var theme_config the theme we belong to. */ + protected $theme; - // Find the appropriate page template, based on $this->page->generaltype. - $templatefile = $this->find_page_template(); - if ($templatefile) { - // Render the template. - $template = $this->render_page_template($templatefile, $menu, $navigation); - } else { - // New style template not found, fall back to using header.html and footer.html. - $template = $this->handle_legacy_theme($navigation, $menu); + /** + * Constructor. + * @param theme_config $theme the theme we belong to. + */ + public function __construct($theme) { + $this->theme = $theme; + } + /** + * For a given module name, return the name of the standard renderer class + * that defines the renderer interface for that module. + * + * Also, if it exists, include the renderer.php file for that module, so + * the class definition of the default renderer has been loaded. + * + * @param string $component name such as 'core', 'mod_forum' or 'qtype_multichoice'. + * @return string the name of the standard renderer class for that module. + */ + protected function standard_renderer_class_for_module($component) { + if ($component != 'core') { + $pluginrenderer = get_component_directory($component) . '/renderer.php'; + if (file_exists($pluginrenderer)) { + include_once($pluginrenderer); + } } - - // Slice the template output into header and footer. - $cutpos = strpos($template, self::MAIN_CONTENT_TOKEN); - if ($cutpos === false) { - throw new coding_exception('Layout template ' . $templatefile . - ' does not contain the string "' . self::MAIN_CONTENT_TOKEN . '".'); + $class = 'moodle_' . $component . '_renderer'; + if (!class_exists($class)) { + throw new coding_exception('Request for an unknown renderer class ' . $class); } - $header = substr($template, 0, $cutpos); - $footer = substr($template, $cutpos + strlen(self::MAIN_CONTENT_TOKEN)); - - send_headers($this->contenttype, $this->page->cacheable); - $this->opencontainers->push('header/footer', $footer); - $this->page->set_state(moodle_page::STATE_IN_BODY); - return $header . $this->skip_link_target(); + return $class; } +} - protected function find_page_template() { - global $THEME; - // If this is a particular page type, look for a specific template. - $type = $this->page->generaltype; - if ($type != 'normal') { - $templatefile = $THEME->dir . '/layout-' . $type . '.php'; - if (is_readable($templatefile)) { - return $templatefile; - } +/** + * This is the default renderer factory for Moodle. It simply returns an instance + * of the appropriate standard renderer class. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 + */ +class standard_renderer_factory extends renderer_factory_base { + /* Implement the subclass method. */ + public function get_renderer($module, $page) { + if ($module == 'core') { + return new moodle_core_renderer($page); + } else { + $class = $this->standard_renderer_class_for_module($module); + return new $class($page, $this->get_renderer('core', $page)); } + } +} - // Otherwise look for the general template. - $templatefile = $THEME->dir . '/layout.php'; - if (is_readable($templatefile)) { - return $templatefile; - } - return false; +/** + * This is a slight variation on the standard_renderer_factory used by CLI scripts. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 + */ +class cli_renderer_factory extends standard_renderer_factory { + /* Implement the subclass method. */ + public function get_renderer($module, $page) { + if ($module == 'core') { + return new cli_core_renderer($page); + } else { + parent::get_renderer($module, $page); + } } +} - protected function render_page_template($templatefile, $menu, $navigation) { - global $CFG, $SITE, $THEME, $USER; - // 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 the same as - // the global $OUTPUT object. When rendering the template, we want to use - // this object. However, people writing Moodle code expect the current - // rederer 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(); - include($templatefile); - $template = ob_get_contents(); - ob_end_clean(); - return $template; +/** + * This is renderer factory allows themes to override the standard renderers using + * php code. + * + * It will load any code from theme/mytheme/renderers.php and + * theme/parenttheme/renderers.php, if then exist. Then whenever you ask for + * a renderer for 'component', it will create a mytheme_component_renderer or a + * parenttheme_component_renderer, instead of a moodle_component_renderer, + * if either of those classes exist. + * + * This generates the slightly different HTML that the custom_corners theme expects. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 + */ +class theme_overridden_renderer_factory extends standard_renderer_factory { + protected $prefixes = array(); + + /** + * Constructor. + * @param object $theme the theme we are rendering for. + * @param moodle_page $page the page we are doing output for. + */ + public function __construct($theme) { + global $CFG; + parent::__construct($theme); + + // Initialise $this->prefixes. + $renderersfile = $theme->dir . '/renderers.php'; + if (is_readable($renderersfile)) { + include_once($renderersfile); + $this->prefixes[] = $theme->name . '_'; + } + if (!empty($theme->parent)) { + $renderersfile = $CFG->themedir .'/'. $theme->parent . '/renderers.php'; + if (is_readable($renderersfile)) { + include_once($renderersfile); + $this->prefixes[] = $theme->parent . '_'; + } + } } - protected function handle_legacy_theme($navigation, $menu) { - global $CFG, $SITE, $THEME, $USER; - // Set a pretend global from the properties of this class. - // See the comment in render_page_template for a fuller explanation. - $COURSE = $this->page->course; + /* Implement the subclass method. */ + public function get_renderer($module, $page) { + foreach ($this->prefixes as $prefix) { + $classname = $prefix . $module . '_renderer'; + if (class_exists($classname)) { + if ($module == 'core') { + return new $classname($page); + } else { + return new $classname($page, $this->get_renderer('core', $page)); + } + } + } + return parent::get_renderer($module, $page); + } +} - // Set up local variables that header.html expects. - $direction = $this->htmlattributes(); - $title = $this->page->title; - $heading = $this->page->heading; - $focus = $this->page->focuscontrol; - $button = $this->page->button; - $pageid = $this->page->pagetype; - $pageclass = $this->page->bodyclasses; - $bodytags = ' class="' . $pageclass . '" id="' . $pageid . '"'; - $home = $this->page->generaltype == 'home'; - $meta = $this->standard_head_html(); - // The next line is a nasty hack. having set $meta to standard_head_html, we have already - // got the contents of include($CFG->javascript). However, legacy themes are going to - // include($CFG->javascript) again. We want to make sure that when they do, nothing is output. - $CFG->javascript = $CFG->libdir . '/emptyfile.php'; +/** + * This is renderer factory that allows you to create templated themes. + * + * This should be considered an experimental proof of concept. In particular, + * the performance is probably not very good. Do not try to use in on a busy site + * without doing careful load testing first! + * + * This renderer factory returns instances of {@link template_renderer} class + * which which implement the corresponding renderer interface in terms of + * templates. To use this your theme must have a templates folder inside it. + * Then suppose the method moodle_core_renderer::greeting($name = 'world'); + * exists. Then, a call to $OUTPUT->greeting() will cause the template + * /theme/yourtheme/templates/core/greeting.php to be rendered, with the variable + * $name available. The greeting.php template might contain + * + *

+ * 

Hello !

+ *
+ * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 + */ +class template_renderer_factory extends renderer_factory_base { + /** + * An array of paths of where to search for templates. Normally this theme, + * the parent theme then the standardtemplate theme. (If some of these do + * not exist, or are the same as each other, then the list will be shorter. + */ + protected $searchpaths = array(); - // Set up local variables that footer.html expects. - $homelink = $this->home_link(); - $loggedinas = $this->login_info(); - $course = $this->page->course; - $performanceinfo = self::PERFORMANCE_INFO_TOKEN; + /** + * Constructor. + * @param object $theme the theme we are rendering for. + * @param moodle_page $page the page we are doing output for. + */ + public function __construct($theme) { + global $CFG; + parent::__construct($theme); - if (!$menu && $navigation) { - $menu = $loggedinas; + // Initialise $this->searchpaths. + if ($theme->name != 'standardtemplate') { + $templatesdir = $theme->dir . '/templates'; + if (is_dir($templatesdir)) { + $this->searchpaths[] = $templatesdir; + } } + if (!empty($theme->parent)) { + $templatesdir = $CFG->themedir .'/'. $theme->parent . '/templates'; + if (is_dir($templatesdir)) { + $this->searchpaths[] = $templatesdir; + } + } + $this->searchpaths[] = $CFG->themedir .'/standardtemplate/templates'; + } - ob_start(); - include($THEME->dir . '/header.html'); - $this->page->requires->get_top_of_body_code(); - echo self::MAIN_CONTENT_TOKEN; + /* Implement the subclass method. */ + public function get_renderer($module, $page) { + // Refine the list of search paths for this module. + $searchpaths = array(); + foreach ($this->searchpaths as $rootpath) { + $path = $rootpath . '/' . $module; + if (is_dir($path)) { + $searchpaths[] = $path; + } + } - $menu = str_replace('navmenu', 'navmenufooter', $menu); - include($THEME->dir . '/footer.html'); + // Create a template_renderer that copies the API of the standard renderer. + $copiedclass = $this->standard_renderer_class_for_module($module); + return new template_renderer($copiedclass, $searchpaths, $page); + } +} - $output = ob_get_contents(); - ob_end_clean(); - $output = str_replace('', self::END_HTML_TOKEN . '', $output); +/** + * 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 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 + */ +class moodle_renderer_base { + /** @var xhtml_container_stack the xhtml_container_stack to use. */ + protected $opencontainers; + /** @var moodle_page the page we are rendering for. */ + protected $page; - return $output; + /** + * Constructor + * @param $opencontainers the xhtml_container_stack to use. + * @param moodle_page $page the page we are doing output for. + */ + public function __construct($page) { + $this->opencontainers = $page->opencontainers; + $this->page = $page; } - public function footer() { + /** + * Have we started output yet? + * @return boolean true if the header has been printed. + */ + public function has_started() { + return $this->page->state >= moodle_page::STATE_IN_BODY; + } + + protected function output_tag($tagname, $attributes, $contents) { + return $this->output_start_tag($tagname, $attributes) . $contents . + $this->output_end_tag($tagname); + } + protected function output_start_tag($tagname, $attributes) { + return '<' . $tagname . $this->output_attributes($attributes) . '>'; + } + protected function output_end_tag($tagname) { + return ''; + } + protected function output_empty_tag($tagname, $attributes) { + return '<' . $tagname . $this->output_attributes($attributes) . ' />'; + } + + protected function output_attribute($name, $value) { + $value = trim($value); + if ($value || is_numeric($value)) { // We want 0 to be output. + return ' ' . $name . '="' . $value . '"'; + } + } + protected function output_attributes($attributes) { + if (empty($attributes)) { + $attributes = array(); + } $output = ''; - if ($this->opencontainers->count() != 1) { - debugging('Some HTML tags were opened in the body of the page but not closed.', DEBUG_DEVELOPER); - $output .= $this->opencontainers->pop_all_but_last(); + foreach ($attributes as $name => $value) { + $output .= $this->output_attribute($name, $value); } + return $output; + } + public static function prepare_classes($classes) { + if (is_array($classes)) { + return implode(' ', array_unique($classes)); + } + return $classes; + } - $footer = $this->opencontainers->pop('header/footer'); + /** + * Return the URL for an icon indentifed as in pre-Moodle 2.0 code. + * + * Suppose you have old code like $url = "$CFG->pixpath/i/course.gif"; + * then old_icon_url('i/course'); will return the equivalent URL that is correct now. + * + * @param $iconname the name of the icon. + * @return string the URL for that icon. + */ + public function old_icon_url($iconname) { + return $this->page->theme->old_icon_url($iconname); + } - // 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_PERFTOLOG') && !function_exists('register_shutdown_function')) { - error_log("PERF: " . $perf['txt']); - } - if (defined('MDL_PERFTOFOOT') || debugging() || $CFG->perfdebug > 7) { - $performanceinfo = $perf['html']; - } - } - $footer = str_replace(self::PERFORMANCE_INFO_TOKEN, $performanceinfo, $footer); + /** + * Return the URL for an icon indentifed as in pre-Moodle 2.0 code. + * + * Suppose you have old code like $url = "$CFG->modpixpath/$mod/icon.gif"; + * then mod_icon_url('icon', $mod); will return the equivalent URL that is correct now. + * + * @param $iconname the name of the icon. + * @param $module the module the icon belongs to. + * @return string the URL for that icon. + */ + public function mod_icon_url($iconname, $module) { + return $this->page->theme->mod_icon_url($iconname, $module); + } +} - $footer = str_replace(self::END_HTML_TOKEN, $this->page->requires->get_end_code(), $footer); - $this->page->set_state(moodle_page::STATE_DONE); +/** + * This is the templated renderer which copies the API of another class, replacing + * all methods calls with instantiation of a template. + * + * When the method method_name is called, this class will search for a template + * called method_name.php in the folders in $searchpaths, taking the first one + * that it finds. Then it will set up variables for each of the arguments of that + * method, and render the template. This is implemented in the {@link __call()} + * PHP magic method. + * + * Methods like print_box_start and print_box_end are handles specially, and + * implemented in terms of the print_box.php method. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 + */ +class template_renderer extends moodle_renderer_base { + /** @var ReflectionClass information about the class whose API we are copying. */ + protected $copiedclass; + /** @var array of places to search for templates. */ + protected $searchpaths; + protected $rendererfactory; - return $output . $footer; + /** + * Magic word used when breaking apart container templates to implement + * _start and _end methods. + */ + const contentstoken = '-@#-Contents-go-here-#@-'; + + /** + * Constructor + * @param string $copiedclass the name of a class whose API we should be copying. + * @param $searchpaths a list of folders to search for templates in. + * @param $opencontainers the xhtml_container_stack to use. + * @param moodle_page $page the page we are doing output for. + */ + public function __construct($copiedclass, $searchpaths, $page) { + parent::__construct($page); + $this->copiedclass = new ReflectionClass($copiedclass); + $this->searchpaths = $searchpaths; + } + + /* PHP magic method implementation. */ + public function __call($method, $arguments) { + if (substr($method, -6) == '_start') { + return $this->process_start(substr($method, 0, -6), $arguments); + } else if (substr($method, -4) == '_end') { + return $this->process_end(substr($method, 0, -4), $arguments); + } else { + return $this->process_template($method, $arguments); + } } /** - * Prints a nice side block with an optional header. - * - * The content is described - * by a {@link block_contents} object. - * - * @param block $content HTML for the content + * Render the template for a given method of the renderer class we are copying, + * using the arguments passed. + * @param string $method the method that was called. + * @param array $arguments the arguments that were passed to it. * @return string the HTML to be output. */ - function block($bc) { - $bc = clone($bc); - $bc->prepare(); - - $title = strip_tags($bc->title); - if (empty($title)) { - $output = ''; - $skipdest = ''; - } else { - $output = $this->output_tag('a', array('href' => '#sb-' . $bc->skipid, 'class' => 'skip-block'), - get_string('skipa', 'access', $title)); - $skipdest = $this->output_tag('span', array('id' => 'sb-' . $bc->skipid, 'class' => 'skip-block-to'), ''); + protected function process_template($method, $arguments) { + if (!$this->copiedclass->hasMethod($method) || + !$this->copiedclass->getMethod($method)->isPublic()) { + throw new coding_exception('Unknown method ' . $method); } - $bc->attributes['id'] = $bc->id; - $bc->attributes['class'] = $bc->get_classes_string(); - $output .= $this->output_start_tag('div', $bc->attributes); + // Find the template file for this method. + $template = $this->find_template($method); - if ($bc->heading) { - // Some callers pass in complete html for the heading, which may include - // complicated things such as the 'hide block' button; some just pass in - // text. If they only pass in plain text i.e. it doesn't include a - //
, then we add in standard tags that make it look like a normal - // page block including the h2 for accessibility - if (strpos($bc->heading, '
') === false) { - $bc->heading = $this->output_tag('div', array('class' => 'title'), - $this->output_tag('h2', null, $bc->heading)); + // Use the reflection API to find out what variable names the arguments + // should be stored in, and fill in any missing ones with the defaults. + $namedarguments = array(); + $expectedparams = $this->copiedclass->getMethod($method)->getParameters(); + foreach ($expectedparams as $param) { + $paramname = $param->getName(); + if (!empty($arguments)) { + $namedarguments[$paramname] = array_shift($arguments); + } else if ($param->isDefaultValueAvailable()) { + $namedarguments[$paramname] = $param->getDefaultValue(); + } else { + throw new coding_exception('Missing required argument ' . $paramname); } - - $output .= $this->output_tag('div', array('class' => 'header'), $bc->heading); } - $output .= $this->output_start_tag('div', array('class' => 'content')); - - if ($bc->content) { - $output .= $bc->content; + // Actually render the template. + return $this->render_template($template, $namedarguments); + } - } else if ($bc->list) { - $row = 0; - $items = array(); - foreach ($bc->list as $key => $string) { - $item = $this->output_start_tag('li', array('class' => 'r' . $row)); - if ($bc->icons) { - $item .= $this->output_tag('div', array('class' => 'icon column c0'), $bc->icons[$key]); - } - $item .= $this->output_tag('div', array('class' => 'column c1'), $string); - $item .= $this->output_end_tag('li'); - $items[] = $item; - $row = 1 - $row; // Flip even/odd. - } - $output .= $this->output_tag('ul', array('class' => 'list'), implode("\n", $items)); - } + /** + * Actually do the work of rendering the template. + * @param $_template the full path to the template file. + * @param $_namedarguments an array variable name => value, the variables + * that should be available to the template. + * @return string the HTML to be output. + */ + protected function render_template($_template, $_namedarguments) { + // Note, we intentionally break the coding guidelines with regards to + // local variable names used in this function, so that they do not clash + // with the names of any variables being passed to the template. - if ($bc->footer) { - $output .= $this->output_tag('div', array('class' => 'footer'), $bc->footer); - } + global $CFG, $SITE, $THEME, $USER; + // 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 the same as + // the global $OUTPUT object. When rendering the template, we want to use + // this object. However, people writing Moodle code expect the current + // rederer 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; - $output .= $this->output_end_tag('div'); - $output .= $this->output_end_tag('div'); - $output .= $skipdest; + // And the parameters from the function call. + extract($_namedarguments); - if (!empty($CFG->allowuserblockhiding) && isset($attributes['id'])) { - $strshow = addslashes_js(get_string('showblocka', 'access', $title)); - $strhide = addslashes_js(get_string('hideblocka', 'access', $title)); - $output .= $this->page->requires->js_function_call('elementCookieHide', array( - $bc->id, $strshow, $strhide))->asap(); - } + // Include the template, capturing the output. + ob_start(); + include($_template); + $_result = ob_get_contents(); + ob_end_clean(); - return $output; + return $_result; } - public function link_to_popup_window() { - + /** + * Searches the folders in {@link $searchpaths} to try to find a template for + * this method name. Throws an exception if one cannot be found. + * @param string $method the method name. + * @return string the full path of the template to use. + */ + protected function find_template($method) { + foreach ($this->searchpaths as $path) { + $filename = $path . '/' . $method . '.php'; + if (file_exists($filename)) { + return $filename; + } + } + throw new coding_exception('Cannot find template for ' . $this->copiedclass->getName() . '::' . $method); } - public function button_to_popup_window() { - + /** + * Handle methods like print_box_start by using the print_box template, + * splitting the result, pusing the end onto the stack, then returning the start. + * @param string $method the method that was called, with _start stripped off. + * @param array $arguments the arguments that were passed to it. + * @return string the HTML to be output. + */ + protected function process_start($template, $arguments) { + array_unshift($arguments, self::contentstoken); + $html = $this->process_template($template, $arguments); + list($start, $end) = explode(self::contentstoken, $html, 2); + $this->opencontainers->push($template, $end); + return $start; } - public function close_window_button($buttontext = null, $reloadopener = false) { - if (empty($buttontext)) { - $buttontext = get_string('closewindow'); - } - // TODO + /** + * Handle methods like print_box_end, we just need to pop the end HTML from + * the stack. + * @param string $method the method that was called, with _end stripped off. + * @param array $arguments not used. Assumed to be irrelevant. + * @return string the HTML to be output. + */ + protected function process_end($template, $arguments) { + return $this->opencontainers->pop($template); } - public function close_window($delay = 0, $reloadopener = false) { - // TODO + /** + * @return array the list of paths where this class searches for templates. + */ + public function get_search_paths() { + return $this->searchpaths; } /** - * Output a + * @return string the name of the class whose API we are copying. */ - public function select_menu($selectmenu) { - $selectmenu = clone($selectmenu); - $selectmenu->prepare(); + public function get_copied_class() { + return $this->copiedclass->getName(); + } +} - if ($selectmenu->nothinglabel) { - $selectmenu->options = array($selectmenu->nothingvalue => $selectmenu->nothinglabel) + - $selectmenu->options; - } - if (empty($selectmenu->id)) { - $selectmenu->id = 'menu' . str_replace(array('[', ']'), '', $selectmenu->name); - } +/** + * This class keeps track of which HTML tags are currently open. + * + * This makes it much easier to always generate well formed XHTML output, even + * if execution terminates abruptly. Any time you output some opening HTML + * without the matching closing HTML, you should push the neccessary close tags + * onto the stack. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 + */ +class xhtml_container_stack { + /** @var array stores the list of open containers. */ + protected $opencontainers = array(); - $attributes = array( - 'name' => $selectmenu->name, - 'id' => $selectmenu->id, - 'class' => $selectmenu->get_classes_string(), - 'onchange' => $selectmenu->script, - ); - if ($selectmenu->disabled) { - $attributes['disabled'] = 'disabled'; - } - if ($selectmenu->tabindex) { - $attributes['tabindex'] = $tabindex; - } + /** + * Push the close HTML for a recently opened container onto the stack. + * @param string $type The type of container. This is checked when {@link pop()} + * is called and must match, otherwise a developer debug warning is output. + * @param string $closehtml The HTML required to close the container. + */ + public function push($type, $closehtml) { + $container = new stdClass; + $container->type = $type; + $container->closehtml = $closehtml; + array_push($this->opencontainers, $container); + } - if ($selectmenu->listbox) { - if (is_integer($selectmenu->listbox)) { - $size = $selectmenu->listbox; - } else { - $size = min($selectmenu->maxautosize, count($selectmenu->options)); - } - $attributes['size'] = $size; - if ($selectmenu->multiple) { - $attributes['multiple'] = 'multiple'; - } + /** + * Pop the HTML for the next closing container from the stack. The $type + * must match the type passed when the container was opened, otherwise a + * warning will be output. + * @param string $type The type of container. + * @return string the HTML requried to close the container. + */ + public function pop($type) { + if (empty($this->opencontainers)) { + debugging('There are no more open containers. This suggests there is a nesting problem.', DEBUG_DEVELOPER); + return; } - $html = $this->output_start_tag('select', $attributes) . "\n"; - foreach ($selectmenu->options as $value => $label) { - $attributes = array('value' => $value); - if ((string)$value == (string)$selectmenu->selectedvalue || - (is_array($selectmenu->selectedvalue) && in_array($value, $selectmenu->selectedvalue))) { - $attributes['selected'] = 'selected'; - } - $html .= ' ' . $this->output_tag('option', $attributes, s($label)) . "\n"; + $container = array_pop($this->opencontainers); + if ($container->type != $type) { + debugging('The type of container to be closed (' . $container->type . + ') does not match the type of the next open container (' . $type . + '). This suggests there is a nesting problem.', DEBUG_DEVELOPER); } - $html .= $this->output_end_tag('select') . "\n"; - - return $html; + return $container->closehtml; } - // TODO choose_from_menu_nested - - // TODO choose_from_radio + /** + * Return how many containers are currently open. + * @return integer how many containers are currently open. + */ + public function count() { + return count($this->opencontainers); + } /** - * Output an error message. By default wraps the error message in . - * If the error message is blank, nothing is output. - * @param $message the error message. - * @return string the HTML to output. + * 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 ) + * before outputting the error message. + * @return string the HTML requried to close any open containers inside . */ - public function error_text($message) { - if (empty($message)) { - return ''; + public function pop_all_but_last() { + $output = ''; + while (count($this->opencontainers) > 1) { + $container = array_pop($this->opencontainers); + $output .= $container->closehtml; } - return $this->output_tag('span', array('class' => 'error'), $message); + return $output; } /** - * Do not call this function directly. - * - * To terminate the current script with a fatal error, call the {@link print_error} - * function, or throw an exception. Doing either of those things will then call this - * funciton to display the error, before terminating the exection. - * - * @param string $message - * @param string $moreinfourl - * @param string $link - * @param array $backtrace - * @param string $debuginfo - * @param bool $showerrordebugwarning - * @return string the HTML to output. + * You can call this function if you want to throw away an instance of this + * class without properly emptying the stack (for example, in a unit test). + * Calling this method stops the destruct method from outputting a developer + * debug warning. After calling this method, the instance can no longer be used. */ - public function fatal_error($message, $moreinfourl, $link, $backtrace, - $debuginfo = null, $showerrordebugwarning = false) { - - $output = ''; + public function discard() { + $this->opencontainers = null; + } - if ($this->has_started()) { - $output .= $this->opencontainers->pop_all_but_last(); - } else { - // Header not yet printed - @header('HTTP/1.0 404 Not Found'); - $this->page->set_title(get_string('error')); - $output .= $this->header(); + /** + * Emergency fallback. If we get to the end of processing and not all + * containers have been closed, output the rest with a developer debug warning. + */ + public function __destruct() { + if (empty($this->opencontainers)) { + return; } - $message = '

' . $message . '

'. - '

' . - get_string('moreinformation') . '

'; - $output .= $this->box($message, 'errorbox'); + debugging('Some containers were left open. This suggests there is a nesting problem.', DEBUG_DEVELOPER); + echo $this->pop_all_but_last(); + $container = array_pop($this->opencontainers); + echo $container->closehtml; + } +} - if (debugging('', DEBUG_DEVELOPER)) { - if ($showerrordebugwarning) { - $output .= $this->notification('error() is a deprecated function. ' . - 'Please call print_error() instead of error()', 'notifytiny'); - } - if (!empty($debuginfo)) { - $output .= $this->notification($debuginfo, 'notifytiny'); - } - if (!empty($backtrace)) { - $output .= $this->notification('Stack trace: ' . - format_backtrace($backtrace), 'notifytiny'); - } - } - if (!empty($link)) { - $output .= $this->continue_button($link); +/** + * The standard implementation of the moodle_core_renderer interface. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 + */ +class moodle_core_renderer extends moodle_renderer_base { + const PERFORMANCE_INFO_TOKEN = '%%PERFORMANCEINFO%%'; + const END_HTML_TOKEN = '%%ENDHTML%%'; + const MAIN_CONTENT_TOKEN = '[MAIN CONTENT GOES HERE]'; + protected $contenttype; + protected $metarefreshtag = ''; + + public function doctype() { + global $CFG; + + $doctype = '' . "\n"; + $this->contenttype = 'text/html; charset=utf-8'; + + if (empty($CFG->xmlstrictheaders)) { + return $doctype; } - $output .= $this->footer(); + // We want to serve the page with an XML content type, to force well-formedness errors to be reported. + $prolog = '' . "\n"; + if (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/xhtml+xml') !== false) { + // Firefox and other browsers that can cope natively with XHTML. + $this->contenttype = 'application/xhtml+xml; charset=utf-8'; - // Padding to encourage IE to display our error page, rather than its own. - $output .= str_repeat(' ', 512); + } else if (preg_match('/MSIE.*Windows NT/', $_SERVER['HTTP_USER_AGENT'])) { + // IE can't cope with application/xhtml+xml, but it will cope if we send application/xml with an XSL stylesheet. + $this->contenttype = 'application/xml; charset=utf-8'; + $prolog .= 'httpswwwroot . '/lib/xhtml.xsl"?>' . "\n"; - return $output; + } else { + $prolog = ''; + } + + return $prolog . $doctype; } - /** - * Output a notification (that is, a status message about something that has - * just happened). - * - * @param string $message the message to print out - * @param string $classes normally 'notifyproblem' or 'notifysuccess'. - * @return string the HTML to output. - */ - public function notification($message, $classes = 'notifyproblem') { - return $this->output_tag('div', array('class' => - moodle_renderer_base::prepare_classes($classes)), clean_text($message)); + public function htmlattributes() { + return get_html_lang(true) . ' xmlns="http://www.w3.org/1999/xhtml"'; } - /** - * Print a continue button that goes to a particular URL. - * - * @param string|moodle_url $link The url the button goes to. - * @return string the HTML to output. - */ - public function continue_button($link) { - if (!is_a($link, 'moodle_url')) { - $link = new moodle_url($link); + public function standard_head_html() { + global $CFG, $THEME; + $output = ''; + $output .= '' . "\n"; + $output .= '' . "\n"; + if (!$this->page->cacheable) { + $output .= '' . "\n"; + $output .= '' . "\n"; } - return $this->output_tag('div', array('class' => 'continuebutton'), - print_single_button($link->out(true), $link->params(), get_string('continue'), 'get', '', true)); + // This is only set by the {@link redirect()} method + $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) { + $metarefesh = ''; + $output .= sprintf($metarefesh, $this->page->periodicrefreshdelay, $this->page->url->out()); + } + + // TODO get rid of $CFG->javascript. We should be able to do everything + // with $PAGE->requires. + ob_start(); + include($CFG->javascript); + $output .= ob_get_contents(); + ob_end_clean(); + $output .= $this->page->requires->get_head_code(); + + // List alternate versions. + foreach ($this->page->alternateversions as $type => $alt) { + $output .= $this->output_empty_tag('link', array('rel' => 'alternate', + 'type' => $type, 'title' => $alt->title, 'href' => $alt->url)); + } + + // Add the meta page from the themes if any were requested + // TODO See if we can get rid of this. + $PAGE = $this->page; + $metapage = ''; + if (!isset($THEME->standardmetainclude) || $THEME->standardmetainclude) { + ob_start(); + include_once($CFG->dirroot.'/theme/standard/meta.php'); + $output .= ob_get_contents(); + ob_end_clean(); + } + if ($THEME->parent && (!isset($THEME->parentmetainclude) || $THEME->parentmetainclude)) { + if (file_exists($CFG->dirroot.'/theme/'.$THEME->parent.'/meta.php')) { + ob_start(); + include_once($CFG->dirroot.'/theme/'.$THEME->parent.'/meta.php'); + $output .= ob_get_contents(); + ob_end_clean(); + } + } + if (!isset($THEME->metainclude) || $THEME->metainclude) { + if (file_exists($CFG->dirroot.'/theme/'.current_theme().'/meta.php')) { + ob_start(); + include_once($CFG->dirroot.'/theme/'.current_theme().'/meta.php'); + $output .= ob_get_contents(); + ob_end_clean(); + } + } + + return $output; } - /** - * Output the place a skip link goes to. - * @param $id The target name from the corresponding $PAGE->requires->skip_link_to($target) call. - * @return string the HTML to output. - */ - public function skip_link_target($id = 'maincontent') { - return $this->output_tag('span', array('id' => $id), ''); + public function standard_top_of_body_html() { + return $this->page->requires->get_top_of_body_code(); } - public function heading($text, $level, $classes = 'main', $id = '') { - $level = (integer) $level; - if ($level < 1 or $level > 6) { - throw new coding_exception('Heading level must be an integer between 1 and 6.'); + public function standard_footer_html() { + $output = self::PERFORMANCE_INFO_TOKEN; + if (debugging()) { + $output .= ''; } - return $this->output_tag('h' . $level, - array('id' => $id, 'class' => moodle_renderer_base::prepare_classes($classes)), $text); + return $output; } - public function box($contents, $classes = 'generalbox', $id = '') { - return $this->box_start($classes, $id) . $contents . $this->box_end(); + public function standard_end_of_body_html() { + echo self::END_HTML_TOKEN; } - public function box_start($classes = 'generalbox', $id = '') { - $this->opencontainers->push('box', $this->output_end_tag('div')); - return $this->output_start_tag('div', array('id' => $id, - 'class' => 'box ' . moodle_renderer_base::prepare_classes($classes))); + public function login_info() { + global $USER; + return user_login_string($this->page->course, $USER); } - public function box_end() { - return $this->opencontainers->pop('box'); - } + public function home_link() { + global $CFG, $SITE; - public function container($contents, $classes = '', $id = '') { - return $this->container_start($classes, $id) . $contents . $this->container_end(); - } + if ($this->page->pagetype == 'site-index') { + // Special case for site home page - please do not remove + return ''; - public function container_start($classes = '', $id = '') { - $this->opencontainers->push('container', $this->output_end_tag('div')); - return $this->output_start_tag('div', array('id' => $id, - 'class' => moodle_renderer_base::prepare_classes($classes))); - } + } else if (!empty($CFG->target_release) && $CFG->target_release != $CFG->release) { + // Special case for during install/upgrade. + return ''; - public function container_end() { - return $this->opencontainers->pop('container'); + } else if ($this->page->course->id == $SITE->id || strpos($this->page->pagetype, 'course-view') === 0) { + return ''; + + } else { + return ''; + } } /** - * At the moment we frequently have a problem with $CFG->pixpath not being - * initialised when it is needed. Unfortunately, there is no nice way to handle - * this. I think we need to replace $CFG->pixpath with something like $OUTPUT->icon(...). - * However, until then, we need a way to force $CFG->pixpath to be initialised, - * to fix the error messages, and that is what this function if for. + * 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 string $messageclass The css class to put on the message that is + * being displayed to the user + * @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 initialise_deprecated_cfg_pixpath() { - // Actually, we don't have to do anything here. Just calling any method - // of $OBJECT is enough. However, if the only reason you are calling - // an $OUTPUT method is to get $CFG->pixpath initialised, please use this - // method, so we can find them and clean them up later once we have - // found a better replacement for $CFG->pixpath. - } -} + public function redirect($encodedurl, $message, $delay, $messageclass='notifyproblem') { + global $CFG; + $url = str_replace('&', '&', $encodedurl); + $disableredirect = false; -/** - *This class represents the configuration variables of a Moodle theme. - * - * Normally, to create an instance of this class, you should use the - * {@link theme_config::load()} factory method to load a themes config.php file. - * - * @copyright 2009 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.0 - */ -class theme_config { - /** - * @var array The names of all the stylesheets from this theme that you would - * like included, in order. - */ - public $sheets = array('styles_layout', 'styles_fonts', 'styles_color'); + if ($delay!=0) { + /// At developer debug level. Don't redirect if errors have been printed on screen. + /// Currenly only works in PHP 5.2+; we do not want strict PHP5 errors + $lasterror = error_get_last(); + $error = defined('DEBUGGING_PRINTED') or (!empty($lasterror) && ($lasterror['type'] & DEBUG_DEVELOPER)); + $errorprinted = debugging('', DEBUG_ALL) && $CFG->debugdisplay && $error; + if ($errorprinted) { + $disableredirect= true; + $message = "Error output, so disabling automatic redirect.

" . $message; + } + } - public $standardsheets = true; + switch ($this->page->state) { + case moodle_page::STATE_BEFORE_HEADER : + // No output yet it is safe to delivery the full arsenol of redirect methods + if (!$disableredirect) { + @header($_SERVER['SERVER_PROTOCOL'] . ' 303 See Other'); //302 might not work for POST requests, 303 is ignored by obsolete clients + @header('Location: '.$url); + $this->metarefreshtag = ''."\n"; + $this->page->requires->js_function_call('document.location.replace', array($url))->after_delay($delay+3); + } + $output = $this->header(); + $output .= $this->notification($message, $messageclass); + $output .= $this->footer(); + 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 (!$disableredirect) { + $this->page->requires->js_function_call('document.location.replace', array($url))->after_delay($delay+3); + } + $output = $this->opencontainers->pop_all_but_last(); + $output .= $this->notification($message, $messageclass); + $output .= $this->footer(); + 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; + } + return $output; + } -/// This variable can be set to an array containing -/// filenames from the *STANDARD* theme. If the -/// array exists, it will be used to choose the -/// files to include in the standard style sheet. -/// When false, then no files are used. -/// When true or NON-EXISTENT, then ALL standard files are used. -/// This parameter can be used, for example, to prevent -/// having to override too many classes. -/// Note that the trailing .css should not be included -/// eg $THEME->standardsheets = array('styles_layout','styles_fonts','styles_color'); -//////////////////////////////////////////////////////////////////////////////// + // TODO remove $navigation and $menu arguments - replace with $PAGE->navigation + public function header($navigation = '', $menu='') { + global $USER, $CFG; + output_starting_hook(); + $this->page->set_state(moodle_page::STATE_PRINTING_HEADER); - public $parent = null; + // Find the appropriate page template, based on $this->page->generaltype. + $templatefile = $this->find_page_template(); + if ($templatefile) { + // Render the template. + $template = $this->render_page_template($templatefile, $menu, $navigation); + } else { + // New style template not found, fall back to using header.html and footer.html. + $template = $this->handle_legacy_theme($navigation, $menu); + } -/// This variable can be set to the name of a parent theme -/// which you want to have included before the current theme. -/// This can make it easy to make modifications to another -/// theme without having to actually change the files -/// If this variable is empty or false then a parent theme -/// is not used. -//////////////////////////////////////////////////////////////////////////////// + // Slice the template output into header and footer. + $cutpos = strpos($template, self::MAIN_CONTENT_TOKEN); + if ($cutpos === false) { + throw new coding_exception('Layout template ' . $templatefile . + ' does not contain the string "' . self::MAIN_CONTENT_TOKEN . '".'); + } + $header = substr($template, 0, $cutpos); + $footer = substr($template, $cutpos + strlen(self::MAIN_CONTENT_TOKEN)); + send_headers($this->contenttype, $this->page->cacheable); + $this->opencontainers->push('header/footer', $footer); + $this->page->set_state(moodle_page::STATE_IN_BODY); + return $header . $this->skip_link_target(); + } - public $parentsheets = false; + protected function find_page_template() { + global $THEME; -/// This variable can be set to an array containing -/// filenames from a chosen *PARENT* theme. If the -/// array exists, it will be used to choose the -/// files to include in the standard style sheet. -/// When false, then no files are used. -/// When true or NON-EXISTENT, then ALL standard files are used. -/// This parameter can be used, for example, to prevent -/// having to override too many classes. -/// Note that the trailing .css should not be included -/// eg $THEME->parentsheets = array('styles_layout','styles_fonts','styles_color'); -//////////////////////////////////////////////////////////////////////////////// + // If this is a particular page type, look for a specific template. + $type = $this->page->generaltype; + if ($type != 'normal') { + $templatefile = $THEME->dir . '/layout-' . $type . '.php'; + if (is_readable($templatefile)) { + return $templatefile; + } + } + // Otherwise look for the general template. + $templatefile = $THEME->dir . '/layout.php'; + if (is_readable($templatefile)) { + return $templatefile; + } - public $modsheets = true; + return false; + } -/// When this is enabled, then this theme will search for -/// files named "styles.php" inside all Activity modules and -/// include them. This allows modules to provide some basic -/// layouts so they work out of the box. -/// It is HIGHLY recommended to leave this enabled. + protected function render_page_template($templatefile, $menu, $navigation) { + global $CFG, $SITE, $THEME, $USER; + // 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 the same as + // the global $OUTPUT object. When rendering the template, we want to use + // this object. However, people writing Moodle code expect the current + // rederer 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(); + include($templatefile); + $template = ob_get_contents(); + ob_end_clean(); + return $template; + } - public $blocksheets = true; + protected function handle_legacy_theme($navigation, $menu) { + global $CFG, $SITE, $THEME, $USER; + // Set a pretend global from the properties of this class. + // See the comment in render_page_template for a fuller explanation. + $COURSE = $this->page->course; + + // Set up local variables that header.html expects. + $direction = $this->htmlattributes(); + $title = $this->page->title; + $heading = $this->page->heading; + $focus = $this->page->focuscontrol; + $button = $this->page->button; + $pageid = $this->page->pagetype; + $pageclass = $this->page->bodyclasses; + $bodytags = ' class="' . $pageclass . '" id="' . $pageid . '"'; + $home = $this->page->generaltype == 'home'; + + $meta = $this->standard_head_html(); + // The next line is a nasty hack. having set $meta to standard_head_html, we have already + // got the contents of include($CFG->javascript). However, legacy themes are going to + // include($CFG->javascript) again. We want to make sure that when they do, nothing is output. + $CFG->javascript = $CFG->libdir . '/emptyfile.php'; -/// When this is enabled, then this theme will search for -/// files named "styles.php" inside all Block modules and -/// include them. This allows Blocks to provide some basic -/// layouts so they work out of the box. -/// It is HIGHLY recommended to leave this enabled. + // Set up local variables that footer.html expects. + $homelink = $this->home_link(); + $loggedinas = $this->login_info(); + $course = $this->page->course; + $performanceinfo = self::PERFORMANCE_INFO_TOKEN; + if (!$menu && $navigation) { + $menu = $loggedinas; + } - public $langsheets = false; + ob_start(); + include($THEME->dir . '/header.html'); + $this->page->requires->get_top_of_body_code(); + echo self::MAIN_CONTENT_TOKEN; -/// By setting this to true, then this theme will search for -/// a file named "styles.php" inside the current language -/// directory. This allows different languages to provide -/// different styles. + $menu = str_replace('navmenu', 'navmenufooter', $menu); + include($THEME->dir . '/footer.html'); + $output = ob_get_contents(); + ob_end_clean(); - public $courseformatsheets = true; + $output = str_replace('', self::END_HTML_TOKEN . '', $output); -/// When this is enabled, this theme will search for files -/// named "styles.php" inside all course formats and -/// include them. This allows course formats to provide -/// their own default styles. + return $output; + } + public function footer() { + $output = ''; + if ($this->opencontainers->count() != 1) { + debugging('Some HTML tags were opened in the body of the page but not closed.', DEBUG_DEVELOPER); + $output .= $this->opencontainers->pop_all_but_last(); + } - public $metainclude = false; + $footer = $this->opencontainers->pop('header/footer'); -/// When this is enabled (or not set!) then Moodle will try -/// to include a file meta.php from this theme into the -/// part of the page. + // 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_PERFTOLOG') && !function_exists('register_shutdown_function')) { + error_log("PERF: " . $perf['txt']); + } + if (defined('MDL_PERFTOFOOT') || debugging() || $CFG->perfdebug > 7) { + $performanceinfo = $perf['html']; + } + } + $footer = str_replace(self::PERFORMANCE_INFO_TOKEN, $performanceinfo, $footer); + $footer = str_replace(self::END_HTML_TOKEN, $this->page->requires->get_end_code(), $footer); - public $standardmetainclude = true; + $this->page->set_state(moodle_page::STATE_DONE); + return $output . $footer; + } -/// When this is enabled (or not set!) then Moodle will try -/// to include a file meta.php from the standard theme into the -/// part of the page. + /** + * Prints a nice side block with an optional header. + * + * The content is described + * by a {@link block_contents} object. + * + * @param block $content HTML for the content + * @return string the HTML to be output. + */ + function block($bc) { + $bc = clone($bc); + $bc->prepare(); + $title = strip_tags($bc->title); + if (empty($title)) { + $output = ''; + $skipdest = ''; + } else { + $output = $this->output_tag('a', array('href' => '#sb-' . $bc->skipid, 'class' => 'skip-block'), + get_string('skipa', 'access', $title)); + $skipdest = $this->output_tag('span', array('id' => 'sb-' . $bc->skipid, 'class' => 'skip-block-to'), ''); + } - public $parentmetainclude = false; + $bc->attributes['id'] = $bc->id; + $bc->attributes['class'] = $bc->get_classes_string(); + $output .= $this->output_start_tag('div', $bc->attributes); -/// When this is enabled (or not set!) then Moodle will try -/// to include a file meta.php from the parent theme into the -/// part of the page. + if ($bc->heading) { + // Some callers pass in complete html for the heading, which may include + // complicated things such as the 'hide block' button; some just pass in + // text. If they only pass in plain text i.e. it doesn't include a + //

, then we add in standard tags that make it look like a normal + // page block including the h2 for accessibility + if (strpos($bc->heading, '
') === false) { + $bc->heading = $this->output_tag('div', array('class' => 'title'), + $this->output_tag('h2', null, $bc->heading)); + } + $output .= $this->output_tag('div', array('class' => 'header'), $bc->heading); + } - public $navmenuwidth = 50; + $output .= $this->output_start_tag('div', array('class' => 'content')); -/// You can use this to control the cutoff point for strings -/// in the navmenus (list of activities in popup menu etc) -/// Default is 50 characters wide. + if ($bc->content) { + $output .= $bc->content; + } else if ($bc->list) { + $row = 0; + $items = array(); + foreach ($bc->list as $key => $string) { + $item = $this->output_start_tag('li', array('class' => 'r' . $row)); + if ($bc->icons) { + $item .= $this->output_tag('div', array('class' => 'icon column c0'), $bc->icons[$key]); + } + $item .= $this->output_tag('div', array('class' => 'column c1'), $string); + $item .= $this->output_end_tag('li'); + $items[] = $item; + $row = 1 - $row; // Flip even/odd. + } + $output .= $this->output_tag('ul', array('class' => 'list'), implode("\n", $items)); + } - public $makenavmenulist = false; + if ($bc->footer) { + $output .= $this->output_tag('div', array('class' => 'footer'), $bc->footer); + } -/// By setting this to true, then you will have access to a -/// new variable in your header.html and footer.html called -/// $navmenulist ... this contains a simple XHTML menu of -/// all activities in the current course, mostly useful for -/// creating popup navigation menus and so on. + $output .= $this->output_end_tag('div'); + $output .= $this->output_end_tag('div'); + $output .= $skipdest; + if (!empty($CFG->allowuserblockhiding) && isset($attributes['id'])) { + $strshow = addslashes_js(get_string('showblocka', 'access', $title)); + $strhide = addslashes_js(get_string('hideblocka', 'access', $title)); + $output .= $this->page->requires->js_function_call('elementCookieHide', array( + $bc->id, $strshow, $strhide))->asap(); + } + return $output; + } - public $resource_mp3player_colors = 'bgColour=000000&btnColour=ffffff&btnBorderColour=cccccc&iconColour=000000&iconOverColour=00cc00&trackColour=cccccc&handleColour=ffffff&loaderColour=ffffff&font=Arial&fontColour=3333FF&buffer=10&waitForPlay=no&autoPlay=yes'; + public function link_to_popup_window() { -/// With this you can control the colours of the "big" MP3 player -/// that is used for MP3 resources. + } + public function button_to_popup_window() { - public $filter_mediaplugin_colors = 'bgColour=000000&btnColour=ffffff&btnBorderColour=cccccc&iconColour=000000&iconOverColour=00cc00&trackColour=cccccc&handleColour=ffffff&loaderColour=ffffff&waitForPlay=yes'; + } -/// ...And this controls the small embedded player + public function close_window_button($buttontext = null, $reloadopener = false) { + if (empty($buttontext)) { + $buttontext = get_string('closewindow'); + } + // TODO + } + public function close_window($delay = 0, $reloadopener = false) { + // TODO + } - public $custompix = false; + /** + * Output a + */ + public function select_menu($selectmenu) { + $selectmenu = clone($selectmenu); + $selectmenu->prepare(); -/// If true, then this theme must have a "pix" -/// subdirectory that contains copies of all -/// files from the moodle/pix directory, plus a -/// "pix/mod" directory containing all the icons -/// for all the activity modules. + if ($selectmenu->nothinglabel) { + $selectmenu->options = array($selectmenu->nothingvalue => $selectmenu->nothinglabel) + + $selectmenu->options; + } + if (empty($selectmenu->id)) { + $selectmenu->id = 'menu' . str_replace(array('[', ']'), '', $selectmenu->name); + } -///$THEME->rarrow = '►' //OR '→'; -///$THEME->larrow = '◄' //OR '←'; -///$CFG->block_search_button = link_arrow_right(get_string('search'), $url='', $accesshide=true); -/// -/// Accessibility: Right and left arrow-like characters are -/// used in the breadcrumb trail, course navigation menu -/// (previous/next activity), calendar, and search forum block. -/// -/// If the theme does not set characters, appropriate defaults -/// are set by (lib/weblib.php:check_theme_arrows). The suggestions -/// above are 'silent' in a screen-reader like JAWS. Please DO NOT -/// use < > » - these are confusing for blind users. -//////////////////////////////////////////////////////////////////////////////// + $attributes = array( + 'name' => $selectmenu->name, + 'id' => $selectmenu->id, + 'class' => $selectmenu->get_classes_string(), + 'onchange' => $selectmenu->script, + ); + if ($selectmenu->disabled) { + $attributes['disabled'] = 'disabled'; + } + if ($selectmenu->tabindex) { + $attributes['tabindex'] = $tabindex; + } + if ($selectmenu->listbox) { + if (is_integer($selectmenu->listbox)) { + $size = $selectmenu->listbox; + } else { + $size = min($selectmenu->maxautosize, count($selectmenu->options)); + } + $attributes['size'] = $size; + if ($selectmenu->multiple) { + $attributes['multiple'] = 'multiple'; + } + } - public $blockregions = array('side-pre', 'side-post'); - public $defaultblockregion = 'side-post'; -/// Areas where blocks may appear on any page that uses this theme. For each -/// region you list in $THEME->blockregions you must call blocks_print_group -/// with that region id somewhere in header.html or footer.html. -/// defaultblockregion is the region where new blocks will be added, and -/// where any blocks in unrecognised regions will be shown. (Suppose someone -/// added a block when anther theme was selected). -//////////////////////////////////////////////////////////////////////////////// + $html = $this->output_start_tag('select', $attributes) . "\n"; + foreach ($selectmenu->options as $value => $label) { + $attributes = array('value' => $value); + if ((string)$value == (string)$selectmenu->selectedvalue || + (is_array($selectmenu->selectedvalue) && in_array($value, $selectmenu->selectedvalue))) { + $attributes['selected'] = 'selected'; + } + $html .= ' ' . $this->output_tag('option', $attributes, s($label)) . "\n"; + } + $html .= $this->output_end_tag('select') . "\n"; - /** @var string the name of this theme. Set automatically. */ - public $name; - /** @var string the folder where this themes fiels are stored. $CFG->themedir . '/' . $this->name */ - public $dir; + return $html; + } - /** @var string Name of the renderer factory class to use. */ - public $rendererfactory = 'standard_renderer_factory'; - /** @var renderer_factory Instance of the renderer_factory class. */ - protected $rf = null; + // TODO choose_from_menu_nested + + // TODO choose_from_radio /** - * If you want to do custom processing on the CSS before it is output (for - * example, to replace certain variable names with particular values) you can - * give the name of a function here. - * - * There are two functions avaiable that you may wish to use (defined in lib/outputlib.php): - * output_css_replacing_constants - * output_css_for_css_edit - * If you wish to write your own function, use those two as examples, and it - * should be clear what you have to do. - * - * @var string the name of a function. + * Output an error message. By default wraps the error message in . + * If the error message is blank, nothing is output. + * @param $message the error message. + * @return string the HTML to output. */ - public $customcssoutputfunction = null; + public function error_text($message) { + if (empty($message)) { + return ''; + } + return $this->output_tag('span', array('class' => 'error'), $message); + } /** - * Load the config.php file for a particular theme, and return an instance - * of this class. (That is, this is a factory method.) + * Do not call this function directly. * - * @param string $themename the name of the theme. - * @return theme_config an instance of this class. + * To terminate the current script with a fatal error, call the {@link print_error} + * function, or throw an exception. Doing either of those things will then call this + * funciton to display the error, before terminating the exection. + * + * @param string $message + * @param string $moreinfourl + * @param string $link + * @param array $backtrace + * @param string $debuginfo + * @param bool $showerrordebugwarning + * @return string the HTML to output. */ - public static function load($themename) { - global $CFG; + public function fatal_error($message, $moreinfourl, $link, $backtrace, + $debuginfo = null, $showerrordebugwarning = false) { - // We have to use the variable name $THEME (upper case) becuase that - // is what is used in theme config.php files. + $output = ''; - // Set some other standard properties of the theme. - $THEME = new theme_config; - $THEME->name = $themename; - $THEME->dir = $CFG->themedir . '/' . $themename; + if ($this->has_started()) { + $output .= $this->opencontainers->pop_all_but_last(); + } else { + // Header not yet printed + @header('HTTP/1.0 404 Not Found'); + $this->page->set_title(get_string('error')); + $output .= $this->header(); + } - // Load up the theme config - $configfile = $THEME->dir . '/config.php'; - if (!is_readable($configfile)) { - throw new coding_exception('Cannot use theme ' . $themename . - '. The file ' . $configfile . ' does not exist or is not readable.'); + $message = '

' . $message . '

'. + '

' . + get_string('moreinformation') . '

'; + $output .= $this->box($message, 'errorbox'); + + if (debugging('', DEBUG_DEVELOPER)) { + if ($showerrordebugwarning) { + $output .= $this->notification('error() is a deprecated function. ' . + 'Please call print_error() instead of error()', 'notifytiny'); + } + if (!empty($debuginfo)) { + $output .= $this->notification($debuginfo, 'notifytiny'); + } + if (!empty($backtrace)) { + $output .= $this->notification('Stack trace: ' . + format_backtrace($backtrace), 'notifytiny'); + } } - include($configfile); - $THEME->update_legacy_information(); + if (!empty($link)) { + $output .= $this->continue_button($link); + } - return $THEME; + $output .= $this->footer(); + + // Padding to encourage IE to display our error page, rather than its own. + $output .= str_repeat(' ', 512); + + return $output; } /** - * Get the renderer for a part of Moodle for this theme. - * @param string $module the name of part of moodle. E.g. 'core', 'quiz', 'qtype_multichoice'. - * @param moodle_page $page the page we are rendering - * @return moodle_renderer_base the requested renderer. + * Output a notification (that is, a status message about something that has + * just happened). + * + * @param string $message the message to print out + * @param string $classes normally 'notifyproblem' or 'notifysuccess'. + * @return string the HTML to output. */ - public function get_renderer($module, $page) { - if (is_null($this->rf)) { - if (CLI_SCRIPT) { - $classname = 'cli_renderer_factory'; - } else { - $classname = $this->rendererfactory; - } - $this->rf = new $classname($this); - } - - return $this->rf->get_renderer($module, $page); + public function notification($message, $classes = 'notifyproblem') { + return $this->output_tag('div', array('class' => + moodle_renderer_base::prepare_classes($classes)), clean_text($message)); } /** - * Set the variable $CFG->pixpath and $CFG->modpixpath to be the right - * ones for this theme. + * Print a continue button that goes to a particular URL. + * + * @param string|moodle_url $link The url the button goes to. + * @return string the HTML to output. */ - public function setup_cfg_paths() { - global $CFG; - if (!empty($CFG->smartpix)) { - if ($CFG->slasharguments) { - // Use this method if possible for better caching - $extra = ''; - } else { - $extra = '?file='; - } - $CFG->pixpath = $CFG->httpswwwroot . '/pix/smartpix.php' . $extra . '/' . $this->name; - $CFG->modpixpath = $CFG->httpswwwroot . '/pix/smartpix.php' . $extra . '/' . $this->name . '/mod'; - - } else if (empty($THEME->custompix)) { - $CFG->pixpath = $CFG->httpswwwroot . '/pix'; - $CFG->modpixpath = $CFG->httpswwwroot . '/mod'; - - } else { - $CFG->pixpath = $CFG->httpsthemewww . '/' . $this->name . '/pix'; - $CFG->modpixpath = $CFG->httpsthemewww . '/' . $this->name . '/pix/mod'; + public function continue_button($link) { + if (!is_a($link, 'moodle_url')) { + $link = new moodle_url($link); } + return $this->output_tag('div', array('class' => 'continuebutton'), + print_single_button($link->out(true), $link->params(), get_string('continue'), 'get', '', true)); } /** - * Get the list of stylesheet URLs that need to go in the header for this theme. - * @return array of URLs. + * Output the place a skip link goes to. + * @param $id The target name from the corresponding $PAGE->requires->skip_link_to($target) call. + * @return string the HTML to output. */ - public function get_stylesheet_urls() { - global $CFG; - - // Put together the parameters - $params = '?for=' . $this->name; + public function skip_link_target($id = 'maincontent') { + return $this->output_tag('span', array('id' => $id), ''); + } - // Stylesheets, in order (standard, parent, this - some of which may be the same). - $stylesheets = array(); - if ($this->name != 'standard' && $this->standardsheets) { - $stylesheets[] = $CFG->httpsthemewww . '/standard/styles.php' . $params; - } - if (!empty($this->parent)) { - $stylesheets[] = $CFG->httpsthemewww . '/' . $this->parent . '/styles.php' . $params; + public function heading($text, $level, $classes = 'main', $id = '') { + $level = (integer) $level; + if ($level < 1 or $level > 6) { + throw new coding_exception('Heading level must be an integer between 1 and 6.'); } + return $this->output_tag('h' . $level, + array('id' => $id, 'class' => moodle_renderer_base::prepare_classes($classes)), $text); + } - // Pass on the current language, if it will be needed. - if (!empty($this->langsheets)) { - $params .= '&lang=' . current_language(); - } - $stylesheets[] = $CFG->httpsthemewww . '/' . $this->name . '/styles.php' . $params; + public function box($contents, $classes = 'generalbox', $id = '') { + return $this->box_start($classes, $id) . $contents . $this->box_end(); + } - // Additional styles for right-to-left languages. - if (right_to_left()) { - $stylesheets[] = $CFG->httpsthemewww . '/standard/rtl.css'; + public function box_start($classes = 'generalbox', $id = '') { + $this->opencontainers->push('box', $this->output_end_tag('div')); + return $this->output_start_tag('div', array('id' => $id, + 'class' => 'box ' . moodle_renderer_base::prepare_classes($classes))); + } - if (!empty($this->parent) && file_exists($CFG->themedir . '/' . $this->parent . '/rtl.css')) { - $stylesheets[] = $CFG->httpsthemewww . '/' . $this->parent . '/rtl.css'; - } + public function box_end() { + return $this->opencontainers->pop('box'); + } - if (file_exists($this->dir . '/rtl.css')) { - $stylesheets[] = $CFG->httpsthemewww . '/' . $this->name . '/rtl.css'; - } - } + public function container($contents, $classes = '', $id = '') { + return $this->container_start($classes, $id) . $contents . $this->container_end(); + } - return $stylesheets; + public function container_start($classes = '', $id = '') { + $this->opencontainers->push('container', $this->output_end_tag('div')); + return $this->output_start_tag('div', array('id' => $id, + 'class' => moodle_renderer_base::prepare_classes($classes))); + } + + public function container_end() { + return $this->opencontainers->pop('container'); } /** - * This methon looks a the settings that have been loaded, to see whether - * any legacy things are being used, and outputs warning and tries to update - * things to use equivalent newer settings. + * At the moment we frequently have a problem with $CFG->pixpath not being + * initialised when it is needed. Unfortunately, there is no nice way to handle + * this. I think we need to replace $CFG->pixpath with something like $OUTPUT->icon(...). + * However, until then, we need a way to force $CFG->pixpath to be initialised, + * to fix the error messages, and that is what this function if for. */ - protected function update_legacy_information() { - if (!empty($this->customcorners)) { - // $THEME->customcorners is deprecated but we provide support for it via the - // custom_corners_renderer_factory class in lib/deprecatedlib.php - debugging('$THEME->customcorners is deprecated. Please use the new $THEME->rendererfactory ' . - 'to control HTML generation. Please use $this->rendererfactory = \'custom_corners_renderer_factory\'; ' . - 'in your config.php file instead.', DEBUG_DEVELOPER); - $this->rendererfactory = 'custom_corners_renderer_factory'; - } - - if (!empty($this->cssconstants)) { - debugging('$THEME->cssconstants is deprecated. Please use ' . - '$THEME->customcssoutputfunction = \'output_css_replacing_constants\'; ' . - 'in your config.php file instead.', DEBUG_DEVELOPER); - $this->customcssoutputfunction = 'output_css_replacing_constants'; - } - - if (!empty($this->CSSEdit)) { - debugging('$THEME->CSSEdit is deprecated. Please use ' . - '$THEME->customcssoutputfunction = \'output_css_for_css_edit\'; ' . - 'in your config.php file instead.', DEBUG_DEVELOPER); - $this->customcssoutputfunction = 'output_css_for_css_edit'; - } + public function initialise_deprecated_cfg_pixpath() { + // Actually, we don't have to do anything here. Just calling any method + // of $OBJECT is enough. However, if the only reason you are calling + // an $OUTPUT method is to get $CFG->pixpath initialised, please use this + // method, so we can find them and clean them up later once we have + // found a better replacement for $CFG->pixpath. } } diff --git a/lib/setup.php b/lib/setup.php index 98785ab461..7230c99853 100644 --- a/lib/setup.php +++ b/lib/setup.php @@ -110,7 +110,7 @@ global $OUTPUT; /** * $THEME is a global that defines the current theme. * - * @global object $THEME + * @global theme_config $THEME * @name THEME */ global $THEME; diff --git a/lib/simpletest/testoutputlib.php b/lib/simpletest/testoutputlib.php index 509e2b4e80..dfa5f0ed9c 100644 --- a/lib/simpletest/testoutputlib.php +++ b/lib/simpletest/testoutputlib.php @@ -30,16 +30,60 @@ if (!defined('MOODLE_INTERNAL')) { require_once($CFG->libdir . '/outputlib.php'); -// TODO this is needed until MDL-16438 is committed. -function get_plugin_dir($module) { - global $CFG; - return $CFG->dirroot; +/** + * Unit tests for the pix_icon_finder class. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class pix_icon_finder_test extends UnitTestCase { + public function test_old_icon_url() { + global $CFG; + $if = new pix_icon_finder(new theme_config()); + $this->assertEqual($CFG->httpswwwroot . '/pix/i/course.gif', $if->old_icon_url('i/course')); + } + + /* Implement interface method. */ + public function test_mod_icon_url() { + global $CFG; + $if = new pix_icon_finder(new theme_config()); + $this->assertEqual($CFG->httpswwwroot . '/mod/quiz/icon.gif', $if->mod_icon_url('icon', 'quiz')); + } +} + + +/** + * Unit tests for the standard_renderer_factory class. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class theme_icon_finder_test extends UnitTestCase { + public function test_old_icon_url_test() { + global $CFG; + $theme = new theme_config(); + $theme->name = 'test'; + $if = new theme_icon_finder($theme); + $this->assertEqual($CFG->httpsthemewww . '/test/pix/i/course.gif', $if->old_icon_url('i/course')); + } + + /* Implement interface method. */ + public function test_mod_icon_url() { + global $CFG; + $theme = new theme_config(); + $theme->name = 'test'; + $if = new theme_icon_finder($theme); + $this->assertEqual($CFG->httpsthemewww . '/test/pix/mod/quiz/icon.gif', $if->mod_icon_url('icon', 'quiz')); + } } /** * Subclass of renderer_factory_base for testing. Implement abstract method and * count calls, so we can test caching behaviour. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class testable_renderer_factory extends renderer_factory_base { public $createcalls = array(); @@ -61,8 +105,11 @@ class testable_renderer_factory extends renderer_factory_base { /** * Renderer class for testing. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class moodle_test_renderer extends moodle_core_renderer { +class moodle_mod_test_renderer extends moodle_core_renderer { public function __construct($containerstack, $page) { parent::__construct($containerstack, $page, null); } @@ -121,9 +168,9 @@ class renderer_factory_base_test extends UnitTestCase { // Set up. $factory = new testable_renderer_factory(); // Exercise SUT. - $classname = $factory->standard_renderer_class_for_module('test'); + $classname = $factory->standard_renderer_class_for_module('mod_test'); // Verify outcome - $this->assertEqual('moodle_test_renderer', $classname); + $this->assertEqual('moodle_mod_test_renderer', $classname); } public function test_standard_renderer_class_for_module_unknown() { @@ -161,8 +208,8 @@ class standard_renderer_factory_test extends UnitTestCase { } public function test_get_test_renderer() { - $renderer = $this->factory->get_renderer('test', new moodle_page); - $this->assertIsA($renderer, 'moodle_test_renderer'); + $renderer = $this->factory->get_renderer('mod_test', new moodle_page); + $this->assertIsA($renderer, 'moodle_mod_test_renderer'); } } @@ -192,8 +239,8 @@ class custom_corners_renderer_factory_test extends UnitTestCase { } public function test_get_test_renderer() { - $renderer = $this->factory->get_renderer('test', new moodle_page); - $this->assertIsA($renderer, 'moodle_test_renderer'); + $renderer = $this->factory->get_renderer('mod_test', new moodle_page); + $this->assertIsA($renderer, 'moodle_mod_test_renderer'); } } @@ -304,25 +351,25 @@ class theme_overridden_renderer_factory_test extends UnitTestCase { $factory = new testable_theme_overridden_renderer_factory($theme, $this->page); // Exercise SUT. - $renderer = $factory->get_renderer('test', new moodle_page); + $renderer = $factory->get_renderer('mod_test', new moodle_page); // Verify outcome - $this->assertIsA($renderer, 'moodle_test_renderer'); + $this->assertIsA($renderer, 'moodle_mod_test_renderer'); } public function test_get_renderer_overridden() { // Set up - be very careful because the class under test uses require-once. Pick a unique theme name. $theme = $this->make_theme('testrenderertheme'); $this->write_renderers_file($theme, ' - class testrenderertheme_test_renderer extends moodle_test_renderer { + class testrenderertheme_mod_test_renderer extends moodle_mod_test_renderer { }'); $factory = new testable_theme_overridden_renderer_factory($theme, $this->page); // Exercise SUT. - $renderer = $factory->get_renderer('test', new moodle_page); + $renderer = $factory->get_renderer('mod_test', new moodle_page); // Verify outcome - $this->assertIsA($renderer, 'testrenderertheme_test_renderer'); + $this->assertIsA($renderer, 'testrenderertheme_mod_test_renderer'); } public function test_get_renderer_overridden_in_parent() { @@ -519,18 +566,18 @@ class template_renderer_factory_test extends UnitTestCase { $theme = $this->make_theme('mytheme'); $theme->parent = 'parenttheme'; $this->make_theme_template_dir('mytheme', 'core'); - $this->make_theme_template_dir('parenttheme', 'test'); - $this->make_theme_template_dir('standardtemplate', 'test'); + $this->make_theme_template_dir('parenttheme', 'mod_test'); + $this->make_theme_template_dir('standardtemplate', 'mod_test'); $factory = new testable_template_renderer_factory($theme); // Exercise SUT. - $renderer = $factory->get_renderer('test', $this->page); + $renderer = $factory->get_renderer('mod_test', $this->page); // Verify outcome - $this->assertEqual('moodle_test_renderer', $renderer->get_copied_class()); + $this->assertEqual('moodle_mod_test_renderer', $renderer->get_copied_class()); $this->assertEqual(array( - $CFG->themedir . '/parenttheme/templates/test', - $CFG->themedir . '/standardtemplate/templates/test'), + $CFG->themedir . '/parenttheme/templates/mod_test', + $CFG->themedir . '/standardtemplate/templates/mod_test'), $renderer->get_search_paths()); } } @@ -709,7 +756,7 @@ class template_renderer_test extends UnitTestCase { $this->templatefolder = $CFG->dataroot . '/temp/template_renderer_fixtures/test'; make_upload_directory('temp/template_renderer_fixtures/test'); $page = new moodle_page; - $this->renderer = new template_renderer('moodle_test_renderer', + $this->renderer = new template_renderer('moodle_mod_test_renderer', array($this->templatefolder), $page); } -- 2.39.5