feat: VivesPOS landing on Winter CMS 1.2 — theme + plugin + Dockerfile
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:
2026-08-21 19:29:00 -06:00
commit 1f72193a64
3266 changed files with 531480 additions and 0 deletions

View 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]));
}
}

View 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);
}
}

View 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())
)
);
}
}