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