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,495 @@
<?php
namespace System\Traits;
use System\Classes\CombineAssets;
use System\Classes\PluginManager;
use System\Classes\Asset\Vite;
use System\Models\Parameter;
use System\Models\PluginVersion;
use Winter\Storm\Exception\SystemException;
use Winter\Storm\Support\Facades\Event;
use Winter\Storm\Support\Facades\File;
use Winter\Storm\Support\Facades\Html;
use Winter\Storm\Support\Facades\Url;
/**
* Asset Maker Trait
* Adds asset based methods to a class
*
* @package winter\wn-system-module
* @author Alexey Bobkov, Samuel Georges
*/
trait AssetMaker
{
/**
* Collection of assets to display in the layout.
*/
protected array $assets = ['js' => [], 'css' => [], 'rss' => [], 'vite' => []];
/**
* @var string Specifies a path to the asset directory.
*/
public $assetPath;
/**
* Ensures "first-come, first-served" applies to assets of the same ordering.
*/
protected int $orderFactor = 0;
/**
* Disables the use, and subequent broadcast, of assets. This is useful
* to call during an AJAX request to speed things up. This method works
* by specifically targeting the hasAssetsDefined method.
*/
public function flushAssets(): void
{
$this->assets = ['js' => [], 'css' => [], 'rss' => [], 'vite' => []];
}
/**
* Outputs `<link>` and `<script>` tags to load assets previously added
* with addJs, addCss, & addRss method calls depending on the provided $type
*/
public function makeAssets(?string $type = null): ?string
{
if ($type != null) {
$type = strtolower($type);
}
$result = null;
$reserved = ['build', 'order'];
$this->removeDuplicates();
if ($type == null || $type == 'css') {
foreach ($this->orderAssets($this->assets['css']) as $asset) {
$attributes = Html::attributes(array_merge(
[
'rel' => 'stylesheet',
'href' => $this->getAssetEntryBuildPath($asset)
],
array_except($asset['attributes'], $reserved)
));
$result .= '<link' . $attributes . '>' . PHP_EOL;
}
foreach ($this->assets['vite'] as $asset) {
$asset['attributes']['entrypoints'] = array_filter(
$asset['attributes']['entrypoints'],
fn ($entrypoint) => $this->getAssetType($entrypoint) === 'css'
);
if ($asset['attributes']['entrypoints']) {
$result .= Vite::tags($asset['attributes']['entrypoints'], $asset['path']);
}
}
}
if ($type == null || $type == 'rss') {
foreach ($this->orderAssets($this->assets['rss']) as $asset) {
$attributes = Html::attributes(array_merge(
[
'rel' => 'alternate',
'href' => $this->getAssetEntryBuildPath($asset),
'title' => 'RSS',
'type' => 'application/rss+xml'
],
array_except($asset['attributes'], $reserved)
));
$result .= '<link' . $attributes . '>' . PHP_EOL;
}
}
if ($type == null || $type == 'js') {
foreach ($this->orderAssets($this->assets['js']) as $asset) {
$attributes = Html::attributes(array_merge(
[
'src' => $this->getAssetEntryBuildPath($asset)
],
array_except($asset['attributes'], $reserved)
));
$result .= '<script' . $attributes . '></script>' . PHP_EOL;
}
foreach ($this->assets['vite'] as $asset) {
$asset['attributes']['entrypoints'] = array_filter(
$asset['attributes']['entrypoints'],
fn ($entrypoint) => $this->getAssetType($entrypoint) === 'js'
);
if ($asset['attributes']['entrypoints']) {
$result .= Vite::tags($asset['attributes']['entrypoints'], $asset['path']);
}
}
}
if ($type == 'vite') {
foreach ($this->assets['vite'] as $asset) {
$result .= Vite::tags($asset['attributes']['entrypoints'], $asset['path']);
}
}
return $result;
}
/**
* Adds JavaScript asset to the asset list. Call $this->makeAssets() in a view
* to output corresponding markup.
* @param string|array $name When an array of paths are provided they will be passed to the Asset Combiner
* @param array|string $attributes When a string is provided it will be used as the 'build' attribute value
*/
public function addJs(string|array $name, array|string $attributes = []): void
{
if (is_array($name)) {
$name = $this->combineAssets($name, $this->getLocalPath($this->assetPath));
}
// Alias october* assets to winter.*
if (str_contains($name, 'js/october')) {
$winterPath = str_replace('js/october', 'js/winter', $name);
if (file_exists(base_path(ltrim(parse_url($winterPath, PHP_URL_PATH), '/')))) {
$name = $winterPath;
}
}
$jsPath = $this->getAssetPath($name);
if (isset($this->controller)) {
$this->controller->addJs($jsPath, $attributes);
}
if (is_string($attributes)) {
$attributes = ['build' => $attributes];
}
$jsPath = $this->getAssetScheme($jsPath);
$this->addAsset('js', $jsPath, $attributes);
}
/**
* Adds StyleSheet asset to the asset list. Call $this->makeAssets() in a view
* to output corresponding markup.
* @param string|array $name When an array of paths are provided they will be passed to the Asset Combiner
* @param array|string $attributes When a string is provided it will be used as the 'build' attribute value
*/
public function addCss(string|array $name, array|string $attributes = []): void
{
if (is_array($name)) {
$name = $this->combineAssets($name, $this->getLocalPath($this->assetPath));
}
$cssPath = $this->getAssetPath($name);
if (isset($this->controller)) {
$this->controller->addCss($cssPath, $attributes);
}
if (is_string($attributes)) {
$attributes = ['build' => $attributes];
}
$cssPath = $this->getAssetScheme($cssPath);
$this->addAsset('css', $cssPath, $attributes);
}
/**
* Adds an RSS link asset to the asset list. Call $this->makeAssets() in a view
* to output corresponding markup.
*/
public function addRss(string $name, array|string $attributes = []): void
{
$rssPath = $this->getAssetPath($name);
if (isset($this->controller)) {
$this->controller->addRss($rssPath, $attributes);
}
if (is_string($attributes)) {
$attributes = ['build' => $attributes];
}
$rssPath = $this->getAssetScheme($rssPath);
$this->addAsset('rss', $rssPath, $attributes);
}
/**
* Adds Vite tags
* @param string|array $entrypoints The list of entry points for Vite
* @param ?string $package The package name of the plugin or theme
*/
public function addVite(array|string $entrypoints, ?string $package = null): void
{
if (!is_array($entrypoints)) {
$entrypoints = [$entrypoints];
}
// If package was not set, attempt to guess
if (is_null($package)) {
$caller = get_called_class();
if (!($plugin = PluginManager::instance()->findByNamespace($caller))) {
throw new SystemException('Unable to determine vite package from namespace: ' . $caller);
}
// Set package to the plugin id
$package = $plugin->getPluginIdentifier();
}
if (isset($this->controller)) {
$this->controller->addVite($entrypoints, $package);
}
$this->addAsset('vite', $package, [
'entrypoints' => $entrypoints
]);
}
/**
* Adds the provided asset to the internal asset collections
*/
protected function addAsset(string $type, string $path, array $attributes): void
{
if (!in_array($path, $this->assets[$type])) {
/**
* @event system.assets.beforeAddAsset
* Provides an opportunity to inspect or modify an asset.
*
* The parameters provided are:
* string `$type`: The type of the asset being added
* string `$path`: The path to the asset being added
* array `$attributes`: The array of attributes for the asset being added.
*
* All the parameters are provided by reference for modification.
* This event is also a halting event, so returning false will prevent the
* current asset from being added. Note that duplicates are filtered out
* before the event is fired.
*
* Example usage:
*
* Event::listen('system.assets.beforeAddAsset', function (string &$type, string &$path, array &$attributes) {
* if (in_array($path, $blockedAssets)) {
* return false;
* }
* });
*
* Or
*
* $this->bindEvent('assets.beforeAddAsset', function (string &$type, string &$path, array &$attributes) {
* $attributes['special_cdn_flag'] = false;
* });
*
*/
if (
// Fire local event if exists
(
method_exists($this, 'fireEvent') &&
($this->fireEvent('assets.beforeAddAsset', [&$type, &$path, &$attributes], true) !== false)
) &&
// Fire global event
(Event::fire('system.assets.beforeAddAsset', [&$type, &$path, &$attributes], true) !== false)
) {
$this->orderFactor++;
// Apply ordering
$attributes['order'] = (!isset($attributes['order']))
? 500 + ($this->orderFactor / 10000)
: intval($attributes['order']) + ($this->orderFactor / 10000);
$this->assets[$type][] = ['path' => $path, 'attributes' => $attributes];
}
}
}
/**
* Run the provided assets through the Asset Combiner
*/
public function combineAssets(array $assets, string $localPath = ''): string
{
// Short circuit if no assets actually provided
if (empty($assets)) {
return '';
}
$assetPath = !empty($localPath) ? $localPath : $this->assetPath;
return Url::to(CombineAssets::combine($assets, $assetPath));
}
/**
* Returns an array of all registered asset paths.
*
* Assets will be prioritized based on their defined ordering.
*/
public function getAssetPaths(): array
{
$this->removeDuplicates();
$assets = [];
foreach ($this->assets as $type => $collection) {
$assets[$type] = [];
foreach ($this->orderAssets($collection) as $asset) {
$assets[$type][] = $this->getAssetEntryBuildPath($asset);
}
}
return $assets;
}
/**
* Returns the URL to the provided asset. If the provided fileName is a relative path
* without a leading slash it will be assumbed to be relative to the asset path.
*/
public function getAssetPath(string $fileName, ?string $assetPath = null): string
{
if (starts_with($fileName, ['//', 'http://', 'https://'])) {
return $fileName;
}
if (!$assetPath) {
$assetPath = $this->assetPath;
}
// Process absolute or symbolized paths
$publicPath = File::localToPublic(File::symbolizePath($fileName));
if ($publicPath) {
$fileName = $publicPath;
}
if (substr($fileName, 0, 1) == '/' || $assetPath === null) {
return Url::asset($fileName);
}
return Url::asset($assetPath . '/' . $fileName);
}
/**
* Returns true if assets any have been added.
*/
public function hasAssetsDefined(): bool
{
return count($this->assets, COUNT_RECURSIVE) > 3;
}
/**
* Internal helper, attaches a build code to an asset path
*/
protected function getAssetEntryBuildPath(array $asset): string
{
$path = $asset['path'];
if (isset($asset['attributes']['build'])) {
$build = $asset['attributes']['build'];
if ($build == 'core') {
$build = 'v' . Parameter::get('system::core.build', 1);
} elseif ($pluginVersion = PluginVersion::getVersion($build)) {
$build = 'v' . $pluginVersion;
}
$path .= '?' . $build;
}
return $path;
}
/**
* Internal helper, get asset scheme
*/
protected function getAssetScheme(string $asset): string
{
if (starts_with($asset, ['//', 'http://', 'https://'])) {
return $asset;
}
if (substr($asset, 0, 1) == '/') {
$asset = Url::asset($asset);
}
return $asset;
}
/**
* Removes duplicate assets from the entire collection.
*/
protected function removeDuplicates(): void
{
foreach ($this->assets as $type => &$collection) {
$pathCache = [];
foreach ($collection as $key => $asset) {
if (!$path = array_get($asset, 'path')) {
continue;
}
if ($type === 'vite') {
// If handling vite, ensure that each entrypoint is considered it's own path within a package
foreach ($asset['attributes']['entrypoints'] ?? [] as $index => $entrypoint) {
$vitePath = $path . '|' . $entrypoint;
// If we detect a duplicate, remove it
if (isset($pathCache[$vitePath])) {
array_forget($collection[$key]['attributes']['entrypoints'], $index);
// If all entry points have been removed from an asset, then remove it
if (empty($collection[$key]['attributes']['entrypoints'])) {
unset($collection[$key]);
}
continue;
}
$pathCache[$vitePath] = true;
}
continue;
}
if (isset($pathCache[$path])) {
array_forget($collection, $key);
continue;
}
$pathCache[$path] = true;
}
}
}
protected function getLocalPath(?string $relativePath): string
{
$relativePath = File::symbolizePath((string) $relativePath);
if (!starts_with($relativePath, [base_path()])) {
$relativePath = base_path($relativePath);
}
return $relativePath;
}
/**
* Prioritize assets based on the given order.
*/
public function orderAssets(array $assets): array
{
// Copy assets array so that the stored asset array is not modified.
$sortedAssets = $assets;
array_multisort(
array_map(function ($item) {
return $item['attributes']['order'];
}, $sortedAssets),
SORT_NUMERIC,
SORT_ASC,
$sortedAssets
);
return $sortedAssets;
}
protected function getAssetType(string $asset): ?string
{
$path = strtolower(parse_url($asset, PHP_URL_PATH) ?? $asset);
$ext = pathinfo($path, PATHINFO_EXTENSION);
return match ($ext) {
'js', 'mjs', 'cjs', 'ts', 'tsx', 'jsx', 'vue', 'svelte' => 'js',
'css', 'scss', 'sass', 'less', 'styl', 'stylus', 'pcss', 'postcss' => 'css',
default => null,
};
}
}

