MDL-62284 output: prevent nested JS mustache handler calls

Prevent the JS mustache helper from being executed from within
the render call of another mustache helper because it can allow
users to inject JS into the page.
This commit is contained in:
Ryan Wyllie
2019-09-04 11:22:51 +08:00
committed by Andrew Nicols
parent 52a74c46ba
commit f9664b0c19
5 changed files with 898 additions and 2 deletions
+77
View File
@@ -0,0 +1,77 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Custom Moodle engine for mustache.
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\output;
/**
* Custom Moodle engine for mustache.
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mustache_engine extends \Mustache_Engine {
/**
* @var mustache_helper_collection
*/
private $helpers;
/**
* @var string[] Names of helpers that aren't allowed to be called within other helpers.
*/
private $blacklistednestedhelpers = [];
/**
* Mustache engine constructor.
*
* This provides an additional option to the parent \Mustache_Engine implementation:
* $options = [
* // A list of helpers (by name) to prevent from executing within the rendering
* // of other helpers.
* 'blacklistednestedhelpers' => ['js']
* ];
* @param array $options [description]
*/
public function __construct(array $options = []) {
if (isset($options['blacklistednestedhelpers'])) {
$this->blacklistednestedhelpers = $options['blacklistednestedhelpers'];
}
parent::__construct($options);
}
/**
* Get the current set of Mustache helpers.
*
* @see Mustache_Engine::setHelpers
*
* @return \Mustache_HelperCollection
*/
public function getHelpers()
{
if (!isset($this->helpers)) {
$this->helpers = new mustache_helper_collection(null, $this->blacklistednestedhelpers);
}
return $this->helpers;
}
}
@@ -0,0 +1,176 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Custom Moodle helper collection for mustache.
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\output;
/**
* Custom Moodle helper collection for mustache.
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mustache_helper_collection extends \Mustache_HelperCollection {
/**
* @var string[] Names of helpers that aren't allowed to be called within other helpers.
*/
private $blacklistednestedhelpers = [];
/**
* Helper Collection constructor.
*
* Optionally accepts an array (or Traversable) of `$name => $helper` pairs.
*
* @throws \Mustache_Exception_InvalidArgumentException if the $helpers argument isn't an array or Traversable
*
* @param array|\Traversable $helpers (default: null)
* @param string[] $blacklistednestedhelpers Names of helpers that aren't allowed to be called within other helpers.
*/
public function __construct($helpers = null, array $blacklistednestedhelpers = []) {
$this->blacklistednestedhelpers = $blacklistednestedhelpers;
parent::__construct($helpers);
}
/**
* Add a helper to this collection.
*
* This function has overridden the parent implementation to provide blacklist
* functionality for certain helpers to prevent them being called from within
* other helpers. This is because the JavaScript helper can be used in a
* security exploit if it can be nested.
*
* The function will wrap callable helpers in an anonymous function that strips
* out the blacklisted helpers from the source string before giving it to the
* helper function. This prevents the blacklisted helper functions from being
* called by nested render functions from within other helpers.
*
* @see \Mustache_HelperCollection::add()
* @param string $name
* @param mixed $helper
*/
public function add($name, $helper)
{
$blacklist = $this->blacklistednestedhelpers;
if (is_callable($helper) && !empty($blacklist)) {
$helper = function($source, \Mustache_LambdaHelper $lambdahelper) use ($helper, $blacklist) {
// Temporarily override the blacklisted helpers to return nothing
// so that they can't be executed from within other helpers.
$disabledhelpers = $this->disable_helpers($blacklist);
// Call the original function with the modified sources.
$result = call_user_func($helper, $source, $lambdahelper);
// Restore the original blacklisted helper implementations now
// that this helper has finished executing so that the rest of
// the rendering process continues to work correctly.
$this->restore_helpers($disabledhelpers);
// Lastly parse the returned string to strip out any unwanted helper
// tags that were added through variable substitution (or other means).
// This is done because a secondary render is called on the result
// of a helper function if it still includes mustache tags. See
// the section function of Mustache_Compiler for details.
return $this->strip_blacklisted_helpers($blacklist, $result);
};
}
parent::add($name, $helper);
}
/**
* Disable a list of helpers (by name) by changing their implementation to
* simply return an empty string.
*
* @param string[] $names List of helper names to disable
* @return \Closure[] The original helper functions indexed by name
*/
private function disable_helpers($names) {
$disabledhelpers = [];
foreach ($names as $name) {
if ($this->has($name)) {
$function = $this->get($name);
// Null out the helper. Must call parent::add here to avoid
// a recursion problem.
parent::add($name, function() {
return '';
});
$disabledhelpers[$name] = $function;
}
}
return $disabledhelpers;
}
/**
* Restore the original helper implementations. Typically used after disabling
* a helper.
*
* @param \Closure[] $helpers The helper functions indexed by name
*/
private function restore_helpers($helpers) {
foreach ($helpers as $name => $function) {
// Restore the helper functions. Must call parent::add here to avoid
// a recursion problem.
parent::add($name, $function);
}
}
/**
* Parse the given string and remove any reference to blacklisted helpers.
*
* E.g.
* $blacklist = ['js'];
* $string = "core, move, {{#js}} some nasty JS hack {{/js}}"
* result: "core, move, {{}}"
*
* @param string[] $blacklist List of helper names to strip
* @param string $string String to parse
* @return string Parsed string
*/
public function strip_blacklisted_helpers($blacklist, $string) {
$starttoken = \Mustache_Tokenizer::T_SECTION;
$endtoken = \Mustache_Tokenizer::T_END_SECTION;
if ($endtoken == '/') {
$endtoken = '\/';
}
$regexes = array_map(function($name) use ($starttoken, $endtoken) {
// We only strip out the name of the helper (excluding delimiters)
// the user is able to change the delimeters on a per template
// basis so they may not be curly braces.
return '/\s*' . $starttoken . '\s*'. $name . '\W+.*' . $endtoken . '\s*' . $name . '\s*/';
}, $blacklist);
// This will strip out unwanted helpers from the $source string
// before providing it to the original helper function.
// E.g.
// Before:
// "core, move, {{#js}} some nasty JS hack {{/js}}"
// After:
// "core, move, {{}}"
return preg_replace_callback($regexes, function() {
return '';
}, $string);
}
}
+6 -2
View File
@@ -120,12 +120,16 @@ class renderer_base {
'userdate' => array($userdatehelper, 'transform'),
);
$this->mustache = new Mustache_Engine(array(
$this->mustache = new \core\output\mustache_engine(array(
'cache' => $cachedir,
'escape' => 's',
'loader' => $loader,
'helpers' => $helpers,
'pragmas' => [Mustache_Engine::PRAGMA_BLOCKS]));
'pragmas' => [Mustache_Engine::PRAGMA_BLOCKS],
// Don't allow the JavaScript helper to be executed from within another
// helper. If it's allowed it can be used by users to inject malicious
// JS into the page.
'blacklistednestedhelpers' => ['js']));
}
@@ -0,0 +1,462 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Unit tests for core renderer render template exploit.
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Unit tests for core renderer render template exploit.
*/
class core_renderer_template_exploit_testcase extends advanced_testcase {
/**
* Test cases to confirm that blacklisted helpers are stripped from the source
* text by the helper before being passed to other another helper. This prevents
* nested calls to helpers.
*/
public function get_template_testcases() {
// Different helper implementations to test various combinations of nested
// calls to render the templates.
$norender = function($text) {
return $text;
};
$singlerender = function($text, $helper) {
return $helper->render($text);
};
$recursiverender = function($text, $helper) {
$result = $helper->render($text);
while (strpos($result, '{{') != false) {
$result = $helper->render($result);
}
return $result;
};
return [
'nested JS helper' => [
'templates' => [
'test' => '{{#testpix}} core, move, {{#js}} some nasty JS {{/js}}{{/testpix}}',
],
'torender' => 'test',
'context' => [],
'helpers' => [
'testpix' => $singlerender
],
'js' => 'some nasty JS',
'expected' => 'core, move,',
'include' => false
],
'other nested helper' => [
'templates' => [
'test' => '{{#testpix}} core, move, {{#test1}} some text {{/test1}}{{/testpix}}',
],
'torender' => 'test',
'context' => [],
'helpers' => [
'testpix' => $singlerender,
'test1' => $norender,
],
'js' => 'some nasty JS',
'expected' => 'core, move, some text',
'include' => false
],
'double nested helper' => [
'templates' => [
'test' => '{{#testpix}} core, move, {{#test1}} some text {{#js}} some nasty JS {{/js}} {{/test1}}{{/testpix}}',
],
'torender' => 'test',
'context' => [],
'helpers' => [
'testpix' => $singlerender,
'test1' => $norender,
],
'js' => 'some nasty JS',
'expected' => 'core, move, some text',
'include' => false
],
'js helper not nested' => [
'templates' => [
'test' => '{{#testpix}} core, move, some text {{/testpix}}{{#js}} some nasty JS {{/js}}',
],
'torender' => 'test',
'context' => [],
'helpers' => [
'testpix' => $singlerender
],
'js' => 'some nasty JS',
'expected' => 'core, move, some text',
'include' => true
],
'js in context not in helper' => [
'templates' => [
'test' => '{{#testpix}} core, move, {{/testpix}}{{hack}}',
],
'torender' => 'test',
'context' => [
'hack' => '{{#js}} some nasty JS {{/js}}'
],
'helpers' => [
'testpix' => $singlerender
],
'js' => 'some nasty JS',
'expected' => 'core, move, {{#js}} some nasty JS {{/js}}',
'include' => false
],
'js in context' => [
'templates' => [
'test' => '{{#testpix}} core, move, {{hack}}{{/testpix}}',
],
'torender' => 'test',
'context' => [
'hack' => '{{#js}} some nasty JS {{/js}}'
],
'helpers' => [
'testpix' => $singlerender
],
'js' => 'some nasty JS',
'expected' => 'core, move,',
'include' => false
],
'js in context double depth with single render' => [
'templates' => [
'test' => '{{#testpix}} core, move, {{first}}{{/testpix}}',
],
'torender' => 'test',
'context' => [
'first' => '{{second}}',
'second' => '{{#js}} some nasty JS {{/js}}'
],
'helpers' => [
'testpix' => $singlerender
],
'js' => 'some nasty JS',
'expected' => 'core, move, {{#js}} some nasty JS {{/js}}',
'include' => false
],
'js in context double depth with recursive render' => [
'templates' => [
'test' => '{{#testpix}} core, move, {{first}}{{/testpix}}',
],
'torender' => 'test',
'context' => [
'first' => '{{second}}',
'second' => '{{#js}} some nasty JS {{/js}}'
],
'helpers' => [
'testpix' => $recursiverender
],
'js' => 'some nasty JS',
'expected' => 'core, move,',
'include' => false
],
'partial' => [
'templates' => [
'test' => '{{#testpix}} core, move, blah{{/testpix}}, {{> test2}}',
'test2' => 'some content',
],
'torender' => 'test',
'context' => [],
'helpers' => [
'testpix' => $recursiverender
],
'js' => 'some nasty JS',
'expected' => 'core, move, blah, some content',
'include' => false
],
'partial nested' => [
'templates' => [
'test' => '{{#testpix}} core, move, {{> test2}}{{/testpix}}',
'test2' => 'some content',
],
'torender' => 'test',
'context' => [],
'helpers' => [
'testpix' => $recursiverender
],
'js' => 'some nasty JS',
'expected' => 'core, move, some content',
'include' => false
],
'partial with js' => [
'templates' => [
'test' => '{{#testpix}} core, move, blah{{/testpix}}, {{> test2}}',
'test2' => '{{#js}} some nasty JS {{/js}}',
],
'torender' => 'test',
'context' => [],
'helpers' => [
'testpix' => $recursiverender
],
'js' => 'some nasty JS',
'expected' => 'core, move, blah,',
'include' => true
],
'partial nested with js' => [
'templates' => [
'test' => '{{#testpix}} core, move, {{> test2}}{{/testpix}}',
'test2' => '{{#js}} some nasty JS {{/js}}',
],
'torender' => 'test',
'context' => [],
'helpers' => [
'testpix' => $recursiverender
],
'js' => 'some nasty JS',
'expected' => 'core, move,',
'include' => false
],
'partial with js from context' => [
'templates' => [
'test' => '{{#testpix}} core, move, blah{{/testpix}}, {{{foo}}}',
'test2' => '{{#js}} some nasty JS {{/js}}',
],
'torender' => 'test',
'context' => [
'foo' => '{{> test2}}'
],
'helpers' => [
'testpix' => $recursiverender
],
'js' => 'some nasty JS',
'expected' => 'core, move, blah, {{> test2}}',
'include' => false
],
'partial nested with js from context recursive render' => [
'templates' => [
'test' => '{{#testpix}} core, move, {{foo}}{{/testpix}}',
'test2' => '{{#js}} some nasty JS {{/js}}',
],
'torender' => 'test',
'context' => [
'foo' => '{{> test2}}'
],
'helpers' => [
'testpix' => $recursiverender
],
'js' => 'some nasty JS',
'expected' => 'core, move,',
'include' => false
],
'partial nested with js from context single render' => [
'templates' => [
'test' => '{{#testpix}} core, move, {{foo}}{{/testpix}}',
'test2' => '{{#js}} some nasty JS {{/js}}',
],
'torender' => 'test',
'context' => [
'foo' => '{{> test2}}'
],
'helpers' => [
'testpix' => $singlerender
],
'js' => 'some nasty JS',
'expected' => 'core, move,',
'include' => false
],
'partial double nested with js from context single render' => [
'templates' => [
'test' => '{{#testpix}} core, move, {{foo}}{{/testpix}}',
'test2' => '{{#js}} some nasty JS {{/js}}',
],
'torender' => 'test',
'context' => [
'foo' => '{{{bar}}}',
'bar' => '{{> test2}}'
],
'helpers' => [
'testpix' => $singlerender
],
'js' => 'some nasty JS',
'expected' => 'core, move, {{> test2}}',
'include' => false
],
'partial double nested with js from context recursive render' => [
'templates' => [
'test' => '{{#testpix}} core, move, {{foo}}{{/testpix}}',
'test2' => '{{#js}} some nasty JS {{/js}}',
],
'torender' => 'test',
'context' => [
'foo' => '{{bar}}',
'bar' => '{{> test2}}'
],
'helpers' => [
'testpix' => $recursiverender
],
'js' => 'some nasty JS',
'expected' => 'core, move,',
'include' => false
],
'array context depth 1' => [
'templates' => [
'test' => '{{#items}}{{#testpix}} core, move, {{.}}{{/testpix}}{{/items}}'
],
'torender' => 'test',
'context' => [
'items' => [
'legit',
'{{#js}}some nasty JS{{/js}}'
]
],
'helpers' => [
'testpix' => $recursiverender
],
'js' => 'some nasty JS',
'expected' => 'core, move, legit core, move,',
'include' => false
],
'array context depth 2' => [
'templates' => [
'test' => '{{#items}}{{#subitems}}{{#testpix}} core, move, {{.}}{{/testpix}}{{/subitems}}{{/items}}'
],
'torender' => 'test',
'context' => [
'items' => [
[
'subitems' => [
'legit',
'{{#js}}some nasty JS{{/js}}'
]
],
]
],
'helpers' => [
'testpix' => $recursiverender
],
'js' => 'some nasty JS',
'expected' => 'core, move, legit core, move,',
'include' => false
],
'object context depth 1' => [
'templates' => [
'test' => '{{#items}}{{#testpix}} core, move, {{.}}{{/testpix}}{{/items}}'
],
'torender' => 'test',
'context' => (object) [
'items' => [
'legit',
'{{#js}}some nasty JS{{/js}}'
]
],
'helpers' => [
'testpix' => $recursiverender
],
'js' => 'some nasty JS',
'expected' => 'core, move, legit core, move,',
'include' => false
],
'object context depth 2' => [
'templates' => [
'test' => '{{#items}}{{#subitems}}{{#testpix}} core, move, {{.}}{{/testpix}}{{/subitems}}{{/items}}'
],
'torender' => 'test',
'context' => (object) [
'items' => [
(object) [
'subitems' => [
'legit',
'{{#js}}some nasty JS{{/js}}'
]
],
]
],
'helpers' => [
'testpix' => $recursiverender
],
'js' => 'some nasty JS',
'expected' => 'core, move, legit core, move,',
'include' => false
],
'change delimeters' => [
'templates' => [
'test' => '{{#testpix}} core, move, {{{foo}}}{{/testpix}}'
],
'torender' => 'test',
'context' => [
'foo' => '{{=<% %>=}} <%#js%>some nasty JS,<%/js%>'
],
'helpers' => [
'testpix' => $recursiverender
],
'js' => 'some nasty JS',
'expected' => 'core, move,',
'include' => false
]
];
}
/**
* Test that the mustache_helper_collection class correctly strips
* @dataProvider get_template_testcases()
* @param string $templates The template to add
* @param string $torender The name of the template to render
* @param array $context The template context
* @param array $helpers Mustache helpers to add
* @param string $js The JS string from the template
* @param string $expected The expected output of the string after stripping JS
* @param bool $include If the JS should be added to the page or not
*/
public function test_core_mustache_engine_strips_js_helper(
$templates,
$torender,
$context,
$helpers,
$js,
$expected,
$include
) {
$page = new \moodle_page();
$renderer = $page->get_renderer('core');
// Get the mustache engine from the renderer.
$reflection = new \ReflectionMethod($renderer, 'get_mustache');
$reflection->setAccessible(true);
$engine = $reflection->invoke($renderer);
// Swap the loader out with an array loader so that we can set some
// inline templates for testing.
$loader = new \Mustache_Loader_ArrayLoader([]);
$engine->setLoader($loader);
// Add our test helpers.
$helpercollection = $engine->getHelpers();
foreach ($helpers as $name => $function) {
$helpercollection->add($name, $function);
}
// Add our test template to be rendered.
foreach ($templates as $name => $template) {
$loader->setTemplate($name, $template);
}
// Confirm that the rendered template matches what we expect.
$this->assertEquals($expected, trim($engine->render($torender, $context)));
if ($include) {
// Confirm that the JS was added to the page.
$this->assertContains($js, $page->requires->get_end_code());
} else {
// Confirm that the JS wasn't added to the page.
$this->assertNotContains($js, $page->requires->get_end_code());
}
}
}
@@ -0,0 +1,177 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Unit tests for lib/classes/output/mustache_helper_collection
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
use core\output\mustache_helper_collection;
/**
* Unit tests for the mustache_helper_collection class.
*/
class core_output_mustache_helper_collection_testcase extends advanced_testcase {
/**
* Test cases to confirm that blacklisted helpers are stripped from the source
* text by the helper before being passed to other another helper. This prevents
* nested calls to helpers.
*/
public function get_strip_blacklisted_helpers_testcases() {
return [
'no blacklist' => [
'blacklist' => [],
'input' => 'core, move, {{#js}} some nasty JS {{/js}}',
'expected' => 'core, move, {{#js}} some nasty JS {{/js}}'
],
'blacklist no match' => [
'blacklist' => ['foo'],
'input' => 'core, move, {{#js}} some nasty JS {{/js}}',
'expected' => 'core, move, {{#js}} some nasty JS {{/js}}'
],
'blacklist partial match 1' => [
'blacklist' => ['js'],
'input' => 'core, move, {{#json}} some nasty JS {{/json}}',
'expected' => 'core, move, {{#json}} some nasty JS {{/json}}'
],
'blacklist partial match 2' => [
'blacklist' => ['js'],
'input' => 'core, move, {{#onjs}} some nasty JS {{/onjs}}',
'expected' => 'core, move, {{#onjs}} some nasty JS {{/onjs}}'
],
'single blacklist 1' => [
'blacklist' => ['js'],
'input' => 'core, move, {{#js}} some nasty JS {{/js}}',
'expected' => 'core, move, {{}}'
],
'single blacklist 2' => [
'blacklist' => ['js'],
'input' => 'core, move, {{ # js }} some nasty JS {{ / js }}',
'expected' => 'core, move, {{}}'
],
'single blacklist 3' => [
'blacklist' => ['js'],
'input' => 'core, {{#js}} some nasty JS {{/js}}, test',
'expected' => 'core, {{}}, test'
],
'single blacklist 3' => [
'blacklist' => ['js'],
'input' => 'core, {{#ok}} this is ok {{/ok}}, {{#js}} some nasty JS {{/js}}',
'expected' => 'core, {{#ok}} this is ok {{/ok}}, {{}}'
],
'single blacklist multiple matches 1' => [
'blacklist' => ['js'],
'input' => 'core, {{#js}} some nasty JS {{/js}}, {{#js}} some nasty JS {{/js}}',
'expected' => 'core, {{}}'
],
'single blacklist multiple matches 2' => [
'blacklist' => ['js'],
'input' => 'core, {{ # js }} some nasty JS {{ / js }}, {{ # js }} some nasty JS {{ / js }}',
'expected' => 'core, {{}}'
],
'single blacklist multiple matches nested 1' => [
'blacklist' => ['js'],
'input' => 'core, move, {{#js}} some nasty JS {{#js}} some nasty JS {{/js}} {{/js}}',
'expected' => 'core, move, {{}}'
],
'single blacklist multiple matches nested 2' => [
'blacklist' => ['js'],
'input' => 'core, move, {{ # js }} some nasty JS {{ # js }} some nasty JS {{ / js }}{{ / js }}',
'expected' => 'core, move, {{}}'
],
'multiple blacklist 1' => [
'blacklist' => ['js', 'foo'],
'input' => 'core, move, {{#js}} some nasty JS {{/js}}',
'expected' => 'core, move, {{}}'
],
'multiple blacklist 2' => [
'blacklist' => ['js', 'foo'],
'input' => 'core, {{#foo}} blah {{/foo}}, {{#js}} js {{/js}}',
'expected' => 'core, {{}}, {{}}'
],
'multiple blacklist 3' => [
'blacklist' => ['js', 'foo'],
'input' => '{{#foo}} blah {{/foo}}, {{#foo}} blah {{/foo}}, {{#js}} js {{/js}}',
'expected' => '{{}}, {{}}'
],
'multiple blacklist 4' => [
'blacklist' => ['js', 'foo'],
'input' => '{{#foo}} blah {{/foo}}, {{#js}} js {{/js}}, {{#foo}} blah {{/foo}}',
'expected' => '{{}}'
],
'multiple blacklist 4' => [
'blacklist' => ['js', 'foo'],
'input' => 'core, move, {{#js}} JS {{#foo}} blah {{/foo}} {{/js}}',
'expected' => 'core, move, {{}}'
],
];
}
/**
* Test that the mustache_helper_collection class correctly strips
* @dataProvider get_strip_blacklisted_helpers_testcases()
* @param string[] $blacklist The list of helpers to strip
* @param string $input The input string for the helper
* @param string $expected The expected output of the string after blacklist strip
*/
public function test_strip_blacklisted_helpers($blacklist, $input, $expected) {
$collection = new mustache_helper_collection(null, $blacklist);
$this->assertEquals($expected, $collection->strip_blacklisted_helpers($blacklist, $input));
}
/**
* Test that the blacklisted helpers are disabled during the execution of other
* helpers.
*
* Any non-blacklisted helper should still be available to call during the
* execution of a helper.
*/
public function test_blacklisted_helpers_disabled_during_execution() {
$engine = new \Mustache_Engine();
$context = new \Mustache_Context();
$lambdahelper = new \Mustache_LambdaHelper($engine, $context);
$blacklist = ['bad'];
$collection = new mustache_helper_collection(null, $blacklist);
$badcalled = false;
$goodcalled = false;
$badhelper = function() use (&$badcalled) {
$badcalled = true;
return '';
};
$goodhelper = function() use (&$goodcalled) {
$goodcalled = true;
return '';
};
// A test helper that just returns the text without modifying it.
$testhelper = function($text, $lambda) use ($collection) {
$collection->get('good')($text, $lambda);
$collection->get('bad')($text, $lambda);
return $text;
};
$collection->add('bad', $badhelper);
$collection->add('good', $goodhelper);
$collection->add('test', $testhelper);
$this->assertEquals('success output', $collection->get('test')('success output', $lambdahelper));
$this->assertTrue($goodcalled);
$this->assertFalse($badcalled);
}
}