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,262 @@
<?php namespace System\Models;
use Exception;
use Illuminate\Support\Facades\App;
use Throwable;
use ReflectionClass;
use Winter\Storm\Database\Model;
use Winter\Storm\Support\Str;
/**
* Model for logging system errors and debug trace messages
*
* @package winter\wn-system-module
* @author Alexey Bobkov, Samuel Georges
*/
class EventLog extends Model
{
protected const EXCEPTION_LOG_VERSION = 2;
protected const EXCEPTION_SNIPPET_LINES = 12;
/**
* @var string The database table used by the model.
*/
protected $table = 'system_event_logs';
/**
* @var array List of attribute names which are json encoded and decoded from the database.
*/
protected $jsonable = ['details'];
/**
* Returns true if this logger should be used.
*/
public static function useLogging(): bool
{
return (
!defined('WINTER_NO_EVENT_LOGGING') &&
class_exists('Model') &&
Model::getConnectionResolver() &&
static::hasDatabaseTable() &&
LogSetting::get('log_events')
);
}
/**
* Creates a log record
*/
public static function add(string $message, string $level = 'info', ?array $details = null): static
{
$record = new static;
$record->message = $message;
$record->level = $level;
if ($details !== null) {
$record->details = (array) $details;
}
try {
$record->save();
}
catch (Exception $ex) {
}
return $record;
}
/**
* Creates an exception log record
*/
public static function addException(Throwable $throwable, string $level = 'error'): static
{
$record = new static;
$record->message = $throwable->getMessage();
$record->level = $level;
$record->details = $record->getDetails($throwable);
try {
$record->save();
}
catch (Exception $ex) {
}
return $record;
}
/**
* Beautify level value.
*/
public function getLevelAttribute(string $level): string
{
return ucfirst($level);
}
/**
* Creates a shorter version of the message attribute,
* extracts the exception message or limits by 100 characters.
*/
public function getSummaryAttribute(): string
{
if (preg_match("/with message '(.+)' in/", $this->message, $match)) {
return $match[1];
}
// Get first line of message
preg_match('/^([^\n\r]+)/m', $this->message, $matches);
return Str::limit($matches[1] ?? '', 500);
}
/**
* Constructs the details array for logging
*/
public function getDetails(Throwable $throwable): array
{
return [
'logVersion' => static::EXCEPTION_LOG_VERSION,
'exception' => $this->exceptionToArray($throwable),
'environment' => $this->getEnviromentInfo(),
];
}
/**
* Convert a throwable into an array of data for logging
*/
protected function exceptionToArray(Throwable $throwable): array
{
return [
'type' => $throwable::class,
'message' => $throwable->getMessage(),
'file' => $throwable->getFile(),
'line' => $throwable->getLine(),
'snippet' => $this->getSnippet($throwable->getFile(), $throwable->getLine()),
'trace' => $this->exceptionTraceToArray($throwable->getTrace()),
'stringTrace' => $throwable->getTraceAsString(),
'code' => $throwable->getCode(),
'previous' => $throwable->getPrevious()
? $this->exceptionToArray($throwable->getPrevious())
: null,
];
}
/**
* Generate an array trace with extra data not provided by the default trace
*
* @throws \ReflectionException
*/
protected function exceptionTraceToArray(array $trace): array
{
foreach ($trace as $index => $frame) {
if (!isset($frame['file']) && isset($frame['class'])) {
$ref = new ReflectionClass($frame['class']);
$frame['file'] = $ref->getFileName();
if (!isset($frame['line']) && isset($frame['function']) && !str_contains($frame['function'], '{')) {
foreach (file($frame['file']) as $line => $text) {
if (preg_match(sprintf('/function\s.*%s/', $frame['function']), $text)) {
$frame['line'] = $line + 1;
break;
}
}
}
}
$trace[$index] = [
'file' => $frame['file'] ?? null,
'line' => $frame['line'] ?? null,
'function' => $frame['function'] ?? null,
'class' => $frame['class'] ?? null,
'type' => $frame['type'] ?? null,
'snippet' => !empty($frame['file']) && !empty($frame['line'])
? $this->getSnippet($frame['file'], $frame['line'])
: '',
'in_app' => ($frame['file'] ?? null) ? $this->isInAppError($frame['file']) : false,
'arguments' => array_map(function ($arg) {
if (is_numeric($arg)) {
return $arg;
}
if (is_string($arg)) {
return "'$arg'";
}
if (is_null($arg)) {
return 'null';
}
if (is_bool($arg)) {
return $arg ? 'true' : 'false';
}
if (is_array($arg)) {
return 'Array';
}
if (is_object($arg)) {
return get_class($arg);
}
if (is_resource($arg)) {
return 'Resource';
}
}, $frame['args'] ?? []),
];
}
return $trace;
}
/**
* Get the code snippet referenced in a trace
*/
protected function getSnippet(string $file, int $line): array
{
if (str_contains($file, ': eval()\'d code')) {
return [];
}
$lines = file($file);
if (count($lines) < static::EXCEPTION_SNIPPET_LINES) {
return $lines;
}
return array_slice(
$lines,
$line - (static::EXCEPTION_SNIPPET_LINES / 2),
static::EXCEPTION_SNIPPET_LINES,
true
);
}
/**
* Get environment details to record with the exception
*/
protected function getEnviromentInfo(): array
{
if (app()->runningInConsole()) {
return [
'context' => 'CLI',
'testing' => app()->runningUnitTests(),
'env' => app()->environment(),
];
}
return [
'context' => 'Web',
'backend' => method_exists(app(), 'runningInBackend') ? app()->runningInBackend() : false,
'testing' => app()->runningUnitTests(),
'url' => app('url')->current(),
'method' => app('request')->method(),
'env' => app()->environment(),
'ip' => app('request')->ip(),
'userAgent' => app('request')->header('User-Agent'),
];
}
/**
* Helper to work out if a file should be considered "In App" or not
*/
protected function isInAppError(string $file): bool
{
if (basename($file) === 'index.php' || basename($file) === 'artisan') {
return false;
}
return !Str::startsWith($file, base_path('vendor')) && !Str::startsWith($file, base_path('modules'));
}
}

View File