View File

@@ -0,0 +1,212 @@
<?php namespace System\Traits;
use Yaml;
use File;
use Lang;
use Event;
use SystemException;
use stdClass;
use Config;
/**
* Config Maker Trait
* Adds configuration based methods to a class
*
* @package winter\wn-system-module
* @author Alexey Bobkov, Samuel Georges
*/
trait ConfigMaker
{
/**
* @var string Specifies a path to the config directory.
*/
protected $configPath;
/**
* Reads the contents of the supplied file and applies it to this object.
* @param array $configFile
* @param array $requiredConfig
* @return array|stdClass
*/
public function makeConfig($configFile = [], $requiredConfig = [])
{
if (!$configFile) {
$configFile = [];
}
/*
* Config already made
*/
if (is_object($configFile)) {
$config = $configFile;
}
/*
* Embedded config
*/
elseif (is_array($configFile)) {
$config = $this->makeConfigFromArray($configFile);
}
/*
* Process config from file contents
*/
else {
if (isset($this->controller) && method_exists($this->controller, 'getConfigPath')) {
$configFile = $this->controller->getConfigPath($configFile);
}
else {
$configFile = $this->getConfigPath($configFile);
}
if (!File::isFile($configFile)) {
throw new SystemException(Lang::get(
'system::lang.config.not_found',
['file' => $configFile, 'location' => get_called_class()]
));
}
$config = Yaml::parseFile($configFile);
/**
* @event system.extendConfigFile
* Provides an opportunity to modify config files
*
* Example usage:
*
* Event::listen('system.extendConfigFile', function ((string) $path, (array) $config) {
* if ($path === '/plugins/author/plugin-name/controllers/mycontroller/config_relation.yaml') {
* unset($config['property_value']['view']['recordUrl']);
* return $config;
* }
* });
*
*/
$publicFile = File::localToPublic($configFile);
if ($results = Event::fire('system.extendConfigFile', [$publicFile, $config])) {
foreach ($results as $result) {
if (!is_array($result)) {
continue;
}
$config = array_merge($config, $result);
}
}
$config = $this->makeConfigFromArray($config);
}
/*
* Validate required configuration
*/
foreach ($requiredConfig as $property) {
if (!property_exists($config, $property)) {
throw new SystemException(Lang::get(
'system::lang.config.required',
['property' => $property, 'location' => get_called_class()]
));
}
}
return $config;
}
/**
* Makes a config object from an array, making the first level keys properties of a new object.
*
* @param array $configArray Config array.
* @return stdClass The config object
*/
public function makeConfigFromArray($configArray = [])
{
$object = new stdClass;
if (!is_array($configArray)) {
return $object;
}
foreach ($configArray as $name => $value) {
$object->{$name} = $value;
}
return $object;
}
/**
* Locates a file based on it's definition. If the file starts with
* the ~ symbol it will be returned in context of the application base path,
* otherwise it will be returned in context of the config path.
* @param string $fileName File to load.
* @param mixed $configPath Explicitly define a config path.
* @return string Full path to the config file.
*/
public function getConfigPath($fileName, $configPath = null)
{
if (!isset($this->configPath)) {
$this->configPath = $this->guessConfigPath();
}
if (!$configPath) {
$configPath = $this->configPath;
}
$fileName = File::symbolizePath($fileName);
if (File::isLocalPath($fileName) ||
(!Config::get('cms.restrictBaseDir', true) && realpath($fileName) !== false)
) {
return $fileName;
}
if (!is_array($configPath)) {
$configPath = [$configPath];
}
foreach ($configPath as $path) {
$_fileName = $path . '/' . $fileName;
if (File::isFile($_fileName)) {
return $_fileName;
}
}
return $fileName;
}
/**
* Guess the package path for the called class.
* @param string $suffix An extra path to attach to the end
* @return string
*/
public function guessConfigPath($suffix = '')
{
$class = get_called_class();
return $this->guessConfigPathFrom($class, $suffix);
}
/**
* Guess the package path from a specified class.
* @param string $class Class to guess path from.
* @param string $suffix An extra path to attach to the end
* @return string
*/
public function guessConfigPathFrom($class, $suffix = '')
{
$classFolder = strtolower(class_basename($class));
$classFile = realpath(dirname(File::fromClass($class)));
return $classFile ? $classFile . '/' . $classFolder . $suffix : null;
}
/**
* Merges two configuration sources, either prepared or not, and returns
* them as a single configuration object.
* @param mixed $configA
* @param mixed $configB
* @return stdClass The config object
*/
public function mergeConfig($configA, $configB)
{
$configA = $this->makeConfig($configA);
$configB = $this->makeConfig($configB);
return (object) array_merge((array) $configA, (array) $configB);
}
}

