Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Load template source strings.
  19   *
  20   * @package    core
  21   * @category   output
  22   * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  namespace core\output;
  27  
  28  defined('MOODLE_INTERNAL') || die();
  29  
  30  use \Mustache_Tokenizer;
  31  
  32  /**
  33   * Load template source strings.
  34   *
  35   * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
  36   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   */
  38  class mustache_template_source_loader {
  39  
  40      /** @var $gettemplatesource Callback function to load the template source from full name */
  41      private $gettemplatesource = null;
  42  
  43      /**
  44       * Constructor that takes a callback to allow the calling code to specify how to retrieve
  45       * the source for a template name.
  46       *
  47       * If no callback is provided then default to the load from disk implementation.
  48       *
  49       * @param callable|null $gettemplatesource Callback to load template source by template name
  50       */
  51      public function __construct(callable $gettemplatesource = null) {
  52          if ($gettemplatesource) {
  53              // The calling code has specified a function for retrieving the template source
  54              // code by name and theme.
  55              $this->gettemplatesource = $gettemplatesource;
  56          } else {
  57              // By default we will pull the template from disk.
  58              $this->gettemplatesource = function($component, $name, $themename) {
  59                  $fulltemplatename = $component . '/' . $name;
  60                  $filename = mustache_template_finder::get_template_filepath($fulltemplatename, $themename);
  61                  return file_get_contents($filename);
  62              };
  63          }
  64      }
  65  
  66      /**
  67       * Remove comments from mustache template.
  68       *
  69       * @param string $templatestr
  70       * @return string
  71       */
  72      protected function strip_template_comments($templatestr) : string {
  73          return preg_replace('/(?={{!)(.*)(}})/sU', '', $templatestr);
  74      }
  75  
  76      /**
  77       * Load the template source from the component and template name.
  78       *
  79       * @param string $component The moodle component (e.g. core_message)
  80       * @param string $name The template name (e.g. message_drawer)
  81       * @param string $themename The theme to load the template for (e.g. boost)
  82       * @param bool $includecomments If the comments should be stripped from the source before returning
  83       * @return string The template source
  84       */
  85      public function load(
  86          string $component,
  87          string $name,
  88          string $themename,
  89          bool $includecomments = false
  90      ) : string {
  91          global $CFG;
  92          // Get the template source from the callback.
  93          $source = ($this->gettemplatesource)($component, $name, $themename);
  94  
  95          // Remove comments from template.
  96          if (!$includecomments) {
  97              $source = $this->strip_template_comments($source);
  98          }
  99          if (!empty($CFG->debugtemplateinfo)) {
 100              return "<!-- template(JS): $name -->" . $source . "<!-- /template(JS): $name -->";
 101          }
 102          return $source;
 103      }
 104  
 105      /**
 106       * Load a template and some of the dependencies that will be needed in order to render
 107       * the template.
 108       *
 109       * The current implementation will return all of the templates and all of the strings in
 110       * each of those templates (excluding string substitutions).
 111       *
 112       * The return format is an array indexed with the dependency type (e.g. templates / strings) then
 113       * the component (e.g. core_message), and then the id (e.g. message_drawer).
 114       *
 115       * For example:
 116       * * We have 3 templates in core named foo, bar, and baz.
 117       * * foo includes bar and bar includes baz.
 118       * * foo uses the string 'home' from core
 119       * * baz uses the string 'help' from core
 120       *
 121       * If we load the template foo this function would return:
 122       * [
 123       *      'templates' => [
 124       *          'core' => [
 125       *              'foo' => '... template source ...',
 126       *              'bar' => '... template source ...',
 127       *              'baz' => '... template source ...',
 128       *          ]
 129       *      ],
 130       *      'strings' => [
 131       *          'core' => [
 132       *              'home' => 'Home',
 133       *              'help' => 'Help'
 134       *          ]
 135       *      ]
 136       * ]
 137       *
 138       * @param string $templatecomponent The moodle component (e.g. core_message)
 139       * @param string $templatename The template name (e.g. message_drawer)
 140       * @param string $themename The theme to load the template for (e.g. boost)
 141       * @param bool $includecomments If the comments should be stripped from the source before returning
 142       * @param array $seentemplates List of templates already processed / to be skipped.
 143       * @param array $seenstrings List of strings already processed / to be skipped.
 144       * @param string|null $lang moodle translation language, null means use current.
 145       * @return array
 146       */
 147      public function load_with_dependencies(
 148          string $templatecomponent,
 149          string $templatename,
 150          string $themename,
 151          bool $includecomments = false,
 152          array $seentemplates = [],
 153          array $seenstrings = [],
 154          string $lang = null
 155      ) : array {
 156          // Initialise the return values.
 157          $templates = [];
 158          $strings = [];
 159          $templatecomponent = trim($templatecomponent);
 160          $templatename = trim($templatename);
 161          // Get the requested template source.
 162          $templatesource = $this->load($templatecomponent, $templatename, $themename, $includecomments);
 163          // This is a helper function to save a value in one of the result arrays (either $templates or $strings).
 164          $save = function(array $results, array $seenlist, string $component, string $id, $value) use ($lang) {
 165              if (!isset($results[$component])) {
 166                  // If the results list doesn't already contain this component then initialise it.
 167                  $results[$component] = [];
 168              }
 169  
 170              // Save the value.
 171              $results[$component][$id] = $value;
 172              // Record that this item has been processed.
 173              array_push($seenlist, "$component/$id");
 174              // Return the updated results and seen list.
 175              return [$results, $seenlist];
 176          };
 177          // This is a helper function for processing a dependency. Does stuff like ignore duplicate processing,
 178          // common result formatting etc.
 179          $handler = function(array $dependency, array $ignorelist, callable $processcallback) use ($lang) {
 180              foreach ($dependency as $component => $ids) {
 181                  foreach ($ids as $id) {
 182                      $dependencyid = "$component/$id";
 183                      if (array_search($dependencyid, $ignorelist) === false) {
 184                          $processcallback($component, $id);
 185                          // Add this to our ignore list now that we've processed it so that we don't
 186                          // process it again.
 187                          array_push($ignorelist, $dependencyid);
 188                      }
 189                  }
 190              }
 191  
 192              return $ignorelist;
 193          };
 194  
 195          // Save this template as the first result in the $templates result array.
 196          list($templates, $seentemplates) = $save($templates, $seentemplates, $templatecomponent, $templatename, $templatesource);
 197  
 198          // Check the template for any dependencies that need to be loaded.
 199          $dependencies = $this->scan_template_source_for_dependencies($templatesource);
 200  
 201          // Load all of the lang strings that this template requires and add them to the
 202          // returned values.
 203          $seenstrings = $handler(
 204              $dependencies['strings'],
 205              $seenstrings,
 206              // Include $strings and $seenstrings by reference so that their values can be updated
 207              // outside of this anonymous function.
 208              function($component, $id) use ($save, &$strings, &$seenstrings, $lang) {
 209                  $string = get_string_manager()->get_string($id, $component, null, $lang);
 210                  // Save the string in the $strings results array.
 211                  list($strings, $seenstrings) = $save($strings, $seenstrings, $component, $id, $string);
 212              }
 213          );
 214  
 215          // Load any child templates that we've found in this template and add them to
 216          // the return list of dependencies.
 217          $seentemplates = $handler(
 218              $dependencies['templates'],
 219              $seentemplates,
 220              // Include $strings, $seenstrings, $templates, and $seentemplates by reference so that their values can be updated
 221              // outside of this anonymous function.
 222              function($component, $id) use (
 223                  $themename,
 224                  $includecomments,
 225                  &$seentemplates,
 226                  &$seenstrings,
 227                  &$templates,
 228                  &$strings,
 229                  $save,
 230                  $lang
 231              ) {
 232                  // We haven't seen this template yet so load it and it's dependencies.
 233                  $subdependencies = $this->load_with_dependencies(
 234                      $component,
 235                      $id,
 236                      $themename,
 237                      $includecomments,
 238                      $seentemplates,
 239                      $seenstrings,
 240                      $lang
 241                  );
 242  
 243                  foreach ($subdependencies['templates'] as $component => $ids) {
 244                      foreach ($ids as $id => $value) {
 245                          // Include the child themes in our results.
 246                          list($templates, $seentemplates) = $save($templates, $seentemplates, $component, $id, $value);
 247                      }
 248                  };
 249  
 250                  foreach ($subdependencies['strings'] as $component => $ids) {
 251                      foreach ($ids as $id => $value) {
 252                          // Include any strings that the child templates need in our results.
 253                          list($strings, $seenstrings) = $save($strings, $seenstrings, $component, $id, $value);
 254                      }
 255                  }
 256              }
 257          );
 258  
 259          return [
 260              'templates' => $templates,
 261              'strings' => $strings
 262          ];
 263      }
 264  
 265      /**
 266       * Scan over a template source string and return a list of dependencies it requires.
 267       * At the moment the list will only include other templates and strings.
 268       *
 269       * The return format is an array indexed with the dependency type (e.g. templates / strings) then
 270       * the component (e.g. core_message) with it's value being an array of the items required
 271       * in that component.
 272       *
 273       * For example:
 274       * If we have a template foo that includes 2 templates, bar and baz, and also 2 strings
 275       * 'home' and 'help' from the core component then the return value would look like:
 276       *
 277       * [
 278       *      'templates' => [
 279       *          'core' => ['foo', 'bar', 'baz']
 280       *      ],
 281       *      'strings' => [
 282       *          'core' => ['home', 'help']
 283       *      ]
 284       * ]
 285       *
 286       * @param string $source The template source
 287       * @return array
 288       */
 289      protected function scan_template_source_for_dependencies(string $source) : array {
 290          $tokenizer = new Mustache_Tokenizer();
 291          $tokens = $tokenizer->scan($source);
 292          $templates = [];
 293          $strings = [];
 294          $addtodependencies = function($dependencies, $component, $id) {
 295              $id = trim($id);
 296              $component = trim($component);
 297  
 298              if (!isset($dependencies[$component])) {
 299                  // Initialise the component if we haven't seen it before.
 300                  $dependencies[$component] = [];
 301              }
 302  
 303              // Add this id to the list of dependencies.
 304              array_push($dependencies[$component], $id);
 305  
 306              return $dependencies;
 307          };
 308  
 309          foreach ($tokens as $index => $token) {
 310              $type = $token['type'];
 311              $name = isset($token['name']) ? $token['name'] : null;
 312  
 313              if ($name) {
 314                  switch ($type) {
 315                      case Mustache_Tokenizer::T_PARTIAL:
 316                          list($component, $id) = explode('/', $name, 2);
 317                          $templates = $addtodependencies($templates, $component, $id);
 318                          break;
 319                      case Mustache_Tokenizer::T_PARENT:
 320                          list($component, $id) = explode('/', $name, 2);
 321                          $templates = $addtodependencies($templates, $component, $id);
 322                          break;
 323                      case Mustache_Tokenizer::T_SECTION:
 324                          if ($name == 'str') {
 325                              list($id, $component) = $this->get_string_identifiers($tokens, $index);
 326  
 327                              if ($id) {
 328                                  $strings = $addtodependencies($strings, $component, $id);
 329                              }
 330                          }
 331                          break;
 332                  }
 333              }
 334          }
 335  
 336          return [
 337              'templates' => $templates,
 338              'strings' => $strings
 339          ];
 340      }
 341  
 342      /**
 343       * Gets the identifier and component of the string.
 344       *
 345       * The string could be defined on one, or multiple lines.
 346       *
 347       * @param array $tokens The templates token.
 348       * @param int $start The index of the start of the string token.
 349       * @return array A list of the string identifier and component.
 350       */
 351      protected function get_string_identifiers(array $tokens, int $start): array {
 352          $current = $start + 1;
 353          $parts = [];
 354  
 355          // Get the contents of the string tag.
 356          while ($tokens[$current]['type'] !== Mustache_Tokenizer::T_END_SECTION) {
 357              if (!isset($tokens[$current]['value']) || empty(trim($tokens[$current]['value']))) {
 358                  // An empty line, so we should ignore it.
 359                  $current++;
 360                  continue;
 361              }
 362  
 363              // We need to remove any spaces before and after the string.
 364              $nospaces = trim($tokens[$current]['value']);
 365  
 366              // We need to remove any trailing commas so that the explode will not add an
 367              // empty entry where two paramters are on multiple lines.
 368              $clean = rtrim($nospaces, ',');
 369  
 370              // We separate the parts of a string with commas.
 371              $subparts = explode(',', $clean);
 372  
 373              // Store the parts.
 374              $parts = array_merge($parts, $subparts);
 375  
 376              $current++;
 377          }
 378  
 379          // The first text should be the first part of a str tag.
 380          $id = isset($parts[0]) ? trim($parts[0]) : null;
 381  
 382          // Default to 'core' for the component, if not specified.
 383          $component = isset($parts[1]) ? trim($parts[1]) : 'core';
 384  
 385          return [$id, $component];
 386      }
 387  }