MDL-69050 lang: Stop using the term blacklist in mustache output engine
[moodle.git] / lib / classes / output / mustache_helper_collection.php
CommitLineData
f9664b0c
RW
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
24namespace 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 */
32class 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 */
972340b6 37 private $disallowednestedhelpers = [];
f9664b0c
RW
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)
972340b6 47 * @param string[] $disallowednestedhelpers Names of helpers that aren't allowed to be called within other helpers.
f9664b0c 48 */
972340b6
DM
49 public function __construct($helpers = null, array $disallowednestedhelpers = []) {
50 $this->disallowednestedhelpers = $disallowednestedhelpers;
f9664b0c
RW
51 parent::__construct($helpers);
52 }
53
54 /**
55 * Add a helper to this collection.
56 *
972340b6 57 * This function has overridden the parent implementation to provide disallowing
f9664b0c
RW
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
972340b6
DM
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
f9664b0c
RW
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 */
972340b6 71 public function add($name, $helper) {
f9664b0c 72
972340b6 73 $disallowedlist = $this->disallowednestedhelpers;
f9664b0c 74
972340b6
DM
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
f9664b0c 79 // so that they can't be executed from within other helpers.
972340b6 80 $disabledhelpers = $this->disable_helpers($disallowedlist);
f9664b0c
RW
81 // Call the original function with the modified sources.
82 $result = call_user_func($helper, $source, $lambdahelper);
972340b6 83 // Restore the original disallowed helper implementations now
f9664b0c
RW
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.
972340b6 92 return $this->strip_disallowed_helpers($disallowedlist, $result);
f9664b0c
RW
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 /**
972340b6 140 * Parse the given string and remove any reference to disallowed helpers.
f9664b0c
RW
141 *
142 * E.g.
972340b6 143 * $disallowedlist = ['js'];
f9664b0c
RW
144 * $string = "core, move, {{#js}} some nasty JS hack {{/js}}"
145 * result: "core, move, {{}}"
146 *
972340b6 147 * @param string[] $disallowedlist List of helper names to strip
f9664b0c
RW
148 * @param string $string String to parse
149 * @return string Parsed string
150 */
972340b6 151 public function strip_disallowed_helpers($disallowedlist, $string) {
f9664b0c
RW
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*/';
972340b6 163 }, $disallowedlist);
f9664b0c
RW
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:
972340b6 171 // "core, move, {{}}".
f9664b0c
RW
172 return preg_replace_callback($regexes, function() {
173 return '';
174 }, $string);
175 }
972340b6
DM
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 }
f9664b0c 192}