View File

@@ -0,0 +1,92 @@
<?php namespace System\Traits;
use Event;
/**
* Adds system event related features to any class.
*
* @package winter\wn-system-module
* @author Alexey Bobkov, Samuel Georges
*/
trait EventEmitter
{
use \Winter\Storm\Support\Traits\Emitter;
/**
* Fires a combination of local and global events. The first segment is removed
* from the event name locally and the local object is passed as the first
* argument to the event globally. Halting is also enabled by default.
*
* For example:
*
* $this->fireSystemEvent('backend.list.myEvent', ['my value']);
*
* Is equivalent to:
*
* $this->fireEvent('list.myEvent', ['myvalue'], true);
*
* Event::fire('backend.list.myEvent', [$this, 'myvalue'], true);
*
* @param string $event Event name
* @param array $params Event parameters
* @param boolean $halt Halt after first non-null result
* @return mixed
*/
public function fireSystemEvent($event, $params = [], $halt = true)
{
$result = [];
$shortEvent = substr($event, strpos($event, '.') + 1);
$longArgs = array_merge([$this], $params);
/*
* Local event first
*/
if ($response = $this->fireEvent($shortEvent, $params, $halt)) {
if ($halt) {
return $response;
}
$result = array_merge($result, $response);
}
/*
* Global event second
*/
if ($response = Event::fire($event, $longArgs, $halt)) {
if ($halt) {
return $response;
}
$result = array_merge($result, $response);
}
return $result;
}
/**
* Special event function used for extending within view files,
* allowing HTML to be injected multiple times.
*
* For example:
*
* <?= $this->fireViewEvent('backend.auth.extendSigninView') ?>
*
* @param string $event Event name
* @param array $params Event parameters
* @return string
*/
public function fireViewEvent($event, $params = [])
{
// Add the local object to the first parameter always
array_unshift($params, $this);
if ($result = Event::fire($event, $params)) {
return implode(PHP_EOL.PHP_EOL, (array) $result);
}
return '';
}
}