@@ -0,0 +1,98 @@
<?php namespace System\Models;
use Url;
use Config;
use Storage;
use Winter\Storm\Database\Attach\File as FileBase;
use Backend\Controllers\Files;
/**
* File attachment model
*
* @package winter\wn-system-module
* @author Alexey Bobkov, Samuel Georges
*/
class File extends FileBase
{
/**
* @var string The database table used by the model.
*/
protected $table = 'system_files';
/**
* {@inheritDoc}
*/
public function getThumb($width, $height, $options = [])
{
$url = '';
$width = !empty($width) ? $width : 0;
$height = !empty($height) ? $height : 0;
if (!$this->isPublic() && class_exists(Files::class)) {
$options = $this->getDefaultThumbOptions($options);
// Ensure that the thumb exists first
parent::getThumb($width, $height, $options);
// Return the Files controller handler for the URL
$url = Files::getThumbUrl($this, $width, $height, $options);
} else {
$url = parent::getThumb($width, $height, $options);
}
return $url;
}
/**
* {@inheritDoc}
*/
public function getPath(?string $fileName = null): string
{
$url = '';
if (!$this->isPublic() && class_exists(Files::class)) {
$url = Files::getDownloadUrl($this);
} else {
$url = parent::getPath($fileName);
}
return $url;
}
/**
* Define the public address for the storage path.
*/
public function getPublicPath(): string
{
$uploadsPath = Config::get('cms.storage.uploads.path', '/storage/app/uploads');
if ($this->isPublic()) {
$uploadsPath .= '/public';
}
else {
$uploadsPath .= '/protected';
}
return Url::asset($uploadsPath) . '/';
}
/**
* Define the internal storage path.
*/
public function getStorageDirectory(): string
{
$uploadsFolder = Config::get('cms.storage.uploads.folder');
if ($this->isPublic()) {
return $uploadsFolder . '/public/';
}
return $uploadsFolder . '/protected/';
}
/**
* Returns the name of the storage disk the file is stored on
*/
public function getDiskName(): string
{
return Config::get('cms.storage.uploads.disk');
}
}

View File

@@ -0,0 +1,69 @@
<?php namespace System\Models;
use Model;
/**
* System log settings
*
* @package winter\wn-system-module
* @author Alexey Bobkov, Samuel Georges
*/
class LogSetting extends Model
{
use \Winter\Storm\Database\Traits\Validation;
/**
* @var array Behaviors implemented by this model.
*/
public $implement = [
\System\Behaviors\SettingsModel::class
];
/**
* @var string Unique code
*/
public $settingsCode = 'system_log_settings';
/**
* @var mixed Settings form field defitions
*/
public $settingsFields = 'fields.yaml';
/**
* Validation rules
*/
public $rules = [];
public static function filterSettingItems($manager)
{
if (!self::isConfigured()) {
$manager->removeSettingItem('Winter.System', 'request_logs');
$manager->removeSettingItem('Winter.Cms', 'theme_logs');
return;
}
if (!self::get('log_events')) {
$manager->removeSettingItem('Winter.System', 'event_logs');
}
if (!self::get('log_requests')) {
$manager->removeSettingItem('Winter.System', 'request_logs');
}
if (!self::get('log_theme')) {
$manager->removeSettingItem('Winter.Cms', 'theme_logs');
}
}
/**
* Initialize the seed data for this model. This only executes when the
* model is first created or reset to default.
* @return void
*/
public function initSettingsData()
{
$this->log_events = true;
$this->log_requests = false;
$this->log_theme = false;
}
}

View File

@@ -0,0 +1,179 @@
<?php namespace System\Models;
use App;
use Str;
use Model;
use Cache;
use Less_Parser;
use Exception;
use File as FileHelper;
use Winter\Storm\Parse\Assetic\Filter\LessImportResolver;
/**
* Mail brand settings
*
* @package winter\wn-system-module
* @author Alexey Bobkov, Samuel Georges
*/
class MailBrandSetting extends Model
{
use \System\Traits\ViewMaker;
use \Winter\Storm\Database\Traits\Validation;
/**
* @var array Behaviors implemented by this model.
*/
public $implement = [
\System\Behaviors\SettingsModel::class
];
/**
* @var string Unique code
*/
public $settingsCode = 'system_mail_brand_settings';
/**
* @var mixed Settings form field defitions
*/
public $settingsFields = 'fields.yaml';
/**
* @var string The key to store rendered CSS in the cache under
*/
public $cacheKey = 'system::mailbrand.custom_css';
const WHITE_COLOR = '#fff';
const BODY_BG = '#f5f8fa';
const PRIMARY_BG = '#d66829';
const POSITIVE_BG = '#52a838';
const NEGATIVE_BG = '#e01346';
const HEADER_COLOR = '#bbbfc3';
const HEADING_COLOR = '#2f3133';
const TEXT_COLOR = '#74787e';
const LINK_COLOR = '#2da7c7';
const FOOTER_COLOR = '#aeaeae';
const BORDER_COLOR = '#edeff2';
const PROMOTION_BORDER_COLOR = '#9ba2ab';
/**
* Validation rules
*/
public $rules = [
];
/**
* Initialize the seed data for this model. This only executes when the
* model is first created or reset to default.
* @return void
*/
public function initSettingsData()
{
$config = App::make('config');
$vars = static::getCssVars();
foreach ($vars as $var => $default) {
$this->{$var} = $config->get('brand.mail.'.Str::studly($var), $default);
}
}
public function afterSave()
{
$this->resetCache();
}
public function resetCache()
{
Cache::forget(self::instance()->cacheKey);
}
public static function renderCss()
{
$cacheKey = self::instance()->cacheKey;
if (Cache::has($cacheKey)) {
return Cache::get($cacheKey);
}
try {
$customCss = self::compileCss();
Cache::forever($cacheKey, $customCss);
}
catch (Exception $ex) {
$customCss = '/* ' . e($ex->getMessage()) . ' */';
}
return $customCss;
}
protected static function getCssVars()
{
$vars = [
'body_bg' => self::BODY_BG,
'content_bg' => self::WHITE_COLOR,
'content_inner_bg' => self::WHITE_COLOR,
'button_text_color' => self::WHITE_COLOR,
'button_primary_bg' => self::PRIMARY_BG,
'button_positive_bg' => self::POSITIVE_BG,
'button_negative_bg' => self::NEGATIVE_BG,
'header_color' => self::HEADER_COLOR,
'heading_color' => self::HEADING_COLOR,
'text_color' => self::TEXT_COLOR,
'link_color' => self::LINK_COLOR,
'footer_color' => self::FOOTER_COLOR,
'body_border_color' => self::BORDER_COLOR,
'subcopy_border_color' => self::BORDER_COLOR,
'table_border_color' => self::BORDER_COLOR,
'panel_bg' => self::BORDER_COLOR,
'promotion_bg' => self::WHITE_COLOR,
'promotion_border_color' => self::PROMOTION_BORDER_COLOR,
];
return $vars;
}
protected static function makeCssVars()
{
$vars = static::getCssVars();
$result = [];
foreach ($vars as $var => $default) {
// panel_bg -> panel-bg
$cssVar = str_replace('_', '-', $var);
$result[$cssVar] = self::get($var, $default);
}
return $result;
}
public static function compileCss()
{
$parser = new Less_Parser(['compress' => true]);
// Refuse every @import directive. The bundled custom.less ships no imports
// and the only user-controlled input here is CSS variable values via
// ModifyVars below — those values are concatenated into the LESS source
// by Less_Parser::serializeVars() with no escaping, so a malicious value
// like `red; @import (inline) "/etc/passwd"` would otherwise reach the
// parser as a real @import directive. See GHSA-58fp-mcx6-7qf9.
//
// Note: unlike BrandSetting/EditorSetting, this model deliberately does
// not strip_tags() its renderCss() output. User input flows in only via
// ModifyVars (CSS variable values), not as a raw CSS string, and the
// output is consumed by the mail rendering pipeline rather than rendered
// inline on a backend page — so the threat model strip_tags() guards
// against does not apply here. The @import injection vector that
// ModifyVars opens up is closed structurally by the SetImportDirs
// deny-all gate, not by strip_tags.
$parser->SetImportDirs(['' => LessImportResolver::makeResolver([], null)]);
$basePath = base_path('modules/system/models/mailbrandsetting');
$parser->ModifyVars(static::makeCssVars());
$parser->parse(FileHelper::get($basePath . '/custom.less'));
return $parser->getCss();
}
}

