Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 311 and 402] [Versions 311 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          // Get the template source from the callback.
  92          $source = ($this->gettemplatesource)($component, $name, $themename);
  93  
  94          // Remove comments from template.
  95          if (!$includecomments) {
  96              $source = $this->strip_template_comments($source);
  97          }
  98  
  99          return $source;
 100      }
 101  
 102      /**
 103       * Load a template and some of the dependencies that will be needed in order to render
 104       * the template.
 105       *
 106       * The current implementation will return all of the templates and all of the strings in
 107       * each of those templates (excluding string substitutions).
 108       *
 109       * The return format is an array indexed with the dependency type (e.g. templates / strings) then
 110       * the component (e.g. core_message), and then the id (e.g. message_drawer).
 111       *
 112       * For example:
 113       * * We have 3 templates in core named foo, bar, and baz.
 114       * * foo includes bar and bar includes baz.
 115       * * foo uses the string 'home' from core
 116       * * baz uses the string 'help' from core
 117       *
 118       * If we load the template foo this function would return:
 119       * [
 120       *      'templates' => [
 121       *          'core' => [
 122       *              'foo' => '... template source ...',
 123       *              'bar' => '... template source ...',
 124       *              'baz' => '... template source ...',
 125       *          ]
 126       *      ],
 127       *      'strings' => [
 128       *          'core' => [
 129       *              'home' => 'Home',
 130       *              'help' => 'Help'
 131       *          ]
 132       *      ]
 133       * ]
 134       *
 135       * @param string $templatecomponent The moodle component (e.g. core_message)
 136       * @param string $templatename The template name (e.g. message_drawer)
 137       * @param string $themename The theme to load the template for (e.g. boost)
 138       * @param bool $includecomments If the comments should be stripped from the source before returning
 139       * @param array $seentemplates List of templates already processed / to be skipped.
 140       * @param array $seenstrings List of strings already processed / to be skipped.
 141       * @param string|null $lang moodle translation language, null means use current.
 142       * @return array
 143       */
 144      public function load_with_dependencies(
 145          string $templatecomponent,
 146          string $templatename,
 147          string $themename,
 148          bool $includecomments = false,
 149          array $seentemplates = [],
 150          array $seenstrings = [],
 151          string $lang = null
 152      ) : array {
 153          // Initialise the return values.
 154          $templates = [];
 155          $strings = [];
 156          $templatecomponent = trim($templatecomponent);
 157          $templatename = trim($templatename);
 158          // Get the requested template source.
 159          $templatesource = $this->load($templatecomponent, $templatename, $themename, $includecomments);
 160          // This is a helper function to save a value in one of the result arrays (either $templates or $strings).
 161          $save = function(array $results, array $seenlist, string $component, string $id, $value) use ($lang) {
 162              if (!isset($results[$component])) {
 163                  // If the results list doesn't already contain this component then initialise it.
 164                  $results[$component] = [];
 165              }
 166  
 167              // Save the value.
 168              $results[$component][$id] = $value;
 169              // Record that this item has been processed.
 170              array_push($seenlist, "$component/$id");
 171              // Return the updated results and seen list.
 172              return [$results, $seenlist];
 173          };
 174          // This is a helper function for processing a dependency. Does stuff like ignore duplicate processing,
 175          // common result formatting etc.
 176          $handler = function(array $dependency, array $ignorelist, callable $processcallback) use ($lang) {
 177              foreach ($dependency as $component => $ids) {
 178                  foreach ($ids as $id) {
 179                      $dependencyid = "$component/$id";
 180                      if (array_search($dependencyid, $ignorelist) === false) {
 181                          $processcallback($component, $id);
 182                          // Add this to our ignore list now that we've processed it so that we don't
 183                          // process it again.
 184                          array_push($ignorelist, $dependencyid);
 185                      }
 186                  }
 187              }
 188  
 189              return $ignorelist;
 190          };
 191  
 192          // Save this template as the first result in the $templates result array.
 193          list($templates, $seentemplates) = $save($templates, $seentemplates, $templatecomponent, $templatename, $templatesource);
 194  
 195          // Check the template for any dependencies that need to be loaded.
 196          $dependencies = $this->scan_template_source_for_dependencies($templatesource);
 197  
 198          // Load all of the lang strings that this template requires and add them to the
 199          // returned values.
 200          $seenstrings = $handler(
 201              $dependencies['strings'],
 202              $seenstrings,
 203              // Include $strings and $seenstrings by reference so that their values can be updated
 204              // outside of this anonymous function.
 205              function($component, $id) use ($save, &$strings, &$seenstrings, $lang) {
 206                  $string = get_string_manager()->get_string($id, $component, null, $lang);
 207                  // Save the string in the $strings results array.
 208                  list($strings, $seenstrings) = $save($strings, $seenstrings, $component, $id, $string);
 209              }
 210          );
 211  
 212          // Load any child templates that we've found in this template and add them to
 213          // the return list of dependencies.
 214          $seentemplates = $handler(
 215              $dependencies['templates'],
 216              $seentemplates,
 217              // Include $strings, $seenstrings, $templates, and $seentemplates by reference so that their values can be updated
 218              // outside of this anonymous function.
 219              function($component, $id) use (
 220                  $themename,
 221                  $includecomments,
 222                  &$seentemplates,
 223                  &$seenstrings,
 224                  &$templates,
 225                  &$strings,
 226                  $save,
 227                  $lang
 228              ) {
 229                  // We haven't seen this template yet so load it and it's dependencies.
 230                  $subdependencies = $this->load_with_dependencies(
 231                      $component,
 232                      $id,
 233                      $themename,
 234                      $includecomments,
 235                      $seentemplates,
 236                      $seenstrings,
 237                      $lang
 238                  );
 239  
 240                  foreach ($subdependencies['templates'] as $component => $ids) {
 241                      foreach ($ids as $id => $value) {
 242                          // Include the child themes in our results.
 243                          list($templates, $seentemplates) = $save($templates, $seentemplates, $component, $id, $value);
 244                      }
 245                  };
 246  
 247                  foreach ($subdependencies['strings'] as $component => $ids) {
 248                      foreach ($ids as $id => $value) {
 249                          // Include any strings that the child templates need in our results.
 250                          list($strings, $seenstrings) = $save($strings, $seenstrings, $component, $id, $value);
 251                      }
 252                  }
 253              }
 254          );
 255  
 256          return [
 257              'templates' => $templates,
 258              'strings' => $strings
 259          ];
 260      }
 261  
 262      /**
 263       * Scan over a template source string and return a list of dependencies it requires.
 264       * At the moment the list will only include other templates and strings.
 265       *
 266       * The return format is an array indexed with the dependency type (e.g. templates / strings) then
 267       * the component (e.g. core_message) with it's value being an array of the items required
 268       * in that component.
 269       *
 270       * For example:
 271       * If we have a template foo that includes 2 templates, bar and baz, and also 2 strings
 272       * 'home' and 'help' from the core component then the return value would look like:
 273       *
 274       * [
 275       *      'templates' => [
 276       *          'core' => ['foo', 'bar', 'baz']
 277       *      ],
 278       *      'strings' => [
 279       *          'core' => ['home', 'help']
 280       *      ]
 281       * ]
 282       *
 283       * @param string $source The template source
 284       * @return array
 285       */
 286      protected function scan_template_source_for_dependencies(string $source) : array {
 287          $tokenizer = new Mustache_Tokenizer();
 288          $tokens = $tokenizer->scan($source);
 289          $templates = [];
 290          $strings = [];
 291          $addtodependencies = function($dependencies, $component, $id) {
 292              $id = trim($id);
 293              $component = trim($component);
 294  
 295              if (!isset($dependencies[$component])) {
 296                  // Initialise the component if we haven't seen it before.
 297                  $dependencies[$component] = [];
 298              }
 299  
 300              // Add this id to the list of dependencies.
 301              array_push($dependencies[$component], $id);
 302  
 303              return $dependencies;
 304          };
 305  
 306          foreach ($tokens as $index => $token) {
 307              $type = $token['type'];
 308              $name = isset($token['name']) ? $token['name'] : null;
 309  
 310              if ($name) {
 311                  switch ($type) {
 312                      case Mustache_Tokenizer::T_PARTIAL:
 313                          list($component, $id) = explode('/', $name, 2);
 314                          $templates = $addtodependencies($templates, $component, $id);
 315                          break;
 316                      case Mustache_Tokenizer::T_PARENT:
 317                          list($component, $id) = explode('/', $name, 2);
 318                          $templates = $addtodependencies($templates, $component, $id);
 319                          break;
 320                      case Mustache_Tokenizer::T_SECTION:
 321                          if ($name == 'str') {
 322                              list($id, $component) = $this->get_string_identifiers($tokens, $index);
 323  
 324                              if ($id) {
 325                                  $strings = $addtodependencies($strings, $component, $id);
 326                              }
 327                          }
 328                          break;
 329                  }
 330              }
 331          }
 332  
 333          return [
 334              'templates' => $templates,
 335              'strings' => $strings
 336          ];
 337      }
 338  
 339      /**
 340       * Gets the identifier and component of the string.
 341       *
 342       * The string could be defined on one, or multiple lines.
 343       *
 344       * @param array $tokens The templates token.
 345       * @param int $start The index of the start of the string token.
 346       * @return array A list of the string identifier and component.
 347       */
 348      protected function get_string_identifiers(array $tokens, int $start): array {
 349          $current = $start + 1;
 350          $parts = [];
 351  
 352          // Get the contents of the string tag.
 353          while ($tokens[$current]['type'] !== Mustache_Tokenizer::T_END_SECTION) {
 354              if (!isset($tokens[$current]['value']) || empty(trim($tokens[$current]['value']))) {
 355                  // An empty line, so we should ignore it.
 356                  $current++;
 357                  continue;
 358              }
 359  
 360              // We need to remove any spaces before and after the string.
 361              $nospaces = trim($tokens[$current]['value']);
 362  
 363              // We need to remove any trailing commas so that the explode will not add an
 364              // empty entry where two paramters are on multiple lines.
 365              $clean = rtrim($nospaces, ',');
 366  
 367              // We separate the parts of a string with commas.
 368              $subparts = explode(',', $clean);
 369  
 370              // Store the parts.
 371              $parts = array_merge($parts, $subparts);
 372  
 373              $current++;
 374          }
 375  
 376          // The first text should be the first part of a str tag.
 377          $id = isset($parts[0]) ? trim($parts[0]) : null;
 378  
 379          // Default to 'core' for the component, if not specified.
 380          $component = isset($parts[1]) ? trim($parts[1]) : 'core';
 381  
 382          return [$id, $component];
 383      }
 384  }