View File

@@ -0,0 +1,28 @@
<?php namespace System\Traits;
/**
* Lazy Owner Alias
* Adds support for statically binding owner aliases
*
* @package winter\wn-system-module
* @author Jack Wilkinson
*/
trait LazyOwnerAlias
{
/**
* @var array List of aliases
*/
protected static $lazyAliases = [];
/**
* Binds the alias of an owner to the lazy alias list. This allows us to bind aliases
* prior to init() which is necessary to do before to `PluginManager` being registered.
* @param string $owner
* @param string $alias
* @return void
*/
public static function lazyRegisterOwnerAlias(string $owner, string $alias)
{
static::$lazyAliases[$alias] = $owner;
}
}

View File

@@ -0,0 +1,121 @@
<?php namespace System\Traits;
/**
* Property container trait
*
* Adds properties and methods for classes that could define properties,
* like components or report widgets.
*
* @package winter\wn-system-module
* @author Alexey Bobkov, Samuel Georges
*/
trait PropertyContainer
{
/**
* @var array Contains the object property values.
*/
protected $properties = [];
/**
* Validates the properties against the defined properties of the class.
* This method also sets default properties.
* @param array $properties The supplied property values.
* @return array The validated property set, with defaults applied.
*/
public function validateProperties(array $properties)
{
$definedProperties = $this->defineProperties() ?: [];
/*
* Determine and implement default values
*/
$defaultProperties = [];
foreach ($definedProperties as $name => $information) {
if (array_key_exists('default', $information)) {
$defaultProperties[$name] = $information['default'];
}
}
$properties = array_merge($defaultProperties, $properties);
return $properties;
}
/**
* Defines the properties used by this class.
*
* This method should be overriden in your extended class and return an array of properties that your class uses,
* with the keys of the array being the name of the properties, and the values being an array of property
* parameters.
*
* Example:
* return [
* 'propertyName' => [
* 'title' => 'Property name',
* 'description' => 'Property description',
* 'default' => 'Default value'
* ],
* ];
*
* @return array
*/
public function defineProperties()
{
return [];
}
/**
* Sets multiple properties.
* @param array $properties
* @return void
*/
public function setProperties($properties)
{
$this->properties = $this->validateProperties($properties);
}
/**
* Sets a property value
* @param string $name
* @param mixed $value
* @return void
*/
public function setProperty($name, $value)
{
$this->properties[$name] = $value;
}
/**
* Returns all properties.
* @return array
*/
public function getProperties()
{
return $this->properties;
}
/**
* Returns a defined property value or default if one is not set.
* @param string $name The property name to look for.
* @param string $default A default value to return if no name is found.
* @return mixed The property value or the default specified.
*/
public function property($name, $default = null)
{
return array_key_exists($name, $this->properties)
? $this->properties[$name]
: $default;
}
/**
* Returns options for multi-option properties (drop-downs, etc.)
* @param string $property Specifies the property name
* @return array Return an array of option values and descriptions
*/
public function getPropertyOptions($property)
{
return [];
}
}