View File

@@ -0,0 +1,204 @@
<?php namespace System\Models;
use View;
use Model;
use System\Classes\MailManager;
use Winter\Storm\Mail\MailParser;
use ApplicationException;
use File as FileHelper;
/**
* Mail layout
*
* @package winter\wn-system-module
* @author Alexey Bobkov, Samuel Georges
*/
class MailLayout extends Model
{
use \Winter\Storm\Database\Traits\Validation;
/**
* @var string The database table used by the model.
*/
protected $table = 'system_mail_layouts';
/**
* @var array Guarded fields
*/
protected $guarded = [];
/**
* @var array Fillable fields
*/
protected $fillable = [];
/**
* @var array Validation rules
*/
public $rules = [
'code' => 'required|unique:system_mail_layouts',
'name' => 'required',
'content_html' => 'required',
];
/**
* @var array Options array
*/
protected $jsonable = [
'options'
];
public static $codeCache;
/**
* Fired before the model is deleted.
*
* @return void
* @throws ApplicationException if the template is locked
*/
public function beforeDelete()
{
if ($this->is_locked) {
throw new ApplicationException('Cannot delete this template because it is locked');
}
}
/**
* List MailLayouts codes keyed by ID.
*
* @return array
*/
public static function listCodes()
{
if (self::$codeCache !== null) {
return self::$codeCache;
}
return self::$codeCache = self::lists('id', 'code');
}
/**
* Return the ID of a MailLayout instance from a defined code.
*
* @param string $code
* @return string
*/
public static function getIdFromCode($code)
{
return array_get(self::listCodes(), $code);
}
/**
* Find a MailLayout instance by its code or create a new instance from the view file.
*
* @param string $code
* @return MailLayout
*/
public static function findOrMakeLayout($code)
{
$layout = self::whereCode($code)->first();
if (!$layout && View::exists($code)) {
$layout = new self;
$layout->code = $code;
$layout->fillFromView($code);
}
return $layout;
}
/**
* Loops over each mail layout and ensures the system has a layout,
* if the layout does not exist, it will create one.
*
* @return void
*/
public static function createLayouts()
{
$dbLayouts = self::lists('code', 'code');
$definitions = MailManager::instance()->listRegisteredLayouts();
foreach ($definitions as $code => $path) {
if (array_key_exists($code, $dbLayouts)) {
continue;
}
$layout = new static;
$layout->code = $code;
$layout->is_locked = true;
$layout->fillFromView($path);
$layout->save();
}
}
/**
* Fill model using a view file retrieved by code.
*
* @param string|null $code
* @return void
* @throws ApplicationException if a layout with the defined code is not registered.
*/
public function fillFromCode($code = null)
{
$definitions = MailManager::instance()->listRegisteredLayouts();
if ($code === null) {
$code = $this->code;
}
if (!$definition = array_get($definitions, $code)) {
throw new ApplicationException('Unable to find a registered layout with code: '.$code);
}
$this->fillFromView($definition);
}
/**
* Fill model using a view file retrieved by path.
*
* @param string $path
* @return void
*/
public function fillFromView($path)
{
$sections = self::getTemplateSections($path);
$css = '
@media only screen and (max-width: 600px) {
.inner-body {
width: 100% !important;
}
.footer {
width: 100% !important;
}
}
@media only screen and (max-width: 500px) {
.button {
width: 100% !important;
}
}
';
$this->name = array_get($sections, 'settings.name', '???');
$this->content_css = $css;
$this->content_html = array_get($sections, 'html');
$this->content_text = array_get($sections, 'text');
}
/**
* Get section array from a view file retrieved by code.
*
* @param string $code
* @return array|null
*/
protected static function getTemplateSections($code)
{
if (!View::exists($code)) {
return null;
}
$view = View::make($code);
return MailParser::parse(FileHelper::get($view->getPath()));
}
}

View File

@@ -0,0 +1,163 @@
<?php namespace System\Models;
use View;
use Model;
use System\Classes\MailManager;
use Winter\Storm\Mail\MailParser;
use ApplicationException;
use Exception;
use File as FileHelper;
/**
* Mail partial
*
* @package winter\wn-system-module
* @author Alexey Bobkov, Samuel Georges
*/
class MailPartial extends Model
{
use \Winter\Storm\Database\Traits\Validation;
/**
* @var string The database table used by the model.
*/
protected $table = 'system_mail_partials';
/**
* @var array Guarded fields
*/
protected $guarded = [];
/**
* @var array Fillable fields
*/
protected $fillable = [];
/**
* @var array Validation rules
*/
public $rules = [
'code' => 'required|unique:system_mail_partials',
'name' => 'required',
'content_html' => 'required',
];
/**
* Fired after the model has been fetched.
*
* @return void
*/
public function afterFetch()
{
if (!$this->is_custom) {
$this->fillFromCode();
}
}
/**
* Find a MailPartial instance by code or create a new instance from a view file.
*
* @param string $code
* @return MailTemplate
*/
public static function findOrMakePartial($code)
{
try {
if (!$template = self::whereCode($code)->first()) {
$template = new self;
$template->code = $code;
$template->fillFromCode($code);
}
return $template;
}
catch (Exception $ex) {
return null;
}
}
/**
* Loops over each mail layout and ensures the system has a layout,
* if the layout does not exist, it will create one.
*
* @return void
*/
public static function createPartials()
{
$partials = MailManager::instance()->listRegisteredPartials();
$dbPartials = self::lists('is_custom', 'code');
$newPartials = array_diff_key($partials, $dbPartials);
/*
* Clean up non-customized partials
*/
foreach ($dbPartials as $code => $isCustom) {
if ($isCustom) {
continue;
}
if (!array_key_exists($code, $partials)) {
self::whereCode($code)->delete();
}
}
foreach ($newPartials as $code => $path) {
$partial = new static;
$partial->code = $code;
$partial->is_custom = 0;
$partial->fillFromView($path);
$partial->save();
}
}
/**
* Fill model using a view file retrieved by code.
*
* @param string|null $code
* @return void
*/
public function fillFromCode($code = null)
{
$definitions = MailManager::instance()->listRegisteredPartials();
if ($code === null) {
$code = $this->code;
}
if (!$definition = array_get($definitions, $code)) {
throw new ApplicationException('Unable to find a registered partial with code: '.$code);
}
$this->fillFromView($definition);
}
/**
* Fill model using a view file retrieved by path.
*
* @param string $path
* @return void
*/
public function fillFromView($path)
{
$sections = self::getTemplateSections($path);
$this->name = array_get($sections, 'settings.name', '???');
$this->content_html = array_get($sections, 'html');
$this->content_text = array_get($sections, 'text');
}
/**
* Get section array from a view file retrieved by code.
*
* @param string $code
* @return array|null
*/
protected static function getTemplateSections($code)
{
if (!View::exists($code)) {
return null;
}
$view = View::make($code);
return MailParser::parse(FileHelper::get($view->getPath()));
}
}

