Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

   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   * Custom Moodle helper collection for mustache.
  19   *
  20   * @copyright  2019 Ryan Wyllie <ryan@moodle.com>
  21   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  22   */
  23  
  24  namespace core\output;
  25  
  26  /**
  27   * Custom Moodle helper collection for mustache.
  28   *
  29   * @copyright  2019 Ryan Wyllie <ryan@moodle.com>
  30   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  31   */
  32  class mustache_helper_collection extends \Mustache_HelperCollection {
  33  
  34      /**
  35       * @var string[] Names of helpers that aren't allowed to be called within other helpers.
  36       */
  37      private $disallowednestedhelpers = [];
  38  
  39      /**
  40       * Helper Collection constructor.
  41       *
  42       * Optionally accepts an array (or Traversable) of `$name => $helper` pairs.
  43       *
  44       * @throws \Mustache_Exception_InvalidArgumentException if the $helpers argument isn't an array or Traversable
  45       *
  46       * @param array|\Traversable $helpers (default: null)
  47       * @param string[] $disallowednestedhelpers Names of helpers that aren't allowed to be called within other helpers.
  48       */
  49      public function __construct($helpers = null, array $disallowednestedhelpers = []) {
  50          $this->disallowednestedhelpers = $disallowednestedhelpers;
  51          parent::__construct($helpers);
  52      }
  53  
  54      /**
  55       * Add a helper to this collection.
  56       *
  57       * This function has overridden the parent implementation to provide disallowing
  58       * functionality for certain helpers to prevent them being called from within
  59       * other helpers. This is because the JavaScript helper can be used in a
  60       * security exploit if it can be nested.
  61       *
  62       * The function will wrap callable helpers in an anonymous function that strips
  63       * out the disallowed helpers from the source string before giving it to the
  64       * helper function. This prevents the disallowed helper functions from being
  65       * called by nested render functions from within other helpers.
  66       *
  67       * @see \Mustache_HelperCollection::add()
  68       * @param string $name
  69       * @param mixed  $helper
  70       */
  71      public function add($name, $helper) {
  72  
  73          $disallowedlist = $this->disallowednestedhelpers;
  74  
  75          if (is_callable($helper) && !empty($disallowedlist)) {
  76              $helper = function($source, \Mustache_LambdaHelper $lambdahelper) use ($helper, $disallowedlist) {
  77  
  78                  // Temporarily override the disallowed helpers to return nothing
  79                  // so that they can't be executed from within other helpers.
  80                  $disabledhelpers = $this->disable_helpers($disallowedlist);
  81                  // Call the original function with the modified sources.
  82                  $result = call_user_func($helper, $source, $lambdahelper);
  83                  // Restore the original disallowed helper implementations now
  84                  // that this helper has finished executing so that the rest of
  85                  // the rendering process continues to work correctly.
  86                  $this->restore_helpers($disabledhelpers);
  87                  // Lastly parse the returned string to strip out any unwanted helper
  88                  // tags that were added through variable substitution (or other means).
  89                  // This is done because a secondary render is called on the result
  90                  // of a helper function if it still includes mustache tags. See
  91                  // the section function of Mustache_Compiler for details.
  92                  return $this->strip_disallowed_helpers($disallowedlist, $result);
  93              };
  94          }
  95  
  96          parent::add($name, $helper);
  97      }
  98  
  99      /**
 100       * Disable a list of helpers (by name) by changing their implementation to
 101       * simply return an empty string.
 102       *
 103       * @param  string[] $names List of helper names to disable
 104       * @return \Closure[] The original helper functions indexed by name
 105       */
 106      private function disable_helpers($names) {
 107          $disabledhelpers = [];
 108  
 109          foreach ($names as $name) {
 110              if ($this->has($name)) {
 111                  $function = $this->get($name);
 112                  // Null out the helper. Must call parent::add here to avoid
 113                  // a recursion problem.
 114                  parent::add($name, function() {
 115                      return '';
 116                  });
 117  
 118                  $disabledhelpers[$name] = $function;
 119              }
 120          }
 121  
 122          return $disabledhelpers;
 123      }
 124  
 125      /**
 126       * Restore the original helper implementations. Typically used after disabling
 127       * a helper.
 128       *
 129       * @param  \Closure[] $helpers The helper functions indexed by name
 130       */
 131      private function restore_helpers($helpers) {
 132          foreach ($helpers as $name => $function) {
 133              // Restore the helper functions. Must call parent::add here to avoid
 134              // a recursion problem.
 135              parent::add($name, $function);
 136          }
 137      }
 138  
 139      /**
 140       * Parse the given string and remove any reference to disallowed helpers.
 141       *
 142       * E.g.
 143       * $disallowedlist = ['js'];
 144       * $string = "core, move, {{#js}} some nasty JS hack {{/js}}"
 145       * result: "core, move, {{}}"
 146       *
 147       * @param  string[] $disallowedlist List of helper names to strip
 148       * @param  string $string String to parse
 149       * @return string Parsed string
 150       */
 151      public function strip_disallowed_helpers($disallowedlist, $string) {
 152          $starttoken = \Mustache_Tokenizer::T_SECTION;
 153          $endtoken = \Mustache_Tokenizer::T_END_SECTION;
 154          if ($endtoken == '/') {
 155              $endtoken = '\/';
 156          }
 157  
 158          $regexes = array_map(function($name) use ($starttoken, $endtoken) {
 159              // We only strip out the name of the helper (excluding delimiters)
 160              // the user is able to change the delimeters on a per template
 161              // basis so they may not be curly braces.
 162              return '/\s*' . $starttoken . '\s*'. $name . '\W+.*' . $endtoken . '\s*' . $name . '\s*/';
 163          }, $disallowedlist);
 164  
 165          // This will strip out unwanted helpers from the $source string
 166          // before providing it to the original helper function.
 167          // E.g.
 168          // Before:
 169          // "core, move, {{#js}} some nasty JS hack {{/js}}"
 170          // After:
 171          // "core, move, {{}}".
 172          return preg_replace_callback($regexes, function() {
 173              return '';
 174          }, $string);
 175      }
 176  
 177      /**
 178       * Parse the given string and remove any reference to disallowed helpers.
 179       *
 180       * @deprecated Deprecated since Moodle 3.10 (MDL-69050) - use {@see self::strip_disallowed_helpers()}
 181       * @param  string[] $disallowedlist List of helper names to strip
 182       * @param  string $string String to parse
 183       * @return string Parsed string
 184       */
 185      public function strip_blacklisted_helpers($disallowedlist, $string) {
 186  
 187          debugging('mustache_helper_collection::strip_blacklisted_helpers() is deprecated. ' .
 188              'Please use mustache_helper_collection::strip_disallowed_helpers() instead.', DEBUG_DEVELOPER);
 189  
 190          return $this->strip_disallowed_helpers($disallowedlist, $string);
 191      }
 192  }