View File

@@ -0,0 +1,136 @@
<?php namespace System\Traits;
use Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\HttpFoundation\Response as BaseResponse;
/**
* Response Maker Trait
* Stores attributes the can be used to prepare a response from the server.
*
* @package winter\wn-system-module
* @author Alexey Bobkov, Samuel Georges
*/
trait ResponseMaker
{
/**
* @var int Response status code
*/
protected $statusCode = 200;
/**
* @var mixed Override the standard controller response.
*/
protected $responseOverride = null;
/**
* @var Symfony\Component\HttpFoundation\ResponseHeaderBag
*/
protected $responseHeaderBag = null;
/**
* Sets the status code for the current web response.
* @param int $code Status code
* @return $this
*/
public function setStatusCode($code)
{
$this->statusCode = (int) $code;
return $this;
}
/**
* Returns the status code for the current web response.
* @return int Status code
*/
public function getStatusCode()
{
return $this->statusCode;
}
/**
* Sets the response for the current page request cycle, this value takes priority
* over the standard response prepared by the controller.
* @param mixed $response Response object or string
* @return $this
*/
public function setResponse($response)
{
$this->responseOverride = $response;
return $this;
}
/**
* Set a header on the Response.
*
* @param string $key
* @param array|string $values
* @param bool $replace
* @return $this
*/
public function setResponseHeader($key, $values, $replace = true)
{
if ($this->responseHeaderBag === null) {
$this->responseHeaderBag = new ResponseHeaderBag;
}
$this->responseHeaderBag->set($key, $values, $replace);
return $this;
}
/**
* Add a cookie to the response.
*
* @param \Symfony\Component\HttpFoundation\Cookie|mixed $cookie
* @return $this
*/
public function setResponseCookie($cookie)
{
if ($this->responseHeaderBag === null) {
$this->responseHeaderBag = new ResponseHeaderBag;
}
if (is_string($cookie) && function_exists('cookie')) {
$cookie = call_user_func_array('cookie', func_get_args());
}
$this->responseHeaderBag->setCookie($cookie);
return $this;
}
/**
* Get the header response bag
* @return Symfony\Component\HttpFoundation\ResponseHeaderBag|null
*/
public function getResponseHeaders()
{
return $this->responseHeaderBag;
}
/**
* Prepares a response that considers overrides and custom responses.
* @param mixed $contents
* @return mixed
*/
public function makeResponse($contents)
{
if ($this->responseOverride !== null) {
$contents = $this->responseOverride;
}
if (is_string($contents)) {
$contents = Response::make($contents, $this->getStatusCode());
}
$responseHeaders = $this->getResponseHeaders();
if ($responseHeaders && $contents instanceof BaseResponse) {
$contents = $contents->withHeaders($responseHeaders);
}
return $contents;
}
}