View File

@@ -0,0 +1,147 @@
<?php namespace System\Models;
use App;
use Model;
/**
* Mail settings
*
* @package winter\wn-system-module
* @author Alexey Bobkov, Samuel Georges
*/
class MailSetting extends Model
{
use \Winter\Storm\Database\Traits\Validation;
const MODE_FAILOVER = 'failover';
const MODE_LOG = 'log';
const MODE_MAIL = 'mail';
const MODE_SENDMAIL = 'sendmail';
const MODE_SMTP = 'smtp';
/**
* @var array Behaviors implemented by this model.
*/
public $implement = [
\System\Behaviors\SettingsModel::class
];
/**
* @var string Unique code
*/
public $settingsCode = 'system_mail_settings';
/**
* @var mixed Settings form field defitions
*/
public $settingsFields = 'fields.yaml';
/*
* Validation rules
*/
public $rules = [
'failover_mailers' => 'required_if:send_mode,'.self::MODE_FAILOVER,
'sender_name' => 'required',
'sender_email' => 'required|email'
];
/**
* Initialize the seed data for this model. This only executes when the
* model is first created or reset to default.
* @return void
*/
public function initSettingsData()
{
$config = App::make('config');
$mailers = $config->get('mail.mailers', [
'sendmail' => ['path' => $config->get('mail.sendmail', '/usr/sbin/sendmail')],
'smtp' => [
'host' => $config->get('mail.host'),
'port' => $config->get('mail.port', 587),
'username' => $config->get('mail.username'),
'password' => $config->get('mail.password'),
],
]);
$this->send_mode = $config->get('mail.default', static::MODE_MAIL);
$this->sender_name = $config->get('mail.from.name', 'Your Site');
$this->sender_email = $config->get('mail.from.address', 'admin@example.com');
$this->sendmail_path = array_get($mailers['sendmail'], 'path', '/usr/sbin/sendmail');
$this->smtp_address = array_get($mailers['smtp'], 'host');
$this->smtp_port = array_get($mailers['smtp'], 'port', 587);
$this->smtp_user = array_get($mailers['smtp'], 'username');
$this->smtp_password = array_get($mailers['smtp'], 'password');
$this->smtp_authorization = !!strlen($this->smtp_user);
$this->failover_mailers = implode(',', $config->get('mail.mailers.failover.mailers', []));
}
public function getFailoverMailersOptions()
{
return collect(App::make('config')->get('mail.mailers'))->except('failover')->keys()->all();
}
public function getSendModeOptions()
{
return [
static::MODE_FAILOVER => 'system::lang.mail.failover',
static::MODE_LOG => 'system::lang.mail.log_file',
static::MODE_MAIL => 'system::lang.mail.php_mail',
static::MODE_SENDMAIL => 'system::lang.mail.sendmail',
static::MODE_SMTP => 'system::lang.mail.smtp',
];
}
public static function applyConfigValues()
{
$config = App::make('config');
$settings = self::instance();
$config->set('mail.default', $settings->send_mode);
$config->set('mail.from.name', $settings->sender_name);
$config->set('mail.from.address', $settings->sender_email);
switch ($settings->send_mode) {
case self::MODE_FAILOVER:
$config->set('mail.mailers.failover.mailers', explode(',', $settings->failover_mailers));
break;
case self::MODE_SMTP:
$config->set('mail.mailers.smtp.host', $settings->smtp_address);
$config->set('mail.mailers.smtp.port', $settings->smtp_port);
$config->set('mail.mailers.smtp.encryption', $settings->smtp_port === 465 ? 'tls' : null);
if ($settings->smtp_authorization) {
$config->set('mail.mailers.smtp.username', $settings->smtp_user);
$config->set('mail.mailers.smtp.password', $settings->smtp_password);
}
else {
$config->set('mail.mailers.smtp.username', null);
$config->set('mail.mailers.smtp.password', null);
}
break;
case self::MODE_SENDMAIL:
$config->set('mail.mailers.sendmail.path', $settings->sendmail_path);
break;
}
}
/**
* Filter fields callback.
*
* We use this to show smtp credential fields only for smtp mode and when smtp authorization is required.
*
* @param array $fields
* @param string|null $context
* @return void
*/
public function filterFields($fields, $context = null)
{
$hideAuth = $fields->send_mode->value !== 'smtp' || !$fields->smtp_authorization->value;
if (isset($fields->smtp_user)) {
$fields->smtp_user->hidden = $hideAuth;
}
if (isset($fields->smtp_password)) {
$fields->smtp_password->hidden = $hideAuth;
}
}
}

View File

