233d7412380fed7dfad288debc17f5be2a56d1f4
[moodle.git] / lib / classes / output / mustache_helper_collection.php
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/>.
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  */
24 namespace core\output;
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 {
34     /**
35      * @var string[] Names of helpers that aren't allowed to be called within other helpers.
36      */
37     private $blacklistednestedhelpers = [];
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[] $blacklistednestedhelpers Names of helpers that aren't allowed to be called within other helpers.
48      */
49     public function __construct($helpers = null, array $blacklistednestedhelpers = []) {
50         $this->blacklistednestedhelpers = $blacklistednestedhelpers;
51         parent::__construct($helpers);
52     }
54     /**
55      * Add a helper to this collection.
56      *
57      * This function has overridden the parent implementation to provide blacklist
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 blacklisted helpers from the source string before giving it to the
64      * helper function. This prevents the blacklisted 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         $blacklist = $this->blacklistednestedhelpers;
75         if (is_callable($helper) && !empty($blacklist)) {
76             $helper = function($source, \Mustache_LambdaHelper $lambdahelper) use ($helper, $blacklist) {
78                 // Temporarily override the blacklisted helpers to return nothing
79                 // so that they can't be executed from within other helpers.
80                 $disabledhelpers = $this->disable_helpers($blacklist);
81                 // Call the original function with the modified sources.
82                 $result = call_user_func($helper, $source, $lambdahelper);
83                 // Restore the original blacklisted 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_blacklisted_helpers($blacklist, $result);
93             };
94         }
96         parent::add($name, $helper);
97     }
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 = [];
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                 });
118                 $disabledhelpers[$name] = $function;
119             }
120         }
122         return $disabledhelpers;
123     }
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     }
139     /**
140      * Parse the given string and remove any reference to blacklisted helpers.
141      *
142      * E.g.
143      * $blacklist = ['js'];
144      * $string = "core, move, {{#js}} some nasty JS hack {{/js}}"
145      * result: "core, move, {{}}"
146      *
147      * @param  string[] $blacklist List of helper names to strip
148      * @param  string $string String to parse
149      * @return string Parsed string
150      */
151     public function strip_blacklisted_helpers($blacklist, $string) {
152         $starttoken = \Mustache_Tokenizer::T_SECTION;
153         $endtoken = \Mustache_Tokenizer::T_END_SECTION;
154         if ($endtoken == '/') {
155             $endtoken = '\/';
156         }
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         }, $blacklist);
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     }