View File

@@ -0,0 +1,82 @@
<?php namespace System\Traits;
use Crypt;
use Config;
use Request;
use Session;
use Carbon\Carbon;
use Symfony\Component\HttpFoundation\Response as BaseResponse;
use Symfony\Component\HttpFoundation\Cookie;
/**
* Security Controller Trait
* Adds cross-site scripting protection methods to a controller based class
*
* @package winter\wn-system-module
* @author Alexey Bobkov, Samuel Georges
*/
trait SecurityController
{
/**
* Adds anti-CSRF cookie.
* Adds a cookie with a token for CSRF checks to the response.
*
* @return \Symfony\Component\HttpFoundation\Cookie
*/
protected function makeXsrfCookie()
{
$config = Config::get('session');
return new Cookie(
'XSRF-TOKEN',
Session::token(),
Carbon::now()->addMinutes((int) $config['lifetime'])->getTimestamp(),
$config['path'],
$config['domain'],
$config['secure'],
false,
false,
$config['same_site'] ?? null
);
}
/**
* Checks the request data / headers for a valid CSRF token.
*
* @return bool Returns false if a valid token is not found or cms.enableCsrfProtection is set to false
*/
protected function verifyCsrfToken()
{
if (!Config::get('cms.enableCsrfProtection', true)) {
return true;
}
$token = Request::input('_token') ?: Request::header('X-CSRF-TOKEN');
if (!$token && $header = Request::header('X-XSRF-TOKEN')) {
$token = Crypt::decrypt($header, false);
}
if (!strlen($token) || !strlen(Session::token())) {
return false;
}
return hash_equals(
Session::token(),
$token
);
}
/**
* Checks if the back-end should force a secure protocol (HTTPS) enabled by config.
* @return bool
*/
protected function verifyForceSecure()
{
if (Request::secure() || Request::ajax()) {
return true;
}
return !Config::get('cms.backendForceSecure', false);
}
}