@@ -0,0 +1,218 @@
<?php namespace System\Models;
use View;
use Model;
use System\Classes\MailManager;
use Winter\Storm\Mail\MailParser;
use File as FileHelper;
/**
* Mail template
*
* @package winter\wn-system-module
* @author Alexey Bobkov, Samuel Georges
*/
class MailTemplate extends Model
{
use \Winter\Storm\Database\Traits\Validation;
/**
* @var string The database table used by the model.
*/
protected $table = 'system_mail_templates';
/**
* @var array Guarded fields
*/
protected $guarded = [];
/**
* @var array Fillable fields
*/
protected $fillable = [];
/**
* @var array Validation rules
*/
public $rules = [
'code' => 'required|unique:system_mail_templates',
'subject' => 'required',
'description' => 'required',
'content_html' => 'required',
];
public $belongsTo = [
'layout' => MailLayout::class
];
/**
* Returns an array of template codes and descriptions.
*
* @return array
*/
public static function listAllTemplates()
{
$fileTemplates = (array) MailManager::instance()->listRegisteredTemplates();
$dbTemplates = (array) self::lists('code', 'code');
$templates = $fileTemplates + $dbTemplates;
ksort($templates);
return $templates;
}
/**
* Returns a list of all mail templates.
*
* @return array Returns an array of the MailTemplate objects.
*/
public static function allTemplates()
{
$result = [];
$codes = array_keys(self::listAllTemplates());
foreach ($codes as $code) {
if (View::exists($code)) {
$result[] = self::findOrMakeTemplate($code);
}
}
return $result;
}
/**
* Syncronise all file templates to the database.
*
* @return void
*/
public static function syncAll()
{
MailLayout::createLayouts();
MailPartial::createPartials();
$templates = MailManager::instance()->listRegisteredTemplates();
$dbTemplates = self::lists('is_custom', 'code');
$newTemplates = array_diff_key($templates, $dbTemplates);
/*
* Clean up non-customized templates
*/
foreach ($dbTemplates as $code => $isCustom) {
if ($isCustom) {
continue;
}
if (!array_key_exists($code, $templates)) {
self::whereCode($code)->delete();
}
}
/*
* Create new templates
*/
foreach ($newTemplates as $code) {
$sections = self::getTemplateSections($code);
$layoutCode = array_get($sections, 'settings.layout', 'default');
$description = array_get($sections, 'settings.description');
$template = self::make();
$template->code = $code;
$template->description = $description;
$template->is_custom = 0;
$template->layout_id = MailLayout::getIdFromCode($layoutCode);
$template->forceSave();
}
}
/**
* Fired after the model has been fetched.
*
* @return void
*/
public function afterFetch()
{
if (!$this->is_custom) {
$this->fillFromView($this->code);
}
}
/**
* Fill model using provided content.
*
* @param string $content
* @return void
*/
public function fillFromContent($content)
{
$this->fillFromSections(MailParser::parse($content));
}
/**
* Fill model using a view file path.
*
* @param string $path
* @return void
*/
public function fillFromView($path)
{
$this->fillFromSections(self::getTemplateSections($path));
}
/**
* Fill model using provided section array.
*
* @param array $sections
* @return void
*/
protected function fillFromSections($sections)
{
$this->content_html = array_get($sections, 'html');
$this->content_text = array_get($sections, 'text');
$this->subject = array_get($sections, 'settings.subject', 'No subject');
$layoutCode = array_get($sections, 'settings.layout', 'default');
$this->layout = MailLayout::findOrMakeLayout($layoutCode);
}
/**
* Get section array from a view file retrieved by code.
*
* @param string $code
* @return array|null
*/
protected static function getTemplateSections($code)
{
if (!View::exists($code)) {
return null;
}
$view = View::make($code);
return MailParser::parse(FileHelper::get($view->getPath()));
}
/**
* Find a MailTemplate record by code or create one from a view file.
*
* @param string $code
* @return MailTemplate model
*/
public static function findOrMakeTemplate($code)
{
$template = self::whereCode($code)->first();
if (!$template && View::exists($code)) {
$template = new self;
$template->code = $code;
$template->fillFromView($code);
}
return $template;
}
/**
* @deprecated see System\Classes\MailManager::registerCallback
* Remove if year >= 2019
*/
public static function registerCallback(callable $callback)
{
traceLog('MailTemplate::registerCallback is deprecated, use ' . MailManager::class . '::registerCallback instead');
MailManager::instance()->registerCallback($callback);
}
}

View File

@@ -0,0 +1,164 @@
<?php namespace System\Models;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Winter\Storm\Database\Model;
/**
* Parameters model
* Used for storing internal application parameters.
*
* @package winter\wn-system-module
* @author Alexey Bobkov, Samuel Georges
*/
class Parameter extends Model
{
use \Winter\Storm\Support\Traits\KeyParser;
/**
* @var string The database table used by the model.
*/
protected $table = 'system_parameters';
public $timestamps = false;
protected static $cache = [];
/**
* @var array List of attribute names which are json encoded and decoded from the database.
*/
protected $jsonable = ['value'];
/**
* Clear the cache after saving.
*/
public function afterSave()
{
Cache::forget(implode('-', [$this->table, $this->namespace, $this->group, $this->item]));
}
/**
* Returns a setting value by the module (or plugin) name and setting name.
* @param string $key Specifies the setting key value, for example 'system:updates.check'
* @param mixed $default The default value to return if the setting doesn't exist in the DB.
* @return mixed Returns the setting value loaded from the database or the default value.
*/
public static function get($key, $default = null)
{
if (array_key_exists($key, static::$cache)) {
return static::$cache[$key];
}
$record = static::findRecord($key);
if (!$record) {
return static::$cache[$key] = $default;
}
return static::$cache[$key] = $record->value;
}
/**
* Stores a setting value to the database.
* @param string|array $key Specifies the setting key value, for example 'system:updates.check'
* @param mixed $value The setting value to store, serializable.
* @return true
*/
public static function set($key, $value = null)
{
if (is_array($key)) {
foreach ($key as $_key => $_value) {
static::set($_key, $_value);
}
return true;
}
$record = static::findRecord($key);
if (!$record) {
$record = new static;
list($namespace, $group, $item) = $record->parseKey($key);
$record->namespace = $namespace;
$record->group = $group;
$record->item = $item;
}
try {
$record->value = $value;
$record->save();
} catch (QueryException $ex) {
// SQLSTATE[42S02]: Base table or view not found - migrations haven't run yet
if ($ex->getCode() !== '42S02') {
Log::error($ex, ['skipDatabaseLog' => true]);
}
}
static::$cache[$key] = $value;
return true;
}
/**
* Resets a setting value by deleting the record.
* @param string $key Specifies the setting key value.
* @return bool
*/
public function reset($key)
{
$record = static::findRecord($key);
if (!$record) {
return false;
}
$record->delete();
unset(static::$cache[$key]);
return true;
}
/**
* Returns a record (cached)
*/
public static function findRecord($key): ?static
{
if (!App::hasDatabase()) {
return null;
}
$record = new static;
list($namespace, $group, $item) = $record->parseKey($key);
$result = null;
try {
$result = $record
->applyKey($key)
->remember(5, implode('-', [$record->getTable(), $namespace, $group, $item]))
->first();
} catch (QueryException $ex) {
// SQLSTATE[42S02]: Base table or view not found - migrations haven't run yet
if ($ex->getCode() !== '42S02') {
Log::error($ex, ['skipDatabaseLog' => true]);
}
}
return $result;
}
/**
* Scope to find a setting record for the specified module (or plugin) name and setting name.
* @param string $key Specifies the setting key value, for example 'system:updates.check'
* @param mixed $default The default value to return if the setting doesn't exist in the DB.
* @return QueryBuilder
*/
public function scopeApplyKey($query, $key)
{
list($namespace, $group, $item) = $this->parseKey($key);
$query = $query
->where('namespace', $namespace)
->where('group', $group)
->where('item', $item);
return $query;
}
}

View File

