From fcc383db0f18b7bf53639f966436a39fb68d3cb3 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Fri, 1 May 2015 16:43:21 +0800 Subject: [PATCH] MDL-50085 Mustache : Strictly define the search paths for templates. This change moves several repeated sections of code that was searching for templates and valid template locations to a new class. It adds unit tests for the new class and verifies subsystem support for templates. --- admin/tool/templatelibrary/classes/api.php | 49 ++++--- .../classes/output/list_templates_page.php | 4 +- .../lang/en/tool_templatelibrary.php | 2 +- lib/classes/output/external.php | 35 +---- .../output/mustache_filesystem_loader.php | 20 +-- .../output/mustache_template_finder.php | 120 ++++++++++++++++++ lib/outputrenderers.php | 47 +------ lib/tests/mustache_template_finder_test.php | 98 ++++++++++++++ 8 files changed, 268 insertions(+), 107 deletions(-) create mode 100644 lib/classes/output/mustache_template_finder.php create mode 100644 lib/tests/mustache_template_finder_test.php diff --git a/admin/tool/templatelibrary/classes/api.php b/admin/tool/templatelibrary/classes/api.php index c818c866317..13cfbfebea7 100644 --- a/admin/tool/templatelibrary/classes/api.php +++ b/admin/tool/templatelibrary/classes/api.php @@ -23,6 +23,7 @@ */ namespace tool_templatelibrary; +use core\output\mustache_template_finder; use stdClass; use core_component; use coding_exception; @@ -43,7 +44,7 @@ class api { * @param string $search Search string to optionally filter the list of templates. * @return array[string] Where each template is in the form "component/templatename". */ - public static function list_templates($component = '', $search = '') { + public static function list_templates($component = '', $search = '', $themename = '') { global $CFG; $templatedirs = array(); @@ -51,41 +52,53 @@ class api { if ($component != '') { // Just look at one component for templates. - $dir = core_component::get_component_directory($component); - if (!$dir) { - return $templatedirs; - } + $dirs = mustache_template_finder::get_template_directories_for_component($component, $themename); - $templatedirs[$component] = $dir . '/templates'; + $templatedirs[$component] = $dirs; } else { - // Look at all the templates dirs for all installed plugins. - $dir = $CFG->libdir . '/templates'; - if (!empty($dir) && is_dir($dir)) { - $templatedirs['core'] = $dir; + // Look at all the templates dirs for core. + $templatedirs['core'] = mustache_template_finder::get_template_directories_for_component('core', $themename); + + // Look at all the templates dirs for subsystems. + $subsystems = core_component::get_core_subsystems(); + foreach ($subsystems as $subsystem => $dir) { + $dir .= '/templates'; + if (is_dir($dir)) { + $dirs = mustache_template_finder::get_template_directories_for_component('core_' . $subsystem, $themename); + $templatedirs['core_' . $subsystem] = $dirs; + } } + + // Look at all the templates dirs for plugins. $plugintypes = core_component::get_plugin_types(); foreach ($plugintypes as $type => $dir) { $plugins = core_component::get_plugin_list_with_file($type, 'templates', false); foreach ($plugins as $plugin => $dir) { if (!empty($dir) && is_dir($dir)) { - $templatedirs[$type . '_' . $plugin] = $dir; + $pluginname = $type . '_' . $plugin; + $dirs = mustache_template_finder::get_template_directories_for_component($pluginname, $themename); + $templatedirs[$pluginname] = $dirs; } } } } - foreach ($templatedirs as $templatecomponent => $dir) { - // List it. - $files = glob($dir . '/*.mustache'); + foreach ($templatedirs as $templatecomponent => $dirs) { + foreach ($dirs as $dir) { + // List it. + $files = glob($dir . '/*.mustache'); - foreach ($files as $file) { - $templatename = basename($file, '.mustache'); - if ($search == '' || strpos($templatename, $search) !== false) { - $results[] = $templatecomponent . '/' . $templatename; + foreach ($files as $file) { + $templatename = basename($file, '.mustache'); + if ($search == '' || strpos($templatename, $search) !== false) { + $results[$templatecomponent . '/' . $templatename] = 1; + } } } } + $results = array_keys($results); + sort($results); return $results; } diff --git a/admin/tool/templatelibrary/classes/output/list_templates_page.php b/admin/tool/templatelibrary/classes/output/list_templates_page.php index 8377e8bccb8..cecf9cb6a12 100644 --- a/admin/tool/templatelibrary/classes/output/list_templates_page.php +++ b/admin/tool/templatelibrary/classes/output/list_templates_page.php @@ -59,8 +59,8 @@ class list_templates_page implements renderable, templatable { foreach ($components as $component) { $info = new stdClass(); $info->component = $component; - if ($component == 'core') { - $info->name = get_string('core_component', 'tool_templatelibrary'); + if (strpos($component, 'core') === 0) { + $info->name = get_string('coresubsystem', 'tool_templatelibrary', $component); } else { $info->name = $pluginmanager->plugin_name($component); } diff --git a/admin/tool/templatelibrary/lang/en/tool_templatelibrary.php b/admin/tool/templatelibrary/lang/en/tool_templatelibrary.php index 87b8ea0303c..1ef475e1e78 100644 --- a/admin/tool/templatelibrary/lang/en/tool_templatelibrary.php +++ b/admin/tool/templatelibrary/lang/en/tool_templatelibrary.php @@ -24,7 +24,7 @@ $string['all'] = 'All components'; $string['component'] = 'Component'; -$string['core_component'] = 'Moodle core'; +$string['coresubsystem'] = 'Core subsystem ({$a})'; $string['documentation'] = 'Documentation'; $string['example'] = 'Example'; $string['noresults'] = 'No results'; diff --git a/lib/classes/output/external.php b/lib/classes/output/external.php index 8e53119f5a8..cd665fc1f22 100644 --- a/lib/classes/output/external.php +++ b/lib/classes/output/external.php @@ -85,38 +85,11 @@ class external extends external_api { $template = $params['template']; $themename = $params['themename']; - // Check if this is a valid component. - $componentdir = core_component::get_component_directory($component); - if (empty($componentdir)) { - throw new moodle_exception('filenotfound', 'error'); - } - // Places to look. - $candidates = array(); - // Theme dir. - $root = $CFG->dirroot; + $templatename = $component . '/' . $template; - $themeconfig = theme_config::load($themename); - - $candidate = "${root}/theme/${themename}/templates/${component}/${template}.mustache"; - $candidates[] = $candidate; - // Theme parents dir. - foreach ($themeconfig->parents as $theme) { - $candidate = "${root}/theme/${theme}/templates/${component}/${template}.mustache"; - $candidates[] = $candidate; - } - // Component dir. - $candidate = "${componentdir}/templates/${template}.mustache"; - $candidates[] = $candidate; - $templatestr = false; - foreach ($candidates as $candidate) { - if (file_exists($candidate)) { - $templatestr = file_get_contents($candidate); - break; - } - } - if ($templatestr === false) { - throw new moodle_exception('filenotfound', 'error'); - } + // Will throw exceptions if the template does not exist. + $filename = mustache_template_finder::get_template_filename($templatename, $themename); + $templatestr = file_get_contents($filename); return $templatestr; } diff --git a/lib/classes/output/mustache_filesystem_loader.php b/lib/classes/output/mustache_filesystem_loader.php index b4caa18eda3..2bba5b8a1ab 100644 --- a/lib/classes/output/mustache_filesystem_loader.php +++ b/lib/classes/output/mustache_filesystem_loader.php @@ -28,7 +28,7 @@ namespace core\output; use coding_exception; /** - * Perform some custom name mapping for template file names (strip leading component/). + * Perform some custom name mapping for template file names. * * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later @@ -36,22 +36,22 @@ use coding_exception; */ class mustache_filesystem_loader extends \Mustache_Loader_FilesystemLoader { + /** + * Provide a default no-args constructor (we don't really need anything). + */ + public function __construct() { + } + /** * Helper function for getting a Mustache template file name. - * Strips the leading component as we are already limited to the correct directories. + * Use the leading component to restrict us specific directories. * * @param string $name * * @return string Template file name */ protected function getFileName($name) { - if (strpos($name, '/') === false) { - throw new coding_exception('Templates names must be specified as "componentname/templatename" (' . $name . ' requested) '); - } - list($component, $templatename) = explode('/', $name, 2); - if (strpos($templatename, '/') !== false) { - throw new coding_exception('Templates cannot be placed in sub directories (' . $name . ' requested)'); - } - return parent::getFileName($templatename); + // Call the Moodle template finder. + return mustache_template_finder::get_template_filename($name); } } diff --git a/lib/classes/output/mustache_template_finder.php b/lib/classes/output/mustache_template_finder.php new file mode 100644 index 00000000000..627861868be --- /dev/null +++ b/lib/classes/output/mustache_template_finder.php @@ -0,0 +1,120 @@ +. + +/** + * List the valid locations to search for a template with a given name. + * + * @package core + * @category output + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\output; + +use coding_exception; +use moodle_exception; +use core_component; + +/** + * Get information about valid locations for mustache templates. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since 2.9 + */ +class mustache_template_finder { + + /** + * Helper function for getting a list of valid template directories for a specific component. + * + * @param string $component + * + * @return string[] List of valid directories for templates for this compoonent. Directories are not checked for existence. + */ + public static function get_template_directories_for_component($component, $themename = '') { + global $CFG, $PAGE; + + // Default the param. + if ($themename == '') { + $themename = $PAGE->theme->name; + } + + // Clean params for safety. + $component = clean_param($component, PARAM_COMPONENT); + $themename = clean_param($themename, PARAM_COMPONENT); + + // Validate the component. + $dirs = array(); + $compdirectory = core_component::get_component_directory($component); + if (!$compdirectory) { + throw new coding_exception("Component was not valid:" . s($component)); + } + + // Find the parent themes. + $parents = array(); + if ($themename === $PAGE->theme->name) { + $parents = $PAGE->theme->parents; + } else { + $themeconfig = theme_config::load($themename); + $parents = $themeconfig->parents; + } + + // First check the theme. + $dirs[] = $CFG->dirroot . '/theme/' . $themename . '/templates/' . $component . '/'; + // Now check the parent themes. + // Search each of the parent themes second. + foreach ($parents as $parent) { + $dirs[] = $CFG->dirroot . '/theme/' . $parent . '/templates/' . $component . '/'; + } + + $dirs[] = $compdirectory . '/templates/'; + + return $dirs; + } + + /** + * Helper function for getting a filename for a template from the template name. + * + * @param string $name - This is the componentname/templatename combined. + * + * @return string + */ + public static function get_template_filename($name, $themename = '') { + global $CFG, $PAGE; + + if (strpos($name, '/') === false) { + throw new coding_exception('Templates names must be specified as "componentname/templatename"' . + ' (' . $name . ' requested) '); + } + list($component, $templatename) = explode('/', $name, 2); + $component = clean_param($component, PARAM_COMPONENT); + if (strpos($templatename, '/') !== false) { + throw new coding_exception('Templates cannot be placed in sub directories (' . $name . ' requested)'); + } + + $dirs = self::get_template_directories_for_component($component, $themename); + + foreach ($dirs as $dir) { + $candidate = $dir . $templatename . '.mustache'; + if (file_exists($candidate)) { + return $candidate; + } + } + + throw new moodle_exception('filenotfound', 'error'); + } +} diff --git a/lib/outputrenderers.php b/lib/outputrenderers.php index 1df85acd5db..94229e533de 100644 --- a/lib/outputrenderers.php +++ b/lib/outputrenderers.php @@ -118,53 +118,10 @@ class renderer_base { $themename = $this->page->theme->name; $themerev = theme_get_revision(); - $target = $this->target; - $cachedir = make_localcache_directory("mustache/$themerev/$themename/$target"); - $loaderoptions = array(); - - // Where are all the places we should look for templates? - - $suffix = $this->component; - if ($this->subtype !== null) { - $suffix .= '_' . $this->subtype; - } - - // Start with an empty list. - $loader = new Mustache_Loader_CascadingLoader(array()); - $loaderdir = $CFG->dirroot . '/theme/' . $themename . '/templates/' . $suffix; - if (is_dir($loaderdir)) { - $loader->addLoader(new \core\output\mustache_filesystem_loader($loaderdir, $loaderoptions)); - } - - // Search each of the parent themes second. - foreach ($this->page->theme->parents as $parent) { - $loaderdir = $CFG->dirroot . '/theme/' . $parent . '/templates/' . $suffix; - if (is_dir($loaderdir)) { - $loader->addLoader(new \core\output\mustache_filesystem_loader($loaderdir, $loaderoptions)); - } - } - - // Look in a components templates dir for a base implementation. - - $compdirectory = core_component::get_component_directory($suffix); - if ($compdirectory) { - $loaderdir = $compdirectory . '/templates'; - if (is_dir($loaderdir)) { - $loader->addLoader(new \core\output\mustache_filesystem_loader($loaderdir, $loaderoptions)); - } - } - - // Look in the core templates dir as a final fallback. - - $compdirectory = $CFG->libdir; - if ($compdirectory) { - $loaderdir = $compdirectory . '/templates'; - if (is_dir($loaderdir)) { - $loader->addLoader(new \core\output\mustache_filesystem_loader($loaderdir, $loaderoptions)); - } - } + $cachedir = make_localcache_directory("mustache/$themerev/$themename"); + $loader = new \core\output\mustache_filesystem_loader(); $stringhelper = new \core\output\mustache_string_helper(); $jshelper = new \core\output\mustache_javascript_helper($this->page->requires); $pixhelper = new \core\output\mustache_pix_helper($this); diff --git a/lib/tests/mustache_template_finder_test.php b/lib/tests/mustache_template_finder_test.php new file mode 100644 index 00000000000..61d9a832b2e --- /dev/null +++ b/lib/tests/mustache_template_finder_test.php @@ -0,0 +1,98 @@ +. + +/** + * Unit tests for lib/classes/output/mustache_template_finder.php + * + * @package core + * @category phpunit + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use core\output\mustache_template_finder; + +/** + * Unit tests for the Mustache template finder class (contains logic about + * resolving mustache template locations. + */ +class core_output_mustache_template_finder_testcase extends advanced_testcase { + + public function test_get_template_directories_for_component() { + global $CFG; + + // Test a plugin. + $dirs = mustache_template_finder::get_template_directories_for_component('mod_assign', 'clean'); + + $correct = array( + 'theme/clean/templates/mod_assign/', + 'theme/bootstrapbase/templates/mod_assign/', + 'mod/assign/templates/' + ); + foreach ($dirs as $index => $dir) { + $this->assertSame($dir, $CFG->dirroot . '/' . $correct[$index]); + } + // Test a subsystem. + $dirs = mustache_template_finder::get_template_directories_for_component('core_user', 'clean'); + + $correct = array( + 'theme/clean/templates/core_user/', + 'theme/bootstrapbase/templates/core_user/', + 'user/templates/' + ); + foreach ($dirs as $index => $dir) { + $this->assertSame($dir, $CFG->dirroot . '/' . $correct[$index]); + } + // Test core. + $dirs = mustache_template_finder::get_template_directories_for_component('core', 'clean'); + + $correct = array( + 'theme/clean/templates/core/', + 'theme/bootstrapbase/templates/core/', + 'lib/templates/' + ); + foreach ($dirs as $index => $dir) { + $this->assertSame($dir, $CFG->dirroot . '/' . $correct[$index]); + } + return; + } + + /** + * @expectedException coding_exception + */ + public function test_invalid_get_template_directories_for_component() { + // Test something invalid. + $dirs = mustache_template_finder::get_template_directories_for_component('octopus', 'clean'); + } + + public function test_get_template_filename() { + global $CFG; + + $filename = mustache_template_finder::get_template_filename('core/pix_icon', 'clean'); + $correct = $CFG->dirroot . '/lib/templates/pix_icon.mustache'; + $this->assertSame($correct, $filename); + } + + /** + * @expectedException moodle_exception + */ + public function test_invalid_get_template_filename() { + // Test something invalid. + $dirs = mustache_template_finder::get_template_filename('core/octopus', 'clean'); + } +}