View File

@@ -0,0 +1,322 @@
<?php namespace System\Traits;
use File;
use Lang;
use Block;
use SystemException;
use Throwable;
use Config;
/**
* View Maker Trait
* Adds view based methods to a class
*
* @package winter\wn-system-module
* @author Alexey Bobkov, Samuel Georges
*/
trait ViewMaker
{
/**
* @var array A list of variables to pass to the page.
*/
public $vars = [];
/**
* @var string|array Specifies a path to the views directory.
*/
protected $viewPath;
/**
* @var string Specifies a path to the layout directory.
*/
protected $layoutPath;
/**
* @var string Layout to use for the view.
*/
public $layout;
/**
* @var bool Prevents the use of a layout.
*/
public $suppressLayout = false;
/**
* Prepends a path on the available view path locations.
*
* @param array|string $path
* @return void
*/
public function prependViewPath(array|string $path): void
{
$this->viewPath = (array) $this->viewPath;
if (is_array($path)) {
$this->viewPath = array_merge($path, $this->viewPath);
} else {
array_unshift($this->viewPath, $path);
}
}
/**
* Append a path on the available view path locations.
*
* @param array|string $path
* @return void
*/
public function appendViewPath(array|string $path): void
{
$this->viewPath = (array) $this->viewPath;
if (is_array($path)) {
$this->viewPath = array_merge($this->viewPath, $path);
} else {
$this->viewPath[] = $path;
}
}
/**
* Prepends a path on the available view path locations.
*
* @deprecated Use prependViewPath()
*/
public function addViewPath(string|array $path): void
{
$this->prependViewPath($path);
}
/**
* Returns the active view path locations.
*/
public function getViewPaths(): array
{
return (array) $this->viewPath;
}
/**
* Render a partial file contents located in the views folder.
* @return mixed Partial contents or false if not throwing an exception.
*/
public function makePartial(string $partial, array $params = [], bool $throwException = true)
{
$notRealPath = realpath($partial) === false || is_dir($partial) === true;
if (!File::isPathSymbol($partial) && $notRealPath) {
$folder = strpos($partial, '/') !== false ? dirname($partial) . '/' : '';
$partial = $folder . '_' . strtolower(basename($partial));
}
$partialPath = $this->getViewPath($partial);
if (!File::exists($partialPath)) {
if ($throwException) {
throw new SystemException(Lang::get('backend::lang.partial.not_found_name', ['name' => $partialPath]));
}
return false;
}
return $this->makeFileContents($partialPath, $params);
}
/**
* Loads the specified view. Applies the layout if one is set.
* The view file must have the .php extension (or ".htm" for historical reasons) and be located in the views directory
*/
public function makeView(string $view): string
{
$viewPath = $this->getViewPath(strtolower($view));
$contents = $this->makeFileContents($viewPath);
return $this->makeViewContent($contents);
}
/**
* Renders supplied contents inside a layout.
*/
public function makeViewContent(string $contents, ?string $layout = null): string
{
if ($this->suppressLayout || $this->layout == '') {
return $contents;
}
// Append any undefined block content to the body block
Block::set('undefinedBlock', $contents);
Block::append('body', Block::get('undefinedBlock'));
return $this->makeLayout($layout);
}
/**
* Render a layout, defaulting to the layout propery specified on the class
* @return string|bool The layout contents, or false.
*/
public function makeLayout(?string $name = null, array $params = [], bool $throwException = true): string|bool
{
$layout = $name ?? $this->layout;
if ($layout == '') {
return '';
}
$layoutPath = $this->getViewPath($layout, $this->layoutPath);
if (!File::exists($layoutPath)) {
if ($throwException) {
throw new SystemException(Lang::get('cms::lang.layout.not_found_name', ['name' => $layoutPath]));
}
return false;
}
return $this->makeFileContents($layoutPath, $params);
}
/**
* Renders a layout partial
*/
public function makeLayoutPartial(string $partial, array $params = []): string
{
if (!File::isLocalPath($partial) && !File::isPathSymbol($partial)) {
$folder = strpos($partial, '/') !== false ? dirname($partial) . '/' : '';
$partial = $folder . '_' . strtolower(basename($partial));
}
return $this->makeLayout($partial, $params);
}
/**
* Locates a file based on its definition. The file name can be prefixed with a
* symbol (~|$) to return in context of the application or plugin base path,
* otherwise it will be returned in context of this object view path.
*
* If the fileName cannot be found it will be returned unmodified.
*/
public function getViewPath(string $fileName, string|array|null $viewPaths = null): string
{
$input = $fileName;
$allowedExtensions = ['php', 'htm'];
if (!isset($this->viewPath)) {
$this->viewPath = $this->guessViewPath();
}
if (!$viewPaths) {
$viewPaths = $this->viewPath;
}
if (!is_array($viewPaths)) {
$viewPaths = [$viewPaths];
}
// Check the path for an extension
$ext = pathinfo($fileName, PATHINFO_EXTENSION);
if (!empty($ext)) {
if (!in_array($ext, $allowedExtensions)) {
throw new SystemException("$ext is not a valid View extension");
}
// Remove the extension from the fileName
$fileName = substr($fileName, 0, strrpos($fileName, '.'));
}
// Check if this a path relative to the view paths
foreach ($viewPaths as $path) {
$absolutePath = File::symbolizePath($path);
foreach ($allowedExtensions as $ext) {
$viewPath = $absolutePath . DIRECTORY_SEPARATOR . $fileName . ".$ext";
if (File::isFile($viewPath)) {
return $viewPath;
}
}
}
// Next, check if this is a local path reference
$absolutePath = File::symbolizePath($fileName);
foreach ($allowedExtensions as $ext) {
$viewPath = $absolutePath . ".$ext";
if (
File::isLocalPath($viewPath)
|| (
!Config::get('cms.restrictBaseDir', true)
&& realpath($viewPath) !== false
)
) {
return $viewPath;
}
}
return $input;
}
/**
* Includes a file path using output buffering, making the provided vars available.
*/
public function makeFileContents(string $filePath, array $extraParams = []): string
{
if (!strlen($filePath) ||
!File::isFile($filePath) ||
(!File::isLocalPath($filePath) && Config::get('cms.restrictBaseDir', true))
) {
return '';
}
if (!is_array($extraParams)) {
$extraParams = [];
}
$vars = array_merge($this->vars, $extraParams);
$obLevel = ob_get_level();
ob_start();
extract($vars);
// We'll evaluate the contents of the view inside a try/catch block so we can
// flush out any stray output that might get out before an error occurs or
// an exception is thrown. This prevents any partial views from leaking.
try {
include $filePath;
}
catch (Throwable $e) {
$this->handleViewException($e, $obLevel);
}
return ob_get_clean();
}
/**
* Handle a view exception.
*/
protected function handleViewException(Throwable $e, int $obLevel): void
{
while (ob_get_level() > $obLevel) {
ob_end_clean();
}
throw $e;
}
/**
* Guess the package path for the called class.
* @param string $suffix An extra path to attach to the end
* @param bool $isPublic Returns public path instead of an absolute one
*/
public function guessViewPath(string $suffix = '', bool $isPublic = false): ?string
{
$class = get_called_class();
return $this->guessViewPathFrom($class, $suffix, $isPublic);
}
/**
* Guess the package path from a specified class.
* @param string $class Class to guess path from.
* @param string $suffix An extra path to attach to the end
* @param bool $isPublic Returns public path instead of an absolute one
*/
public function guessViewPathFrom(string $class, string $suffix = '', bool $isPublic = false): ?string
{
$classFolder = strtolower(class_basename($class));
$classFile = realpath(dirname(File::fromClass($class)));
$guessedPath = $classFile ? $classFile . DIRECTORY_SEPARATOR . $classFolder . $suffix : null;
return $isPublic ? File::localToPublic($guessedPath) : $guessedPath;
}
}