@@ -0,0 +1,168 @@
<?php namespace System\Models;
use Lang;
use Model;
use System\Classes\PluginManager;
/**
* Stores information about current plugin versions.
*
* @package winter\wn-system-module
* @author Alexey Bobkov, Samuel Georges
*/
class PluginVersion extends Model
{
public $table = 'system_plugin_versions';
/**
* @var array Guarded fields
*/
protected $guarded = ['*'];
/**
* @var bool Disable model timestamps.
*/
public $timestamps = false;
/**
* @var array Cache store for version information
*/
protected static $versionCache;
/**
* @var bool Plugin has been disabled by a missing dependency.
*/
public $disabledBySystem = false;
/**
* @var bool Plugin has been disabled by the user or configuration.
*/
public $disabledByConfig = false;
/**
* @var bool If true, plugin exists in the database but not the filesystem.
*/
public $orphaned = false;
/**
* @var string Plugin name, sourced from plugin details
*/
public $name;
/**
* @var string Plugin description, sourced from plugin details
*/
public $description;
/**
* @var string Plugin author, sourced from plugin details
*/
public $author;
/**
* @var string Plugin icon, sourced from plugin details
*/
public $icon;
/**
* @var string Plugin homepage, sourced from plugin details
*/
public $homepage;
/**
* The accessors to append to the model's array form.
* @var array
*/
protected $appends = ['slug'];
/**
* After the model is populated
*/
public function afterFetch()
{
/*
* Override the database columns with the plugin details
* found in the plugin registration file.
*/
$manager = PluginManager::instance();
$pluginObj = $manager->findByIdentifier($this->code);
if ($pluginObj) {
$pluginInfo = $pluginObj->pluginDetails();
foreach ($pluginInfo as $attribute => $info) {
if (property_exists($this, $attribute)) {
$this->{$attribute} = Lang::get($info);
}
}
$activeFlags = $manager->getPluginFlags($pluginObj);
if (!empty($activeFlags)) {
foreach ($activeFlags as $flag => $enabled) {
if (in_array($flag, [
PluginManager::DISABLED_MISSING,
PluginManager::DISABLED_REPLACED,
PluginManager::DISABLED_REPLACEMENT_FAILED,
PluginManager::DISABLED_MISSING_DEPENDENCIES,
])) {
$this->disabledBySystem = true;
}
if ($flag === PluginManager::DISABLED_BY_CONFIG) {
$this->disabledByConfig = true;
}
}
}
}
else {
$this->name = $this->code;
$this->description = Lang::get('system::lang.plugins.unknown_plugin');
$this->orphaned = true;
}
}
/**
* Returns true if the plugin should be updated by the system.
*/
public function getIsUpdatableAttribute(): bool
{
return !$this->is_disabled && !$this->disabledBySystem && !$this->disabledByConfig;
}
/**
* Only include enabled plugins
* @param $query
* @return QueryBuilder
*/
public function scopeApplyEnabled($query)
{
return $query->where('is_disabled', '!=', 1);
}
/**
* Returns the current version for a plugin
*/
public static function getVersion(string $pluginCode): ?string
{
if (self::$versionCache === null) {
self::$versionCache = self::lists('version', 'code');
}
return self::$versionCache[$pluginCode] ?? null;
}
/**
* Provides the slug attribute.
*/
public function getSlugAttribute(): string
{
return self::makeSlug($this->code);
}
/**
* Generates a slug for the plugin.
*/
public static function makeSlug(string $code): string
{
return strtolower(str_replace('.', '-', $code));
}
}

View File

@@ -0,0 +1,65 @@
<?php namespace System\Models;
use App;
use Model;
use Request;
/**
* Model for logging 404 errors
*
* @package winter\wn-system-module
* @author Alexey Bobkov, Samuel Georges
*/
class RequestLog extends Model
{
/**
* @var string The database table used by the model.
*/
protected $table = 'system_request_logs';
/**
* @var array The attributes that aren't mass assignable.
*/
protected $guarded = [];
/**
* @var array List of attribute names which are json encoded and decoded from the database.
*/
protected $jsonable = ['referer'];
/**
* Creates a log record
* @return self
*/
public static function add($statusCode = 404)
{
if (!App::hasDatabase()) {
return;
}
if (!LogSetting::get('log_requests')) {
return;
}
$record = static::firstOrNew([
'url' => substr(Request::fullUrl(), 0, 191),
'status_code' => $statusCode,
]);
if ($referer = Request::header('referer')) {
$referers = (array) $record->referer ?: [];
$referers[] = $referer;
$record->referer = $referers;
}
if (!$record->exists) {
$record->count = 1;
$record->save();
}
else {
$record->increment('count');
}
return $record;
}
}

View File

@@ -0,0 +1,17 @@
<?php namespace System\Models;
use Winter\Storm\Database\Models\Revision as RevisionBase;
/**
* Revision history model
*
* @package winter\wn-system-module
* @author Alexey Bobkov, Samuel Georges
*/
class Revision extends RevisionBase
{
/**
* @var string The database table used by the model.
*/
public $table = 'system_revisions';
}

View File

@@ -0,0 +1,22 @@
# ===================================
# Column Definitions
# ===================================
columns:
id:
label: system::lang.event_log.id
searchable: yes
width: 75px
created_at:
label: system::lang.event_log.created_at
searchable: yes
width: 160px
type: timetense
message:
label: system::lang.event_log.message
searchable: yes
type: partial
path: message_column
cssClass: column-break-word

View File

@@ -0,0 +1,15 @@
# ===================================
# Field Definitions
# ===================================
fields:
message:
type: partial
path: field_message
containerAttributes:
data-plugin: exception-beautifier
details:
type: partial
path: field_details

View File

@@ -0,0 +1,14 @@
# ===================================
# Field Definitions
# ===================================
fields:
title:
type: text
placeholder: backend::lang.fileupload.title_label
span: full
description:
type: textarea
placeholder: backend::lang.fileupload.description_label
span: full
size: tiny

View File

@@ -0,0 +1,25 @@
# ===================================
# Field Definitions
# ===================================
tabs:
defaultTab: system::lang.log.default_tab
fields:
log_requests:
label: system::lang.log.log_requests
span: auto
type: switch
comment: system::lang.log.log_requests_comment
log_theme:
label: system::lang.log.log_theme
span: auto
type: switch
comment: system::lang.log.log_theme_comment
log_events:
label: system::lang.log.log_events
span: auto
type: switch
comment: system::lang.log.log_events_comment

View File

