From 166ac0a35d0cf95d0a4b6f2afc7f54ed784b5f67 Mon Sep 17 00:00:00 2001 From: Sam Hemelryk Date: Thu, 12 Sep 2013 09:51:58 +1200 Subject: [PATCH] MDL-41398 output: implemented maintenance renderer --- lib/outputfactories.php | 21 ++-- lib/outputrenderers.php | 204 +++++++++++++++++++++++++++++++++ lib/pagelib.php | 16 ++- lib/tests/moodle_page_test.php | 78 ++++++++++++- theme/upgrade.txt | 25 ++++ 5 files changed, 335 insertions(+), 9 deletions(-) diff --git a/lib/outputfactories.php b/lib/outputfactories.php index bb854f41261..b06ee92444c 100644 --- a/lib/outputfactories.php +++ b/lib/outputfactories.php @@ -31,6 +31,9 @@ defined('MOODLE_INTERNAL') || die(); /** General rendering target, usually normal browser page */ define('RENDERER_TARGET_GENERAL', 'general'); +/** General rendering target, usually normal browser page, but with limited capacity to avoid API use */ +define('RENDERER_TARGET_MAINTENANCE', 'maintenance'); + /** Plain text rendering for CLI scripts and cron */ define('RENDERER_TARGET_CLI', 'cli'); @@ -130,8 +133,10 @@ abstract class renderer_factory_base implements renderer_factory { * @return array two element array, first element is target, second the target suffix string */ protected function get_target_suffix($target) { - if (empty($target)) { - // automatically guessed defaults + if (empty($target) || $target === RENDERER_TARGET_MAINTENANCE) { + // If the target hasn't been specified we need to guess the defaults. + // We also override the target with the default if the maintenance target has been provided. + // This ensures we don't use the maintenance renderer if we are processing a special target. if (CLI_SCRIPT) { $target = RENDERER_TARGET_CLI; } else if (AJAX_SCRIPT) { @@ -144,6 +149,7 @@ abstract class renderer_factory_base implements renderer_factory { case RENDERER_TARGET_AJAX: $suffix = '_ajax'; break; case RENDERER_TARGET_TEXTEMAIL: $suffix = '_textemail'; break; case RENDERER_TARGET_HTMLEMAIL: $suffix = '_htmlemail'; break; + case RENDERER_TARGET_MAINTENANCE: $suffix = '_maintenance'; break; default: $target = RENDERER_TARGET_GENERAL; $suffix = ''; } @@ -160,11 +166,12 @@ abstract class renderer_factory_base implements renderer_factory { * @param string $component name such as 'core', 'mod_forum' or 'qtype_multichoice'. * @param string $subtype optional subtype such as 'news' resulting to 'mod_forum_news' * @return string the name of the standard renderer class for that module. + * @throws coding_exception */ protected function standard_renderer_classname($component, $subtype = null) { - global $CFG; // needed in included files + global $CFG; // Needed in included files. - // standardize component name ala frankenstyle + // Standardize component name ala frankenstyle. list($plugin, $type) = core_component::normalize_component($component); if ($type === null) { $component = $plugin; @@ -173,9 +180,9 @@ abstract class renderer_factory_base implements renderer_factory { } if ($component !== 'core') { - // renderers are stored in renderer.php files + // Renderers are stored in renderer.php files. if (!$compdirectory = core_component::get_component_directory($component)) { - throw new coding_exception('Invalid component specified in renderer request'); + throw new coding_exception('Invalid component specified in renderer request', $component); } $rendererfile = $compdirectory . '/renderer.php'; if (file_exists($rendererfile)) { @@ -185,7 +192,7 @@ abstract class renderer_factory_base implements renderer_factory { } else if (!empty($subtype)) { $coresubsystems = core_component::get_core_subsystems(); if (!array_key_exists($subtype, $coresubsystems)) { // There may be nulls. - throw new coding_exception('Invalid core subtype "' . $subtype . '" in renderer request'); + throw new coding_exception('Invalid core subtype "' . $subtype . '" in renderer request', $subtype); } if ($coresubsystems[$subtype]) { $rendererfile = $coresubsystems[$subtype] . '/renderer.php'; diff --git a/lib/outputrenderers.php b/lib/outputrenderers.php index 1ca922ad352..186ce3d64b9 100644 --- a/lib/outputrenderers.php +++ b/lib/outputrenderers.php @@ -199,6 +199,12 @@ class plugin_renderer_base extends renderer_base { * @param string $target one of rendering target constants */ public function __construct(moodle_page $page, $target) { + if (empty($target) && $page->pagelayout === 'maintenance') { + // If the page is using the maintenance layout then we're going to force the target to maintenance. + // This way we'll get a special maintenance renderer that is designed to block access to API's that are likely + // unavailable for this page layout. + $target = RENDERER_TARGET_MAINTENANCE; + } $this->output = $page->get_renderer('core', null, $target); parent::__construct($page, $target); } @@ -3764,3 +3770,201 @@ class core_media_renderer extends plugin_renderer_base { return $this->embeddablemarkers; } } + +/** + * The maintenance renderer. + * + * The purpose of this renderer is to block out the core renderer methods that are not usable when the site + * is running a maintenance related task. + * It must always extend the core_renderer as we switch from the core_renderer to this renderer in a couple of places. + * + * @since 2.6 + * @package core + * @category output + * @copyright 2013 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_renderer_maintenance extends core_renderer { + + /** + * Initialises the renderer instance. + * @param moodle_page $page + * @param string $target + * @throws coding_exception + */ + public function __construct(moodle_page $page, $target) { + if ($target !== RENDERER_TARGET_MAINTENANCE || $page->pagelayout !== 'maintenance') { + throw new coding_exception('Invalid request for the maintenance renderer.'); + } + parent::__construct($page, $target); + } + + /** + * Does nothing. The maintenance renderer cannot produce blocks. + * + * @param block_contents $bc + * @param string $region + * @return string + */ + public function block(block_contents $bc, $region) { + // Computer says no blocks. + // debugging('Please do not use $OUTPUT->'.__FUNCTION__.'() when performing maintenance tasks.', DEBUG_DEVELOPER); + return ''; + } + + /** + * Does nothing. The maintenance renderer cannot produce blocks. + * + * @param string $region + * @param array $classes + * @param string $tag + * @return string + */ + public function blocks($region, $classes = array(), $tag = 'aside') { + // Computer says no blocks. + // debugging('Please do not use $OUTPUT->'.__FUNCTION__.'() when performing maintenance tasks.', DEBUG_DEVELOPER); + return ''; + } + + /** + * Does nothing. The maintenance renderer cannot produce blocks. + * + * @param string $region + * @return string + */ + public function blocks_for_region($region) { + // Computer says no blocks for region. + // debugging('Please do not use $OUTPUT->'.__FUNCTION__.'() when performing maintenance tasks.', DEBUG_DEVELOPER); + return ''; + } + + /** + * Does nothing. The maintenance renderer cannot produce a course content header. + * + * @param bool $onlyifnotcalledbefore + * @return string + */ + public function course_content_header($onlyifnotcalledbefore = false) { + // Computer says no course content header. + // debugging('Please do not use $OUTPUT->'.__FUNCTION__.'() when performing maintenance tasks.', DEBUG_DEVELOPER); + return ''; + } + + /** + * Does nothing. The maintenance renderer cannot produce a course content footer. + * + * @param bool $onlyifnotcalledbefore + * @return string + */ + public function course_content_footer($onlyifnotcalledbefore = false) { + // Computer says no course content footer. + // debugging('Please do not use $OUTPUT->'.__FUNCTION__.'() when performing maintenance tasks.', DEBUG_DEVELOPER); + return ''; + } + + /** + * Does nothing. The maintenance renderer cannot produce a course header. + * + * @return string + */ + public function course_header() { + // Computer says no course header. + // debugging('Please do not use $OUTPUT->'.__FUNCTION__.'() when performing maintenance tasks.', DEBUG_DEVELOPER); + return ''; + } + + /** + * Does nothing. The maintenance renderer cannot produce a course footer. + * + * @return string + */ + public function course_footer() { + // Computer says no course footer. + // debugging('Please do not use $OUTPUT->'.__FUNCTION__.'() when performing maintenance tasks.', DEBUG_DEVELOPER); + return ''; + } + + /** + * Does nothing. The maintenance renderer cannot produce a custom menu. + * + * @param string $custommenuitems + * @return string + */ + public function custom_menu($custommenuitems = '') { + // Computer says no custom menu. + // debugging('Please do not use $OUTPUT->'.__FUNCTION__.'() when performing maintenance tasks.', DEBUG_DEVELOPER); + return ''; + } + + /** + * Does nothing. The maintenance renderer cannot produce a file picker. + * + * @param array $options + * @return string + */ + public function file_picker($options) { + // Computer says no file picker. + // debugging('Please do not use $OUTPUT->'.__FUNCTION__.'() when performing maintenance tasks.', DEBUG_DEVELOPER); + return ''; + } + + /** + * Does nothing. The maintenance renderer cannot produce and HTML file tree. + * + * @param array $dir + * @return string + */ + public function htmllize_file_tree($dir) { + // Hell no we don't want no htmllized file tree. + // Also why on earth is this function on the core renderer??? + // debugging('Please do not use $OUTPUT->'.__FUNCTION__.'() when performing maintenance tasks.', DEBUG_DEVELOPER); + return ''; + + } + + /** + * Does nothing. The maintenance renderer does not support JS. + * + * @param block_contents $bc + */ + public function init_block_hider_js(block_contents $bc) { + // Computer says no JavaScript. + // Do nothing, ridiculous method. + } + + /** + * Does nothing. The maintenance renderer cannot produce language menus. + * + * @return string + */ + public function lang_menu() { + // Computer says no lang menu. + // debugging('Please do not use $OUTPUT->'.__FUNCTION__.'() when performing maintenance tasks.', DEBUG_DEVELOPER); + return ''; + } + + /** + * Does nothing. The maintenance renderer has no need for login information. + * + * @param null $withlinks + * @return string + */ + public function login_info($withlinks = null) { + // Computer says no login info. + // debugging('Please do not use $OUTPUT->'.__FUNCTION__.'() when performing maintenance tasks.', DEBUG_DEVELOPER); + return ''; + } + + /** + * Does nothing. The maintenance renderer cannot produce user pictures. + * + * @param stdClass $user + * @param array $options + * @return string + */ + public function user_picture(stdClass $user, array $options = null) { + // Computer says no user pictures. + // debugging('Please do not use $OUTPUT->'.__FUNCTION__.'() when performing maintenance tasks.', DEBUG_DEVELOPER); + return ''; + } +} diff --git a/lib/pagelib.php b/lib/pagelib.php index 9c7db77892a..d8456f94f9e 100644 --- a/lib/pagelib.php +++ b/lib/pagelib.php @@ -769,6 +769,13 @@ class moodle_page { * @return renderer_base */ public function get_renderer($component, $subtype = null, $target = null) { + $target = null; + if ($this->pagelayout === 'maintenance') { + // If the page is using the maintenance layout then we're going to force target to maintenance. + // This leads to a special core renderer that is designed to block access to API's that are likely unavailable for this + // page layout. + $target = RENDERER_TARGET_MAINTENANCE; + } return $this->magic_get_theme()->get_renderer($this, $component, $subtype, $target); } @@ -1475,7 +1482,14 @@ class moodle_page { } if ($this === $PAGE) { - $OUTPUT = $this->get_renderer('core'); + $target = null; + if ($this->pagelayout === 'maintenance') { + // If the page is using the maintenance layout then we're going to force target to maintenance. + // This leads to a special core renderer that is designed to block access to API's that are likely unavailable for this + // page layout. + $target = RENDERER_TARGET_MAINTENANCE; + } + $OUTPUT = $this->get_renderer('core', null, $target); } $this->_wherethemewasinitialised = debug_backtrace(); diff --git a/lib/tests/moodle_page_test.php b/lib/tests/moodle_page_test.php index a32e91a7600..bf2631745af 100644 --- a/lib/tests/moodle_page_test.php +++ b/lib/tests/moodle_page_test.php @@ -31,6 +31,10 @@ require_once($CFG->libdir . '/blocklib.php'); class core_moodle_page_testcase extends advanced_testcase { + + /** + * @var testable_moodle_page + */ protected $testpage; public function setUp() { @@ -570,8 +574,80 @@ class core_moodle_page_testcase extends advanced_testcase { $expectedcaps = array('moodle/course:manageactivities', 'moodle/site:other', 'moodle/site:manageblocks'); $this->assertEquals(array_values($expectedcaps), array_values($actualcaps)); } -} + /** + * Test getting a renderer. + */ + public function test_get_renderer() { + global $OUTPUT, $PAGE; + $oldoutput = $OUTPUT; + $oldpage = $PAGE; + $PAGE = $this->testpage; + + $this->testpage->set_pagelayout('standard'); + $this->assertEquals('standard', $this->testpage->pagelayout); + // Initialise theme and output for the next tests. + $this->testpage->initialise_theme_and_output(); + // Check the generated $OUTPUT object is a core renderer. + $this->assertInstanceOf('core_renderer', $OUTPUT); + // Check we can get a core renderer if we explicitly request one (no component). + $this->assertInstanceOf('core_renderer', $this->testpage->get_renderer('core')); + // Check we get a CLI renderer if we request a maintenance renderer. The CLI target should take precedence. + $this->assertInstanceOf('core_renderer_cli', + $this->testpage->get_renderer('core', null, RENDERER_TARGET_MAINTENANCE)); + + // Check we can get a coures renderer if we explicitly request one (valid component). + $this->assertInstanceOf('core_course_renderer', $this->testpage->get_renderer('core', 'course')); + + // Check a properly invalid component. + try { + $this->testpage->get_renderer('core', 'monkeys'); + $this->fail('Request for renderer with invalid component didn\'t throw expected exception.'); + } catch (coding_exception $exception) { + $this->assertEquals('monkeys', $exception->debuginfo); + } + + $PAGE = $oldpage; + $OUTPUT = $oldoutput; + } + + /** + * Tests getting a renderer with a maintenance layout. + * + * This layout has special hacks in place in order to deliver a "maintenance" renderer. + */ + public function test_get_renderer_maintenance() { + global $OUTPUT, $PAGE; + $oldoutput = $OUTPUT; + $oldpage = $PAGE; + $PAGE = $this->testpage; + + $this->testpage->set_pagelayout('maintenance'); + $this->assertEquals('maintenance', $this->testpage->pagelayout); + // Initialise theme and output for the next tests. + $this->testpage->initialise_theme_and_output(); + // Check the generated $OUTPUT object is a core cli renderer. + // It shouldn't be maintenance because there the cli target should take greater precedence. + $this->assertInstanceOf('core_renderer_cli', $OUTPUT); + // Check we can get a core renderer if we explicitly request one (no component). + $this->assertInstanceOf('core_renderer', $this->testpage->get_renderer('core')); + // Check we get a CLI renderer if we request a maintenance renderer. The CLI target should take precedence. + $this->assertInstanceOf('core_renderer_cli', + $this->testpage->get_renderer('core', null, RENDERER_TARGET_MAINTENANCE)); + // Check we can get a coures renderer if we explicitly request one (valid component). + $this->assertInstanceOf('core_course_renderer', $this->testpage->get_renderer('core', 'course')); + + try { + $this->testpage->get_renderer('core', 'monkeys'); + $this->fail('Request for renderer with invalid component didn\'t throw expected exception.'); + } catch (coding_exception $exception) { + $this->assertEquals('monkeys', $exception->debuginfo); + } + + $PAGE = $oldpage; + $OUTPUT = $oldoutput; + } +} /** * Test-specific subclass to make some protected things public. diff --git a/theme/upgrade.txt b/theme/upgrade.txt index 5c4ff146b83..811a92b4b59 100644 --- a/theme/upgrade.txt +++ b/theme/upgrade.txt @@ -15,6 +15,11 @@ Notes: * For the themes based on bootstrapbase please also read theme/bootstrapbase/upgrade.txt * A new component action_menu is now used to display editing icons for courses and blocks within a drop down. If you have a theme that doesn't extend base, canvas, or clean then you will need to style for this new component within your theme. +* The maintenance layout now has a special renderer that extends the core_renderer in order to prevent some methods from interacting + with the database. Please be aware that for the maintenance layout some methods now always return an empty string. + This has been done because it is important that during maintenance routines we don't show any links, interact with the database, + or interact with caches as doing so may lead to errors, + Please see the maintenance renderer notes below for details on the functions affected. Renderer changes: * core_course_renderer::course_section_cm_edit_actions has two new optional arguments and now uses and action_menu component. @@ -31,6 +36,26 @@ Selector changes: * Classes ".header and .headingblock" were removed from all front page content headings. * Classes ".headingblock .header .tag-header" were removed from the tag index page +Maintenance renderer notes: +When the maintenance layout is being used $OUTPUT will be an instance of core_renderer_maintenance. +This renderer mimics the core_renderer except that the following functions always return an empty string. +* core_maintenance_renderer::block +* core_maintenance_renderer::blocks +* core_maintenance_renderer::blocks_for_regions +* core_maintenance_renderer::course_header +* core_maintenance_renderer::course_footer +* core_maintenance_renderer::course_content_header +* core_maintenance_renderer::course_content_footer +* core_maintenance_renderer::custom_menu +* core_maintenance_renderer::file_picker +* core_maintenance_renderer::htmllize_file_tree +* core_maintenance_renderer::lang_menu +* core_maintenance_renderer::login_info +* core_maintenance_renderer::user_picture + +If you have overridden methods of the core_renderer in your theme and want those changes to be shown during times of maintenance you +will also need to override the core_renderer_maintenance and copy your customisations from the core_renderer to that. + === 2.5.1 === Notes: