feat: VivesPOS landing on Winter CMS 1.2 — theme + plugin + Dockerfile
Some checks are pending
Module sub-split / Sub-split (push) Waiting to run
Some checks are pending
Module sub-split / Sub-split (push) Waiting to run
- Base: wintercms/winter branch 1.2 (full framework) - Theme vivespos: Canvas 7 + Bootstrap 5 CDN, custom CSS - Layout: deferred GTM/GA4 tracking, JSON-LD SoftwareApplication - Partials: hero (offline-first), features, modes (offline/nube toggle), screenshots, pricing (3 planes), comparison, FAQ, CTA - Plugin VivesPOS.Site with ContactForm - Dockerfile: PHP 8.2 Apache, port 80, healthcheck - Added winter/wn-pages, blog, sitemap, seo plugins - Active theme set to vivespos
This commit is contained in:
155
modules/system/tests/twig/FilterTest.php
Normal file
155
modules/system/tests/twig/FilterTest.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace System\Tests\Twig;
|
||||
|
||||
use System\Tests\Bootstrap\TestCase;
|
||||
|
||||
class FilterTest extends TestCase
|
||||
{
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->twig = $this->app->make('twig.environment');
|
||||
}
|
||||
|
||||
public function testFilterUpper()
|
||||
{
|
||||
// Run the Twig Filter tests
|
||||
// @see https://github.com/twigphp/Twig/commit/d475a92c83d13951fbcadd20e64b8ebca82bfc7e
|
||||
$tests = [
|
||||
// "filter" tag applies a filter on its children
|
||||
'filter_upper' => [
|
||||
'template' => <<<TEMPLATE
|
||||
{% filter upper %}
|
||||
Some text with a {{ var }}
|
||||
{% endfilter %}
|
||||
TEMPLATE,
|
||||
'data' => ['var' => 'var'],
|
||||
'expect' => 'SOME TEXT WITH A VAR',
|
||||
],
|
||||
|
||||
// "filter" tag applies a filter on its children
|
||||
'filter_jsonencode' => [
|
||||
'template' => '{% filter json_encode|raw %}test{% endfilter %}',
|
||||
'data' => [],
|
||||
'expect' => '"test"',
|
||||
],
|
||||
|
||||
// "filter" tags accept multiple chained filters
|
||||
'filter_multiple' => [
|
||||
'template' => <<<TEMPLATE
|
||||
{% filter lower|title %}
|
||||
{{ var }}
|
||||
{% endfilter %}
|
||||
TEMPLATE,
|
||||
'data' => ['var' => 'VAR'],
|
||||
'expect' => ' Var',
|
||||
],
|
||||
|
||||
// "filter" tags can be nested at will
|
||||
'filter_multiple' => [
|
||||
'template' => <<<TEMPLATE
|
||||
{% filter lower|title %}
|
||||
{{ var }}
|
||||
{% filter upper %}
|
||||
{{ var }}
|
||||
{% endfilter %}
|
||||
{{ var }}
|
||||
{% endfilter %}
|
||||
TEMPLATE,
|
||||
'data' => ['var' => 'VAR'],
|
||||
'expect' => <<<EXPECT
|
||||
Var
|
||||
Var
|
||||
Var
|
||||
EXPECT,
|
||||
],
|
||||
|
||||
// "filter" tag applies the filter on "for" tags
|
||||
'filter_for' => [
|
||||
'template' => <<<TEMPLATE
|
||||
{% filter upper %}
|
||||
{% for item in items %}
|
||||
{{ item }}
|
||||
{% endfor %}
|
||||
{% endfilter %}
|
||||
TEMPLATE,
|
||||
'data' => ['items' => ['a', 'b']],
|
||||
'expect' => <<<EXPECT
|
||||
A
|
||||
B
|
||||
EXPECT,
|
||||
],
|
||||
|
||||
// "filter" tag applies the filter on "if" tags
|
||||
'filter_for' => [
|
||||
'template' => <<<TEMPLATE
|
||||
{% filter upper %}
|
||||
{% if items %}
|
||||
{{ items|join(', ') }}
|
||||
{% endif %}
|
||||
|
||||
{% if items.3 is defined %}
|
||||
FOO
|
||||
{% else %}
|
||||
{{ items.1 }}
|
||||
{% endif %}
|
||||
|
||||
{% if items.3 is defined %}
|
||||
FOO
|
||||
{% elseif items.1 %}
|
||||
{{ items.0 }}
|
||||
{% endif %}
|
||||
|
||||
{% endfilter %}
|
||||
TEMPLATE,
|
||||
'data' => ['items' => ['a', 'b']],
|
||||
'expect' => <<<EXPECT
|
||||
A, B
|
||||
|
||||
B
|
||||
|
||||
A
|
||||
EXPECT,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($tests as $name => $test) {
|
||||
$template = $this->twig->createTemplate($test['template']);
|
||||
$this->assertEquals(
|
||||
str_replace(
|
||||
"\r\n",
|
||||
"\n",
|
||||
trim($test['expect']),
|
||||
),
|
||||
str_replace(
|
||||
"\r\n",
|
||||
"\n",
|
||||
trim($template->render($test['data']))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function testFilterMdNull()
|
||||
{
|
||||
$template = $this->twig->createTemplate('{% filter md %}{{ value }}{% endfilter %}');
|
||||
|
||||
$this->assertEquals('', $template->render(['value' => null]));
|
||||
}
|
||||
|
||||
public function testFilterMdSafeNull()
|
||||
{
|
||||
$template = $this->twig->createTemplate('{% filter md_safe %}{{ value }}{% endfilter %}');
|
||||
|
||||
$this->assertEquals('', $template->render(['value' => null]));
|
||||
}
|
||||
|
||||
public function testFilterMdLineNull()
|
||||
{
|
||||
$template = $this->twig->createTemplate('{% filter md_line %}{{ value }}{% endfilter %}');
|
||||
|
||||
$this->assertEquals('', $template->render(['value' => null]));
|
||||
}
|
||||
}
|
||||
532
modules/system/tests/twig/SecurityPolicyTest.php
Normal file
532
modules/system/tests/twig/SecurityPolicyTest.php
Normal file
@@ -0,0 +1,532 @@
|
||||
<?php
|
||||
|
||||
namespace System\Tests\Twig;
|
||||
|
||||
use Cms\Classes\Controller;
|
||||
use Cms\Classes\Page;
|
||||
use Cms\Classes\Theme;
|
||||
use System\Tests\Bootstrap\TestCase;
|
||||
use Twig\Environment;
|
||||
use Winter\Storm\Filesystem\Filesystem;
|
||||
use Winter\Storm\Halcyon\Datasource\FileDatasource;
|
||||
|
||||
class SecurityPolicyTest extends TestCase
|
||||
{
|
||||
protected Environment $twig;
|
||||
|
||||
public function testCannotGetTwigInstanceFromCmsController()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
|
||||
$this->renderTwigInCmsController('
|
||||
{% set twig = this.controller.getTwig() %}
|
||||
{{ this.controller.getTwig() }}
|
||||
');
|
||||
}
|
||||
|
||||
public function testAllowedMethods()
|
||||
{
|
||||
// put, get
|
||||
$value = trim($this->renderTwigInCmsController('
|
||||
{{ this.session.put("test", "value") }}
|
||||
{{ this.session.get("test", "default") }}
|
||||
'));
|
||||
$this->assertEquals("value", $value);
|
||||
|
||||
// has
|
||||
$value = trim($this->renderTwigInCmsController('
|
||||
{{ this.session.put("test", "value") }}
|
||||
{% if this.session.has("test") %}success{% else %}failure{% endif %}
|
||||
'));
|
||||
$this->assertEquals("success", $value);
|
||||
|
||||
// forget
|
||||
$value = trim($this->renderTwigInCmsController('
|
||||
{{ this.session.put("test", "value") }}
|
||||
{{ this.session.forget("test") }}
|
||||
{% if this.session.has("test") %}failure{% else %}success{% endif %}
|
||||
'));
|
||||
$this->assertEquals("success", $value);
|
||||
|
||||
// flush
|
||||
$value = trim($this->renderTwigInCmsController('
|
||||
{{ this.session.put("test", "value") }}
|
||||
{{ this.session.flush() }}
|
||||
{% if this.session.has("test") %}failure{% else %}success{% endif %}
|
||||
'));
|
||||
$this->assertEquals("success", $value);
|
||||
|
||||
// Test all other methods blocked
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
$this->renderTwigInCmsController('
|
||||
{{ this.session.driver }}
|
||||
');
|
||||
}
|
||||
|
||||
public function testCannotGetTwigLoaderFromCmsController()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
|
||||
$this->renderTwigInCmsController('
|
||||
{% set loader = this.controller.getLoader() %}
|
||||
{{ loader.load(\'/\') }}
|
||||
');
|
||||
}
|
||||
|
||||
public function testCannotRunAPageObjectFromWithinTwig()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
|
||||
$this->renderTwigInCmsController('
|
||||
{{ this.controller.runPage() }}
|
||||
');
|
||||
}
|
||||
|
||||
public function testCannotExtendAPageWithADynamicMethod()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
|
||||
$this->renderTwigInCmsController('
|
||||
{% set page = this.page.addDynamicMethod("test") %}
|
||||
');
|
||||
}
|
||||
|
||||
public function testCannotExtendAPageWithADynamicProperty()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
|
||||
$this->renderTwigInCmsController('
|
||||
{% set page = this.page.addDynamicProperty("test", "value") %}
|
||||
');
|
||||
}
|
||||
|
||||
public function testCannotWriteToAModel()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
|
||||
$this->renderTwigInCmsController('
|
||||
{% set modelTest = model.setAttribute("test", "value") %}
|
||||
', [
|
||||
'model' => new \Winter\Storm\Database\Model(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function testCanReadFromAModel()
|
||||
{
|
||||
$model = new \Winter\Storm\Database\Model();
|
||||
$model->test = 'value';
|
||||
|
||||
$result = trim($this->renderTwigInCmsController('
|
||||
{% set modelTest = model.getAttribute("test") %}
|
||||
{{- modelTest -}}
|
||||
', [
|
||||
'model' => $model,
|
||||
]));
|
||||
$this->assertEquals('value', $result);
|
||||
}
|
||||
|
||||
public function testCannotAccessModelQuery()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
|
||||
$this->renderTwigInCmsController('
|
||||
{{ dump(model.getQuery) }}
|
||||
', [
|
||||
'model' => new \Winter\Storm\Database\Model(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function testCannotFillAModel()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
|
||||
try {
|
||||
$model = new \Winter\Storm\Database\Model();
|
||||
$model->addFillable('test');
|
||||
$model->test = 'value';
|
||||
|
||||
$this->renderTwigInCmsController('
|
||||
{% set modelTest = model.fill({ test: \'value2\' }) %}
|
||||
', [
|
||||
'model' => new \Winter\Storm\Database\Model(),
|
||||
]);
|
||||
} catch (\Twig\Sandbox\SecurityNotAllowedMethodError $e) {
|
||||
// Ensure value hasn't changed
|
||||
$this->assertEquals('value', $model->test);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function testCannotSaveAModel()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
|
||||
$this->renderTwigInCmsController('
|
||||
{% set modelTest = model.save() %}
|
||||
', [
|
||||
'model' => new \Winter\Storm\Database\Model(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function testCannotPushAModel()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
|
||||
$this->renderTwigInCmsController('
|
||||
{% set modelTest = model.push() %}
|
||||
', [
|
||||
'model' => new \Winter\Storm\Database\Model(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function testCannotUpdateAModel()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
|
||||
$model = new \Winter\Storm\Database\Model();
|
||||
$model->addFillable('test');
|
||||
$model->test = 'value';
|
||||
|
||||
$this->renderTwigInCmsController('
|
||||
{% set modelTest = model.update({ test: \'value2\' }) %}
|
||||
', [
|
||||
'model' => $model,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testCannotDeleteAModel()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
|
||||
$this->renderTwigInCmsController('
|
||||
{% set modelTest = model.delete() %}
|
||||
', [
|
||||
'model' => new \Winter\Storm\Database\Model(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function testCannotForceDeleteAModel()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
|
||||
$this->renderTwigInCmsController('
|
||||
{% set modelTest = model.forceDelete() %}
|
||||
', [
|
||||
'model' => new \Winter\Storm\Database\Model(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function testCannotExtendAModelWithABehaviour()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
|
||||
$this->renderTwigInCmsController('
|
||||
{% set model = model.extendClassWith("Winter\Storm\Database\Behaviors\Encryptable") %}
|
||||
', [
|
||||
'model' => new \Winter\Storm\Database\Model(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function testExtendingModelBeforePassingIntoTwigShouldStillWork()
|
||||
{
|
||||
$model = new \Winter\Storm\Database\Model();
|
||||
$model->addDynamicMethod('foo', function () {
|
||||
return 'foo';
|
||||
});
|
||||
|
||||
$result = trim($this->renderTwigInCmsController('
|
||||
{{- model.foo() -}}
|
||||
', [
|
||||
'model' => $model,
|
||||
]));
|
||||
$this->assertEquals('foo', $result);
|
||||
}
|
||||
|
||||
public function testCannotGetDatasourceFromTheme()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
|
||||
$this->renderTwigInCmsController('
|
||||
{% set datasource = this.theme.getDatasource() %}
|
||||
');
|
||||
}
|
||||
|
||||
// Even if someone decides to be clever and make the datasource available, you shouldn't be able to insert/delete/update
|
||||
public function testCannotDeleteInDatasource()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
|
||||
$this->renderTwigInCmsController('
|
||||
{% set datasource = datasource.delete() %}
|
||||
', [
|
||||
'datasource' => new FileDatasource(
|
||||
base_path('modules/system/tests/fixtures/themes/test'),
|
||||
new Filesystem()
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
public function testCannotInsertInDatasource()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
|
||||
$this->renderTwigInCmsController('
|
||||
{% set datasource = datasource.insert() %}
|
||||
', [
|
||||
'datasource' => new FileDatasource(
|
||||
base_path('modules/system/tests/fixtures/themes/test'),
|
||||
new Filesystem()
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
public function testCannotUpdateInDatasource()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
|
||||
$this->renderTwigInCmsController('
|
||||
{% set datasource = datasource.update() %}
|
||||
', [
|
||||
'datasource' => new FileDatasource(
|
||||
base_path('modules/system/tests/fixtures/themes/test'),
|
||||
new Filesystem()
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
public function testCannotChangeThemeDirectory()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
|
||||
$this->renderTwigInCmsController('
|
||||
{% set theme = this.theme.setDirName("test") %}
|
||||
');
|
||||
}
|
||||
|
||||
//
|
||||
// GHSA-8cfw-pcwh-v63w — bypasses of the CVE-2024-54149 patch, and adjacent vectors
|
||||
//
|
||||
|
||||
public function testCannotSaveQuietlyAModel()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
$this->renderTwigInCmsController('
|
||||
{% set _ = model.saveQuietly() %}
|
||||
', ['model' => new \Winter\Storm\Database\Model()]);
|
||||
}
|
||||
|
||||
public function testCannotForceFillAModel()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
$this->renderTwigInCmsController('
|
||||
{% set _ = model.forceFill({ is_admin: 1 }) %}
|
||||
', ['model' => new \Winter\Storm\Database\Model()]);
|
||||
}
|
||||
|
||||
public function testCannotDestroyAModel()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
$this->renderTwigInCmsController('
|
||||
{% set _ = model.destroy(1) %}
|
||||
', ['model' => new \Winter\Storm\Database\Model()]);
|
||||
}
|
||||
|
||||
// Reaches the Query Builder through the Model's __call forwarding (blocked via the chain)
|
||||
public function testCannotIncrementAModelViaForwarderChain()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
$this->renderTwigInCmsController('
|
||||
{% set _ = model.increment("price", 99999) %}
|
||||
', ['model' => new \Winter\Storm\Database\Model()]);
|
||||
}
|
||||
|
||||
// callable-typed builder method reached via the chain — would execute a string callable
|
||||
public function testCannotCallWhenExecutorOnModel()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
$this->renderTwigInCmsController('
|
||||
{% set _ = model.when(1, "phpinfo") %}
|
||||
', ['model' => new \Winter\Storm\Database\Model()]);
|
||||
}
|
||||
|
||||
public function testCannotCallEachExecutorOnModel()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
$this->renderTwigInCmsController('
|
||||
{% set _ = model.each("phpinfo") %}
|
||||
', ['model' => new \Winter\Storm\Database\Model()]);
|
||||
}
|
||||
|
||||
public function testCannotGetConnectionResolverFromModel()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
$this->renderTwigInCmsController('
|
||||
{% set r = model.getConnectionResolver() %}
|
||||
', ['model' => new \Winter\Storm\Database\Model()]);
|
||||
}
|
||||
|
||||
// The DatabaseManager (ConnectionResolverInterface) forwards any method to a live Connection
|
||||
public function testCannotRunArbitrarySqlViaConnectionResolver()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
$this->renderTwigInCmsController('
|
||||
{% set rows = resolver.select("SELECT 1") %}
|
||||
', ['resolver' => app('db')]);
|
||||
}
|
||||
|
||||
// extend() runs an arbitrary callable bound to the model — a direct RCE primitive
|
||||
public function testCannotExecuteCallableViaExtend()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
$this->renderTwigInCmsController('
|
||||
{% set _ = model.extend("phpinfo") %}
|
||||
', ['model' => new \Winter\Storm\Database\Model()]);
|
||||
}
|
||||
|
||||
public function testCannotRunCallableViaUnguarded()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
$this->renderTwigInCmsController('
|
||||
{% set _ = model.unguarded("phpinfo") %}
|
||||
', ['model' => new \Winter\Storm\Database\Model()]);
|
||||
}
|
||||
|
||||
// Deferred callable-injection via the extension callback registrars
|
||||
public function testCannotRegisterExtendCallbackOnModel()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
$this->renderTwigInCmsController('
|
||||
{% set _ = model.extendableExtendCallback("phpinfo") %}
|
||||
', ['model' => new \Winter\Storm\Database\Model()]);
|
||||
}
|
||||
|
||||
public function testCannotRepointAModelTable()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
$this->renderTwigInCmsController('
|
||||
{% set _ = model.setTable("backend_users") %}
|
||||
', ['model' => new \Winter\Storm\Database\Model()]);
|
||||
}
|
||||
|
||||
// RCE PoC: writing PHP into the current page/layout code section via the Halcyon Builder
|
||||
public function testCannotUpdateViaHalcyonBuilder()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
$this->renderTwigInCmsController('
|
||||
{% set _ = this.page.newQuery().update({ code: "<?php echo 1; ?>" }) %}
|
||||
');
|
||||
}
|
||||
|
||||
public function testCannotUpdateALayoutModel()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
$this->renderTwigInCmsController('
|
||||
{% set _ = this.layout.update({ code: "x" }) %}
|
||||
');
|
||||
}
|
||||
|
||||
public function testCannotWriteThemeConfig()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
$this->renderTwigInCmsController('
|
||||
{% set _ = this.theme.writeConfig({ foo: "bar" }) %}
|
||||
');
|
||||
}
|
||||
|
||||
public function testCannotRunNestedPageCycleFromController()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
$this->renderTwigInCmsController('
|
||||
{% set _ = this.controller.run("/") %}
|
||||
');
|
||||
}
|
||||
|
||||
public function testCannotFireSystemEventFromController()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
$this->renderTwigInCmsController('
|
||||
{% set _ = this.controller.fireSystemEvent("test.event", []) %}
|
||||
');
|
||||
}
|
||||
|
||||
public function testCannotUseSourceFunction()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedFunctionError::class);
|
||||
$this->renderTwigInCmsController('
|
||||
{{ source("backend::index") }}
|
||||
');
|
||||
}
|
||||
|
||||
public function testCannotUseConstantFunction()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedFunctionError::class);
|
||||
$this->renderTwigInCmsController('
|
||||
{{ constant("PHP_VERSION") }}
|
||||
');
|
||||
}
|
||||
|
||||
// Regression guard: the custom GetAttrNode must still enforce the policy (forward $sandboxed)
|
||||
public function testCustomAttributeNodeStillEnforcesPolicy()
|
||||
{
|
||||
$this->expectException(\Twig\Sandbox\SecurityNotAllowedMethodError::class);
|
||||
$this->renderTwigInCmsController('
|
||||
{% set _ = model.save() %}
|
||||
', ['model' => new \Winter\Storm\Database\Model()]);
|
||||
}
|
||||
|
||||
//
|
||||
// SafeCollection — higher-order callable arguments are neutralised, reads still work
|
||||
//
|
||||
|
||||
// filter("is_numeric") would keep only numeric items; stripped to filter(null) it only drops
|
||||
// falsy values, so all three truthy strings survive — proving the callable was neutralised.
|
||||
public function testSafeCollectionStripsMethodCallback()
|
||||
{
|
||||
$result = trim($this->renderTwigInCmsController(
|
||||
'{{- items.filter("is_numeric").count() -}}',
|
||||
['items' => collect(['1', 'a', '2'])]
|
||||
));
|
||||
$this->assertEquals('3', $result);
|
||||
}
|
||||
|
||||
// The built-in attribute() function compiles to an ANY_CALL and must be cast as well.
|
||||
public function testSafeCollectionStripsViaAttributeFunction()
|
||||
{
|
||||
$result = trim($this->renderTwigInCmsController(
|
||||
'{{- attribute(items, "filter", ["is_numeric"]).count() -}}',
|
||||
['items' => collect(['1', 'a', '2'])]
|
||||
));
|
||||
$this->assertEquals('3', $result);
|
||||
}
|
||||
|
||||
public function testSafeCollectionRemainsIterable()
|
||||
{
|
||||
$result = trim($this->renderTwigInCmsController(
|
||||
'{% for i in items.filter("is_numeric") %}{{ i }}{% endfor %}',
|
||||
['items' => collect(['1', 'a', '2'])]
|
||||
));
|
||||
$this->assertEquals('1a2', $result);
|
||||
}
|
||||
|
||||
public function testTwigMapFilterFormStillWorks()
|
||||
{
|
||||
$result = trim($this->renderTwigInCmsController(
|
||||
'{{- items|map(v => v ~ "!")|join(",") -}}',
|
||||
['items' => collect(['a', 'b'])]
|
||||
));
|
||||
$this->assertEquals('a!,b!', $result);
|
||||
}
|
||||
|
||||
protected function renderTwigInCmsController(string $source, array $vars = [])
|
||||
{
|
||||
$controller = new Controller();
|
||||
$twig = $controller->getTwig();
|
||||
$template = $twig->createTemplate($source, 'test.case');
|
||||
|
||||
return $twig->render($template, [
|
||||
'this' => array_merge($controller->getControllerGlobalVars(), [
|
||||
'theme' => new Theme(),
|
||||
]),
|
||||
] + $vars);
|
||||
}
|
||||
}
|
||||
37
modules/system/tests/twig/SpacelessTest.php
Normal file
37
modules/system/tests/twig/SpacelessTest.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace System\Tests\Twig;
|
||||
|
||||
use System\Tests\Bootstrap\TestCase;
|
||||
|
||||
class SpacelessTest extends TestCase
|
||||
{
|
||||
public function testSpacelessNode()
|
||||
{
|
||||
$twig = $this->app->make('twig.environment');
|
||||
$template = $twig->createTemplate(
|
||||
'<p>Test</p>
|
||||
|
||||
{% spaceless %}
|
||||
<p class="test-1 test-2">This is a sentence.</p>
|
||||
|
||||
<p> This is another sentence.</p>
|
||||
{% endspaceless %}'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
str_replace(
|
||||
"\r\n",
|
||||
"\n",
|
||||
'<p>Test</p>
|
||||
|
||||
<p class="test-1 test-2">This is a sentence.</p><p> This is another sentence.</p>',
|
||||
),
|
||||
str_replace(
|
||||
"\r\n",
|
||||
"\n",
|
||||
trim($template->render())
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user