@@ -0,0 +1,304 @@
/* Base */
body, body *:not(html):not(style):not(br):not(tr):not(code) {
font-family: Avenir, Helvetica, sans-serif;
box-sizing: border-box;
}
body {
background-color: @body-bg;
color: @text-color;
height: 100%;
hyphens: auto;
line-height: 1.4;
margin: 0;
-moz-hyphens: auto;
-ms-word-break: break-word;
width: 100% !important;
-webkit-hyphens: auto;
-webkit-text-size-adjust: none;
word-break: break-word;
}
p,
ul,
ol,
blockquote {
line-height: 1.4;
text-align: left;
}
a {
color: @link-color;
}
a img {
border: none;
}
/* Typography */
h1 {
color: @heading-color;
font-size: 19px;
font-weight: bold;
margin-top: 0;
text-align: left;
}
h2 {
color: @heading-color;
font-size: 16px;
font-weight: bold;
margin-top: 0;
text-align: left;
}
h3 {
color: @heading-color;
font-size: 14px;
font-weight: bold;
margin-top: 0;
text-align: left;
}
p {
color: @text-color;
font-size: 16px;
line-height: 1.5em;
margin-top: 0;
text-align: left;
}
code {
color: @text-color;
font-size: 16px;
line-height: 1.5em;
word-break: break-all;
}
p.sub {
font-size: 12px;
}
img {
max-width: 100%;
}
.break-all, .break-all * {
word-break: break-all;
}
/* Layout */
.wrapper {
background-color: @body-bg;
margin: 0;
padding: 0;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
.content {
margin: 0;
padding: 0;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
/* Header */
.header {
padding: 25px 0;
text-align: center;
}
.header a, .header span {
color: @header-color;
font-size: 19px;
font-weight: bold;
text-decoration: none;
}
/* Body */
.body {
background-color: @content-bg;
border-bottom: 1px solid @body-border-color;
border-top: 1px solid @body-border-color;
margin: 0;
padding: 0;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
.inner-body {
background-color: @content-inner-bg;
margin: 0 auto;
padding: 0;
width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 570px;
}
/* Subcopy */
.subcopy {
border-top: 1px solid @subcopy-border-color;
margin-top: 25px;
padding-top: 25px;
}
.subcopy p {
font-size: 12px;
}
/* Footer */
.footer {
margin: 0 auto;
padding: 0;
text-align: center;
width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 570px;
}
.footer p {
color: @footer-color;
font-size: 12px;
text-align: center;
}
/* Tables */
.table table {
border-collapse: collapse;
margin: 30px auto;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
.table th {
border-bottom: 1px solid @table-border-color;
padding-bottom: 8px;
}
.table td {
color: @text-color;
font-size: 15px;
line-height: 18px;
padding: 10px 0;
}
.content-cell {
padding: 35px;
}
.wrapper.layout-system .content-cell {
padding: 35px 0;
}
/* Buttons */
.action {
margin: 30px auto;
padding: 0;
text-align: center;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
.button {
border-radius: 3px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
color: @button-text-color;
display: inline-block;
text-decoration: none;
-webkit-text-size-adjust: none;
}
.button-primary {
background-color: @button-primary-bg;
border-top: 10px solid @button-primary-bg;
border-right: 18px solid @button-primary-bg;
border-bottom: 10px solid @button-primary-bg;
border-left: 18px solid @button-primary-bg;
}
.button-positive {
background-color: @button-positive-bg;
border-top: 10px solid @button-positive-bg;
border-right: 18px solid @button-positive-bg;
border-bottom: 10px solid @button-positive-bg;
border-left: 18px solid @button-positive-bg;
}
.button-negative {
background-color: @button-negative-bg;
border-top: 10px solid @button-negative-bg;
border-right: 18px solid @button-negative-bg;
border-bottom: 10px solid @button-negative-bg;
border-left: 18px solid @button-negative-bg;
}
/* Panels */
.panel {
margin: 0 0 21px;
}
.panel-content {
background-color: @panel-bg;
padding: 16px;
}
.panel-item {
padding: 0;
}
.panel-item p:last-of-type {
margin-bottom: 0;
padding-bottom: 0;
}
/* Promotions */
.promotion {
background-color: @promotion-bg;
border: 2px dashed @promotion-border-color;
margin: 0;
margin-bottom: 25px;
margin-top: 25px;
padding: 24px;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
.promotion h1 {
text-align: center;
}
.promotion p {
font-size: 15px;
text-align: center;
}
.promotion p:last-of-type {
margin-bottom: 0;
padding-bottom: 0;
}

View File

@@ -0,0 +1,122 @@
# ===================================
# Field Definitions
# ===================================
fields:
_mail_preview:
type: partial
path: field_mail_preview
secondaryTabs:
fields:
_section_background:
label: system::lang.mail_brand.fields._section_background
type: section
body_bg:
label: system::lang.mail_brand.fields.body_bg
type: colorpicker
availableColors: []
content_bg:
label: system::lang.mail_brand.fields.content_bg
type: colorpicker
availableColors: []
content_inner_bg:
label: system::lang.mail_brand.fields.content_inner_bg
type: colorpicker
availableColors: []
_section_buttons:
label: system::lang.mail_brand.fields._section_buttons
type: section
button_text_color:
label: system::lang.mail_brand.fields.button_text_color
type: colorpicker
availableColors: []
button_primary_bg:
label: system::lang.mail_brand.fields.button_primary_bg
type: colorpicker
availableColors: []
button_positive_bg:
label: system::lang.mail_brand.fields.button_positive_bg
type: colorpicker
availableColors: []
button_negative_bg:
label: system::lang.mail_brand.fields.button_negative_bg
type: colorpicker
availableColors: []
_section_type:
label: system::lang.mail_brand.fields._section_type
type: section
header_color:
label: system::lang.mail_brand.fields.header_color
type: colorpicker
availableColors: []
heading_color:
label: system::lang.mail_brand.fields.heading_color
type: colorpicker
availableColors: []
text_color:
label: system::lang.mail_brand.fields.text_color
type: colorpicker
availableColors: []
link_color:
label: system::lang.mail_brand.fields.link_color
type: colorpicker
availableColors: []
footer_color:
label: system::lang.mail_brand.fields.footer_color
type: colorpicker
availableColors: []
_section_borders:
label: system::lang.mail_brand.fields._section_borders
type: section
body_border_color:
label: system::lang.mail_brand.fields.body_border_color
type: colorpicker
availableColors: []
subcopy_border_color:
label: system::lang.mail_brand.fields.subcopy_border_color
type: colorpicker
availableColors: []
table_border_color:
label: system::lang.mail_brand.fields.table_border_color
type: colorpicker
availableColors: []
_section_components:
label: system::lang.mail_brand.fields._section_components
type: section
panel_bg:
label: system::lang.mail_brand.fields.panel_bg
type: colorpicker
availableColors: []
promotion_bg:
label: system::lang.mail_brand.fields.promotion_bg
type: colorpicker
availableColors: []
promotion_border_color:
label: system::lang.mail_brand.fields.promotion_border_color
type: colorpicker
availableColors: []

View File

@@ -0,0 +1,45 @@
# {{texts.heading}} 1
{{texts.paragraph|raw}}
## {{texts.heading}} 2
{% partial 'table' body %}
| {{texts.table.item}} | {{texts.table.description}} | {{texts.table.price}} |
|:------------- |:-------------:| --------:|
| {{texts.table.item}} 1 | {{texts.table.centered}} | $10 |
| {{texts.table.item}} 2 | {{texts.table.right_aligned}} | $20 |
{% endpartial %}
### {{texts.heading}} 3
{{texts.paragraph|raw}}
{% partial 'button' url='javascript:;' body %}
{{texts.buttons.primary}}
{% endpartial %}
{% partial 'button' type='positive' url='javascript:;' body %}
{{texts.buttons.positive}}
{% endpartial %}
{% partial 'button' type='negative' url='javascript:;' body %}
{{texts.buttons.negative}}
{% endpartial %}
{% partial 'panel' body %}
{{texts.panel}}
{% endpartial %}
{{texts.more}}
{% partial 'promotion' body %}
{{texts.promotion}}
{% endpartial %}
{{texts.thanks}},
{{ appName }}
{% partial 'subcopy' body %}
{{texts.subcopy}}
{% endpartial %}

View File

@@ -0,0 +1,13 @@
# ===================================
# Column Definitions
# ===================================
columns:
name:
label: system::lang.mail_templates.name
searchable: true
code:
label: system::lang.mail_templates.code
searchable: true

View File

@@ -0,0 +1,47 @@
# ===================================
# Field Definitions
# ===================================
fields:
code:
label: system::lang.mail_templates.code
comment: system::lang.mail_templates.code_comment
span: left
context: create
name@create:
label: system::lang.mail_templates.name
span: right
name@update:
label: system::lang.mail_templates.name
secondaryTabs:
fields:
content_html:
type: codeeditor
size: giant
tab: system::lang.mail_templates.content_html
language: html
stretch: true
content_css:
type: codeeditor
size: giant
tab: system::lang.mail_templates.content_css
language: css
stretch: true
content_text:
type: textarea
size: giant
tab: system::lang.mail_templates.content_text
stretch: true
options[disable_auto_inline_css]:
label: system::lang.mail_templates.disable_auto_inline_css
type: checkbox
tab: system::lang.mail_templates.options
default: false

View File

@@ -0,0 +1,13 @@
# ===================================
# Column Definitions
# ===================================
columns:
name:
label: system::lang.mail_templates.name
searchable: true
code:
label: system::lang.mail_templates.code
searchable: true

View File

@@ -0,0 +1,34 @@
# ===================================
# Field Definitions
# ===================================
fields:
code:
label: system::lang.mail_templates.code
comment: system::lang.mail_templates.code_comment
span: left
context: create
name@create:
label: system::lang.mail_templates.name
span: right
name@update:
label: system::lang.mail_templates.name
secondaryTabs:
fields:
content_html:
type: codeeditor
size: giant
tab: system::lang.mail_templates.content_html
language: html
stretch: true
content_text:
type: textarea
size: giant
tab: system::lang.mail_templates.content_text
stretch: true

View File

@@ -0,0 +1 @@
<?= trans('system::lang.mail.drivers_hint_content', ['url'=> 'https://wintercms.com/docs/services/mail#drivers']); ?>

View File

@@ -0,0 +1,9 @@
<button
type="button"
class="btn btn-success"
data-request="onTest"
data-request-data="redirect:0"
data-load-indicator="<?= e(trans('system::lang.mail_templates.sending')) ?>"
data-request-confirm="<?= e(trans('system::lang.settings.test_confirm', [ 'email' => e(BackendAuth::getUser()->email)])) ?>">
<?= e(trans('system::lang.mail_templates.test_send')) ?>
</button>

View File

@@ -0,0 +1,98 @@
# ===================================
# Field Definitions
# ===================================
tabs:
fields:
sender_name:
label: system::lang.mail.sender_name
span: auto
tab: system::lang.mail.general
sender_email:
label: system::lang.mail.sender_email
span: auto
type: email
tab: system::lang.mail.general
driver_hint:
type: hint
path: ~/modules/system/models/mailsetting/_drivers_hint.php
tab: system::lang.mail.general
send_mode:
label: system::lang.mail.method
type: balloon-selector
tab: system::lang.mail.general
smtp_address:
label: system::lang.mail.smtp_address
tab: system::lang.mail.general
span: left
trigger:
action: show
field: send_mode
condition: value[smtp]
smtp_port:
label: system::lang.mail.smtp_port
type: number
tab: system::lang.mail.general
span: auto
trigger:
action: show
field: send_mode
condition: value[smtp]
smtp_authorization:
type: checkbox
label: system::lang.mail.smtp_authorization
tab: system::lang.mail.general
comment: system::lang.mail.smtp_authorization_comment
trigger:
action: show
field: send_mode
condition: value[smtp]
smtp_user:
label: system::lang.mail.smtp_username
tab: system::lang.mail.general
span: left
dependsOn:
- send_mode
- smtp_authorization
smtp_password:
label: system::lang.mail.smtp_password
tab: system::lang.mail.general
type: sensitive
span: right
dependsOn:
- send_mode
- smtp_authorization
sendmail_path:
label: system::lang.mail.sendmail_path
commentAbove: system::lang.mail.sendmail_path_comment
tab: system::lang.mail.general
trigger:
action: show
field: send_mode
condition: value[sendmail]
failover_mailers:
label: system::lang.mail.failover_mailers
placeholder: system::lang.mail.failover_mailers_placeholer
type: taglist
options: getFailoverMailersOptions
customTags: false
tab: system::lang.mail.general
trigger:
action: show
field: send_mode
condition: value[failover]
_send_test:
type: partial
path: ~/modules/system/models/mailsetting/_send_test_button.php
tab: system::lang.mail.general

View File

@@ -0,0 +1,23 @@
# ===================================
# Column Definitions
# ===================================
columns:
code:
label: system::lang.mail_templates.code
searchable: true
subject:
label: system::lang.mail_templates.subject
searchable: true
description:
label: system::lang.mail_templates.description
searchable: true
layout:
label: system::lang.mail_templates.layout
relation: layout
select: name
sortable: false

View File

@@ -0,0 +1,45 @@
# ===================================
# Field Definitions
# ===================================
fields:
layout:
label: system::lang.mail_templates.layout
type: relation
emptyOption: system::lang.mail_templates.no_layout
code:
label: system::lang.mail_templates.code
comment: system::lang.mail_templates.code_comment
span: left
context: create
subject@create:
label: system::lang.mail_templates.subject
comment: system::lang.mail_templates.subject_comment
span: right
subject@update:
label: system::lang.mail_templates.subject
description:
label: system::lang.mail_templates.description
type: textarea
size: tiny
secondaryTabs:
fields:
content_html:
type: markdown
size: giant
tab: system::lang.mail_templates.content_html
safe: true
stretch: true
content_text:
type: textarea
size: giant
tab: system::lang.mail_templates.content_text
stretch: true

View File

@@ -0,0 +1,21 @@
# ===================================
# Column Definitions
# ===================================
columns:
name:
label: system::lang.updates.plugin_name
sortable: false
description:
label: system::lang.updates.plugin_description
sortable: false
version:
label: system::lang.updates.plugin_version
sortable: false
author:
label: system::lang.updates.plugin_author
sortable: false

View File

@@ -0,0 +1,27 @@
# ===================================
# Column Definitions
# ===================================
columns:
code:
label: system::lang.updates.plugin_code
sortable: false
type: partial
path: column_code
version:
label: system::lang.updates.plugin_version
sortable: false
is_unfrozen:
label: system::lang.plugins.unfrozen
type: partial
path: is_unfrozen
sortable: false
is_enabled:
label: system::lang.plugins.enabled
type: partial
path: is_enabled
sortable: false

View File

@@ -0,0 +1,18 @@
# ===================================
# Column Definitions
# ===================================
columns:
status_code:
label: system::lang.request_log.status_code
searchable: yes
width: 100px
url:
label: system::lang.request_log.url
searchable: yes
cssClass: column-break-word
count:
label: system::lang.request_log.count
width: 150px

View File

@@ -0,0 +1,13 @@
# ===================================
# Field Definitions
# ===================================
fields:
url:
label: system::lang.request_log.url
referer:
label: system::lang.request_log.referer
type: partial
path: referer_field