From 0261d1effafde4aaa2a0b137be2f8edd139e01aa Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Wed, 24 Jul 2019 09:31:17 +0800 Subject: [PATCH] MDL-50346 core: Allow sub-directories in template names --- .../templatelibrary/amd/build/display.min.js | 2 +- .../amd/build/display.min.js.map | 2 +- admin/tool/templatelibrary/amd/src/display.js | 2 +- admin/tool/templatelibrary/classes/api.php | 30 ++- .../tool/templatelibrary/classes/external.php | 2 +- admin/tool/templatelibrary/version.php | 2 +- lib/amd/build/templates.min.js | 2 +- lib/amd/build/templates.min.js.map | 2 +- lib/amd/src/templates.js | 2 +- lib/classes/output/external.php | 4 +- .../output/mustache_template_finder.php | 4 +- lib/tests/mustache_template_finder_test.php | 192 ++++++++++++++---- lib/upgrade.txt | 10 + theme/upgrade.txt | 15 ++ version.php | 2 +- 15 files changed, 209 insertions(+), 64 deletions(-) diff --git a/admin/tool/templatelibrary/amd/build/display.min.js b/admin/tool/templatelibrary/amd/build/display.min.js index 76ec8130610..9ac8f6ba001 100644 --- a/admin/tool/templatelibrary/amd/build/display.min.js +++ b/admin/tool/templatelibrary/amd/build/display.min.js @@ -1,2 +1,2 @@ -define ("tool_templatelibrary/display",["jquery","core/ajax","core/log","core/notification","core/templates","core/config","core/str"],function(a,b,c,d,e,f,g){var h=function(a,b){if(!a){return!1}var c="@template "+b,d=0,e=[];e=a.match(/{{!([\s\S]*?)}}/g);if(null!==e){for(d=0;d.\n\n/**\n * This module adds ajax display functions to the template library page.\n *\n * @module tool_templatelibrary/display\n * @package tool_templatelibrary\n * @copyright 2015 Damyon Wiese \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/ajax', 'core/log', 'core/notification', 'core/templates', 'core/config', 'core/str'],\n function($, ajax, log, notification, templates, config, str) {\n\n /**\n * Search through a template for a template docs comment.\n *\n * @param {String} templateSource The raw template\n * @param {String} templateName The name of the template used to search for docs tag\n * @return {String|boolean} the correct comment or false\n */\n var findDocsSection = function(templateSource, templateName) {\n\n if (!templateSource) {\n return false;\n }\n // Find the comment section marked with @template component/template.\n var marker = \"@template \" + templateName,\n i = 0,\n sections = [];\n\n sections = templateSource.match(/{{!([\\s\\S]*?)}}/g);\n\n // If no sections match - show the entire file.\n if (sections !== null) {\n for (i = 0; i < sections.length; i++) {\n var section = sections[i];\n var start = section.indexOf(marker);\n if (start !== -1) {\n // Remove {{! and }} from start and end.\n var offset = start + marker.length + 1;\n section = section.substr(offset, section.length - 2 - offset);\n return section;\n }\n }\n }\n // No matching comment.\n return false;\n };\n\n /**\n * Handle a template loaded response.\n *\n * @param {String} templateName The template name\n * @param {String} source The template source\n * @param {String} originalSource The original template source (not theme overridden)\n */\n var templateLoaded = function(templateName, source, originalSource) {\n str.get_string('templateselected', 'tool_templatelibrary', templateName).done(function(s) {\n $('[data-region=\"displaytemplateheader\"]').text(s);\n }).fail(notification.exception);\n\n // Find the comment section marked with @template component/template.\n var docs = findDocsSection(source, templateName);\n\n if (docs === false) {\n // Docs was not in theme template, try original.\n docs = findDocsSection(originalSource, templateName);\n }\n\n // If we found a docs section, limit the template library to showing this section.\n if (docs) {\n source = docs;\n }\n\n $('[data-region=\"displaytemplatesource\"]').text(source);\n\n // Now search the text for a json example.\n\n var example = source.match(/Example context \\(json\\):([\\s\\S]*)/);\n var context = false;\n if (example) {\n var rawJSON = example[1].trim();\n try {\n context = $.parseJSON(rawJSON);\n } catch (e) {\n log.debug('Could not parse json example context for template.');\n log.debug(e);\n }\n }\n if (context) {\n templates.render(templateName, context).done(function(html, js) {\n templates.replaceNodeContents($('[data-region=\"displaytemplateexample\"]'), html, js);\n }).fail(notification.exception);\n } else {\n str.get_string('templatehasnoexample', 'tool_templatelibrary').done(function(s) {\n $('[data-region=\"displaytemplateexample\"]').text(s);\n }).fail(notification.exception);\n }\n };\n\n /**\n * Load the a template source from Moodle.\n *\n * @param {String} templateName\n */\n var loadTemplate = function(templateName) {\n var parts = templateName.split('/');\n var component = parts.shift();\n var name = parts.shift();\n\n var promises = ajax.call([{\n methodname: 'core_output_load_template',\n args: {\n component: component,\n template: name,\n themename: config.theme,\n includecomments: true\n }\n }, {\n methodname: 'tool_templatelibrary_load_canonical_template',\n args: {\n component: component,\n template: name\n }\n }], true, false);\n\n // When returns a new promise that is resolved when all the passed in promises are resolved.\n // The arguments to the done become the values of each resolved promise.\n $.when.apply($, promises)\n .done(function(source, originalSource) {\n templateLoaded(templateName, source, originalSource);\n })\n .fail(notification.exception);\n };\n\n // Add the event listeners.\n $('[data-region=\"list-templates\"]').on('click', '[data-templatename]', function(e) {\n var templatename = $(this).data('templatename');\n e.preventDefault();\n loadTemplate(templatename);\n });\n\n // This module does not expose anything.\n return {};\n});\n"],"file":"display.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/display.js"],"names":["define","$","ajax","log","notification","templates","config","str","findDocsSection","templateSource","templateName","marker","i","sections","match","length","section","start","indexOf","offset","substr","templateLoaded","source","originalSource","get_string","done","s","text","fail","exception","docs","example","context","rawJSON","trim","parseJSON","e","debug","render","html","js","replaceNodeContents","loadTemplate","parts","split","component","shift","name","join","promises","call","methodname","args","template","themename","theme","includecomments","when","apply","on","templatename","data","preventDefault"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * This module adds ajax display functions to the template library page.\n *\n * @module tool_templatelibrary/display\n * @package tool_templatelibrary\n * @copyright 2015 Damyon Wiese \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/ajax', 'core/log', 'core/notification', 'core/templates', 'core/config', 'core/str'],\n function($, ajax, log, notification, templates, config, str) {\n\n /**\n * Search through a template for a template docs comment.\n *\n * @param {String} templateSource The raw template\n * @param {String} templateName The name of the template used to search for docs tag\n * @return {String|boolean} the correct comment or false\n */\n var findDocsSection = function(templateSource, templateName) {\n\n if (!templateSource) {\n return false;\n }\n // Find the comment section marked with @template component/template.\n var marker = \"@template \" + templateName,\n i = 0,\n sections = [];\n\n sections = templateSource.match(/{{!([\\s\\S]*?)}}/g);\n\n // If no sections match - show the entire file.\n if (sections !== null) {\n for (i = 0; i < sections.length; i++) {\n var section = sections[i];\n var start = section.indexOf(marker);\n if (start !== -1) {\n // Remove {{! and }} from start and end.\n var offset = start + marker.length + 1;\n section = section.substr(offset, section.length - 2 - offset);\n return section;\n }\n }\n }\n // No matching comment.\n return false;\n };\n\n /**\n * Handle a template loaded response.\n *\n * @param {String} templateName The template name\n * @param {String} source The template source\n * @param {String} originalSource The original template source (not theme overridden)\n */\n var templateLoaded = function(templateName, source, originalSource) {\n str.get_string('templateselected', 'tool_templatelibrary', templateName).done(function(s) {\n $('[data-region=\"displaytemplateheader\"]').text(s);\n }).fail(notification.exception);\n\n // Find the comment section marked with @template component/template.\n var docs = findDocsSection(source, templateName);\n\n if (docs === false) {\n // Docs was not in theme template, try original.\n docs = findDocsSection(originalSource, templateName);\n }\n\n // If we found a docs section, limit the template library to showing this section.\n if (docs) {\n source = docs;\n }\n\n $('[data-region=\"displaytemplatesource\"]').text(source);\n\n // Now search the text for a json example.\n\n var example = source.match(/Example context \\(json\\):([\\s\\S]*)/);\n var context = false;\n if (example) {\n var rawJSON = example[1].trim();\n try {\n context = $.parseJSON(rawJSON);\n } catch (e) {\n log.debug('Could not parse json example context for template.');\n log.debug(e);\n }\n }\n if (context) {\n templates.render(templateName, context).done(function(html, js) {\n templates.replaceNodeContents($('[data-region=\"displaytemplateexample\"]'), html, js);\n }).fail(notification.exception);\n } else {\n str.get_string('templatehasnoexample', 'tool_templatelibrary').done(function(s) {\n $('[data-region=\"displaytemplateexample\"]').text(s);\n }).fail(notification.exception);\n }\n };\n\n /**\n * Load the a template source from Moodle.\n *\n * @param {String} templateName\n */\n var loadTemplate = function(templateName) {\n var parts = templateName.split('/');\n var component = parts.shift();\n var name = parts.join('/');\n\n var promises = ajax.call([{\n methodname: 'core_output_load_template',\n args: {\n component: component,\n template: name,\n themename: config.theme,\n includecomments: true\n }\n }, {\n methodname: 'tool_templatelibrary_load_canonical_template',\n args: {\n component: component,\n template: name\n }\n }], true, false);\n\n // When returns a new promise that is resolved when all the passed in promises are resolved.\n // The arguments to the done become the values of each resolved promise.\n $.when.apply($, promises)\n .done(function(source, originalSource) {\n templateLoaded(templateName, source, originalSource);\n })\n .fail(notification.exception);\n };\n\n // Add the event listeners.\n $('[data-region=\"list-templates\"]').on('click', '[data-templatename]', function(e) {\n var templatename = $(this).data('templatename');\n e.preventDefault();\n loadTemplate(templatename);\n });\n\n // This module does not expose anything.\n return {};\n});\n"],"file":"display.min.js"} \ No newline at end of file diff --git a/admin/tool/templatelibrary/amd/src/display.js b/admin/tool/templatelibrary/amd/src/display.js index 39793660d40..222e4bdd44b 100644 --- a/admin/tool/templatelibrary/amd/src/display.js +++ b/admin/tool/templatelibrary/amd/src/display.js @@ -119,7 +119,7 @@ define(['jquery', 'core/ajax', 'core/log', 'core/notification', 'core/templates' var loadTemplate = function(templateName) { var parts = templateName.split('/'); var component = parts.shift(); - var name = parts.shift(); + var name = parts.join('/'); var promises = ajax.call([{ methodname: 'core_output_load_template', diff --git a/admin/tool/templatelibrary/classes/api.php b/admin/tool/templatelibrary/classes/api.php index 7ae2b0e8408..f0d7a43ce4f 100644 --- a/admin/tool/templatelibrary/classes/api.php +++ b/admin/tool/templatelibrary/classes/api.php @@ -99,13 +99,34 @@ class api { foreach ($templatedirs as $templatecomponent => $dirs) { foreach ($dirs as $dir) { + if (!is_dir($dir) || !is_readable($dir)) { + continue; + } + $dir = realpath($dir); + // List it. - $files = glob($dir . '/*.mustache'); + $directory = new \RecursiveDirectoryIterator($dir); + $files = new \RecursiveIteratorIterator($directory); foreach ($files as $file) { - $templatename = basename($file, '.mustache'); - if ($search == '' || strpos($templatename, $search) !== false) { - $results[$templatecomponent . '/' . $templatename] = 1; + if (!$file->isFile()) { + continue; + } + $filename = substr($file->getRealpath(), strlen($dir) + 1); + if (strpos($templatecomponent, 'theme_') === 0) { + if (strpos($filename, '/') !== false && strpos($filename, 'local/') !== 0) { + // Skip any template in a sub-directory of a theme which is not in a local directory. + // These are theme overrides of core templates. + // Note: There is a rare edge case where a theme may override a template and then have additional + // dependant templates and these will not be shown. + continue; + } + } + $templatename = str_replace('.mustache', '', $filename); + $componenttemplatename = "{$templatecomponent}/{$templatename}"; + + if ($search == '' || strpos($componenttemplatename, $search) !== false) { + $results[$componenttemplatename] = 1; } } } @@ -152,5 +173,4 @@ class api { return $templatestr; } - } diff --git a/admin/tool/templatelibrary/classes/external.php b/admin/tool/templatelibrary/classes/external.php index f1c72f55937..edda9f2dc4a 100644 --- a/admin/tool/templatelibrary/classes/external.php +++ b/admin/tool/templatelibrary/classes/external.php @@ -105,7 +105,7 @@ class external extends external_api { public static function load_canonical_template_parameters() { return new external_function_parameters( array('component' => new external_value(PARAM_COMPONENT, 'component containing the template'), - 'template' => new external_value(PARAM_ALPHANUMEXT, 'name of the template')) + 'template' => new external_value(PARAM_SAFEPATH, 'name of the template')) ); } diff --git a/admin/tool/templatelibrary/version.php b/admin/tool/templatelibrary/version.php index c8d15190c23..625a195b03c 100644 --- a/admin/tool/templatelibrary/version.php +++ b/admin/tool/templatelibrary/version.php @@ -21,6 +21,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2019052000; // The current plugin version (Date: YYYYMMDDXX). +$plugin->version = 2019052002; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2019051100; // Requires this Moodle version. $plugin->component = 'tool_templatelibrary'; // Full name of the plugin (used for diagnostics). diff --git a/lib/amd/build/templates.min.js b/lib/amd/build/templates.min.js index 1affa27f687..a2dcac93bb1 100644 --- a/lib/amd/build/templates.min.js +++ b/lib/amd/build/templates.min.js @@ -1,2 +1,2 @@ -define ("core/templates",["core/mustache","jquery","core/ajax","core/str","core/notification","core/url","core/config","core/localstorage","core/icon_system","core/event","core/yui","core/log","core/truncate","core/user_date","core/pending"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o){var p=0,q={},r={},s={},t={},u=[],v=!1,w=function(a){if(a in r){return r[a]}if(a in q){r[a]=b.Deferred().resolve(q[a]).promise();return r[a]}var c=h.get("core_template/"+a);if(c){q[a]=c;r[a]=b.Deferred().resolve(c).promise();return r[a]}return null},x=function(){if(!u.length){return}if(v){return}v=!0;var a=u.slice(),e=b.Deferred(),f=[],g=a.map(function(a){var c=a.component,i=a.name,j=a.searchKey,k=a.theme,l=a.deferred,m=null,n=w(j);if(n){m=n}else{f.push({methodname:"core_output_load_template_with_dependencies",args:{component:c,template:i,themename:k,lang:b("html").attr("lang").replace(/-/g,"_")}});var o=f.length-1;m=e.promise().then(function(a){g[j]=a[o].then(function(a){var b=null;a.templates.forEach(function(a){var d=[k,a.component,a.name].join("/");q[d]=a.value;h.set("core_template/"+d,a.value);if(a.component==c&&a.name==i){b=a.value}});if(a.strings.length){d.cache_strings(a.strings.map(function(a){return{component:a.component,key:a.name,value:a.value}}))}return b});return g[j]})}return m.then(function(a){return l.resolve(a)}).catch(function(a){l.reject(a);throw a})});if(f.length){e.resolve(c.call(f,!0,!1,!1,0,M.cfg.themerev))}else{e.resolve()}b.when.apply(null,g).then(function(){u.splice(0,a.length);v=!1;x()}).catch(function(){u.splice(0,a.length);v=!1;x()})},y=function(){this.requiredStrings=[];this.requiredJS=[];this.requiredDates=[];this.currentThemeName=""};y.prototype.requiredStrings=null;y.prototype.requiredDates=[];y.prototype.requiredJS=null;y.prototype.currentThemeName="";y.prototype.getTemplate=function(a){var c=this.currentThemeName,d=c+"/"+a,e=w(d);if(e){return e}var f=u.filter(function(a){return a.searchKey==d});if(f.length){return f[0].deferred.promise()}var g=a.split("/"),h=g.shift(),i=g.shift(),j=b.Deferred();u.push({component:h,name:i,theme:c,searchKey:d,deferred:j});x();return j.promise()};y.prototype.partialHelper=function(a){var b=this.currentThemeName+"/"+a;if(!(b in q)){e.exception(new Error("Failed to pre-fetch the template: "+a))}return q[b]};y.prototype.renderIcon=function(a,c,d){var f=g.iconsystemmodule,h=b.Deferred();require([f],function(a){var b=new a;if(!(b instanceof i)){h.reject("Invalid icon system specified"+g.iconsystemmodule)}else{t=b;b.init().then(h.resolve).catch(e.exception)}});return h.then(function(a){return this.getTemplate(a.getTemplateName())}.bind(this)).then(function(b){return t.renderIcon(a,c,d,b)})};y.prototype.pixHelper=function(a,b,c){var d=b.split(","),e="",f="",g="";if(0=}}$1<%={{ }}=%>").replace(/(\r\n|\r|\n)/g," ");return"\""+d+"\""};y.prototype.shortenTextHelper=function(a,b,c){var d=/(.*?),(.*)/,e=b.match(d),f=e[1].trim(),g=e[2].trim(),h=c(g,a);return m.truncate(h,{length:f,words:!0,ellipsis:"..."})};y.prototype.userDateHelper=function(a,b,c){var d=/(.*?),(.*)/,e=b.match(d),f=c(e[1].trim(),a),g=c(e[2].trim(),a),h=this.requiredDates.length;this.requiredDates.push({timestamp:f,format:g});return"[[_t_"+h+"]]"};y.prototype.addHelpers=function(a,b){this.currentThemeName=b;this.requiredStrings=[];this.requiredJS=[];a.uniqid=p++;a.str=function(){return this.stringHelper.bind(this,a)}.bind(this);a.pix=function(){return this.pixHelper.bind(this,a)}.bind(this);a.js=function(){return this.jsHelper.bind(this,a)}.bind(this);a.quote=function(){return this.quoteHelper.bind(this,a)}.bind(this);a.shortentext=function(){return this.shortenTextHelper.bind(this,a)}.bind(this);a.userdate=function(){return this.userDateHelper.bind(this,a)}.bind(this);a.globals={config:g};a.currentTheme=b};y.prototype.getJS=function(){var a="";if(0").attr("type","text/javascript").html(a);b("head").append(c)}},A=function(a,c,d,e){var f=b(a);if(f.length){var g=b(c),h=null;if(e){h=new k.NodeList(f.children().get());h.destroy(!0);f.empty();f.append(g)}else{h=new k.NodeList(f.get());h.destroy(!0);f.replaceWith(g)}z(d);j.notifyFilterContentUpdated(g)}};y.prototype.scanForPartials=function(b){var c=a.parse(b),d=[],e=function(a,b){var c,d;for(c=0;c"==d[0]||"<"==d[0]){b.push(d[1])}if(4=}}$1<%={{ }}=%>").replace(/(\r\n|\r|\n)/g," ");return"\""+d+"\""};y.prototype.shortenTextHelper=function(a,b,c){var d=/(.*?),(.*)/,e=b.match(d),f=e[1].trim(),g=e[2].trim(),h=c(g,a);return m.truncate(h,{length:f,words:!0,ellipsis:"..."})};y.prototype.userDateHelper=function(a,b,c){var d=/(.*?),(.*)/,e=b.match(d),f=c(e[1].trim(),a),g=c(e[2].trim(),a),h=this.requiredDates.length;this.requiredDates.push({timestamp:f,format:g});return"[[_t_"+h+"]]"};y.prototype.addHelpers=function(a,b){this.currentThemeName=b;this.requiredStrings=[];this.requiredJS=[];a.uniqid=p++;a.str=function(){return this.stringHelper.bind(this,a)}.bind(this);a.pix=function(){return this.pixHelper.bind(this,a)}.bind(this);a.js=function(){return this.jsHelper.bind(this,a)}.bind(this);a.quote=function(){return this.quoteHelper.bind(this,a)}.bind(this);a.shortentext=function(){return this.shortenTextHelper.bind(this,a)}.bind(this);a.userdate=function(){return this.userDateHelper.bind(this,a)}.bind(this);a.globals={config:g};a.currentTheme=b};y.prototype.getJS=function(){var a="";if(0").attr("type","text/javascript").html(a);b("head").append(c)}},A=function(a,c,d,e){var f=b(a);if(f.length){var g=b(c),h=null;if(e){h=new k.NodeList(f.children().get());h.destroy(!0);f.empty();f.append(g)}else{h=new k.NodeList(f.get());h.destroy(!0);f.replaceWith(g)}z(d);j.notifyFilterContentUpdated(g)}};y.prototype.scanForPartials=function(b){var c=a.parse(b),d=[],e=function(a,b){var c,d;for(c=0;c"==d[0]||"<"==d[0]){b.push(d[1])}if(4.\n\n/**\n * Template renderer for Moodle. Load and render Moodle templates with Mustache.\n *\n * @module core/templates\n * @package core\n * @class templates\n * @copyright 2015 Damyon Wiese \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 2.9\n */\ndefine([\n 'core/mustache',\n 'jquery',\n 'core/ajax',\n 'core/str',\n 'core/notification',\n 'core/url',\n 'core/config',\n 'core/localstorage',\n 'core/icon_system',\n 'core/event',\n 'core/yui',\n 'core/log',\n 'core/truncate',\n 'core/user_date',\n 'core/pending',\n ],\n function(mustache, $, ajax, str, notification, coreurl, config, storage, IconSystem, event, Y, Log, Truncate, UserDate,\n Pending) {\n\n // Module variables.\n /** @var {Number} uniqInstances Count of times this constructor has been called. */\n var uniqInstances = 0;\n\n /** @var {String[]} templateCache - Cache of already loaded template strings */\n var templateCache = {};\n\n /** @var {Promise[]} templatePromises - Cache of already loaded template promises */\n var templatePromises = {};\n\n /** @var {Promise[]} cachePartialPromises - Cache of already loaded template partial promises */\n var cachePartialPromises = {};\n\n /** @var {Object} iconSystem - Object extending core/iconsystem */\n var iconSystem = {};\n\n /** @var {Object[]} loadTemplateBuffer - List of templates to be loaded */\n var loadTemplateBuffer = [];\n\n /** @var {Bool} isLoadingTemplates - Whether templates are currently being loaded */\n var isLoadingTemplates = false;\n\n /**\n * Search the various caches for a template promise for the given search key.\n * The search key should be in the format //