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,61 @@
<?php namespace Backend\Models;
use Model;
use Request;
/**
* Model for logging access to the back-end
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
class AccessLog extends Model
{
/**
* @var string The database table used by the model.
*/
protected $table = 'backend_access_log';
/**
* @var array Relations
*/
public $belongsTo = [
'user' => User::class
];
/**
* Creates a log record
* @param Backend\Models\User $user Admin user
* @return self
*/
public static function add($user)
{
$record = new static;
$record->user = $user;
$record->ip_address = Request::getClientIp();
$record->save();
return $record;
}
/**
* Returns a recent entry, latest entry is not considered recent
* if the creation day is the same as today.
* @return self
*/
public static function getRecent($user)
{
$records = static::where('user_id', $user->id)
->orderBy('created_at', 'desc')
->limit(2)
->get();
if (!count($records)) {
return null;
}
$first = $records->first();
return !$first->created_at->isToday() ? $first : $records->pop();
}
}

View File

@@ -0,0 +1,262 @@
<?php
namespace Backend\Models;
use Backend\Facades\Backend;
use Exception;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Lang;
use Less_Parser;
use Winter\Storm\Database\Model;
use Winter\Storm\Parse\Assetic\Filter\LessImportResolver;
use Winter\Storm\Support\Facades\Config;
use Winter\Storm\Support\Facades\File;
use Winter\Storm\Support\Facades\Url;
/**
* Brand settings that affect all users
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
* @author Winter CMS
*/
class BrandSetting 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 = 'backend_brand_settings';
/**
* @var mixed Settings form field definitions
*/
public $settingsFields = 'fields.yaml';
public $attachOne = [
'favicon' => \System\Models\File::class,
'logo' => \System\Models\File::class
];
/**
* @var string The key to store rendered CSS in the cache under
*/
public $cacheKey = 'backend::brand.custom_css';
const PRIMARY_COLOR = '#103141'; // Elephant
const SECONDARY_COLOR = '#2da7c7'; // Winter
const ACCENT_COLOR = '#6cc551'; // Shaded Green
const INLINE_MENU = 'inline';
const TILE_MENU = 'tile';
const COLLAPSE_MENU = 'collapse';
/**
* Validation rules
*/
public $rules = [
'app_name' => 'required',
'app_tagline' => 'required',
];
/**
* 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');
$this->app_name = $config->get('brand.appName', Lang::get('system::lang.app.name'));
$this->app_tagline = $config->get('brand.tagline', Lang::get('system::lang.app.tagline'));
$this->primary_color = $config->get('brand.primaryColor', self::PRIMARY_COLOR);
$this->secondary_color = $config->get('brand.secondaryColor', self::SECONDARY_COLOR);
$this->accent_color = $config->get('brand.accentColor', self::ACCENT_COLOR);
$this->default_colors = $config->get('brand.defaultColors', [
[
'color' => '#1abc9c',
],
[
'color' => '#16a085',
],
[
'color' => '#2ecc71',
],
[
'color' => '#27ae60',
],
[
'color' => '#3498db',
],
[
'color' => '#2980b9',
],
[
'color' => '#9b59b6',
],
[
'color' => '#8e44ad',
],
[
'color' => '#34495e',
],
[
'color' => '#2b3e50',
],
[
'color' => '#f1c40f',
],
[
'color' => '#f39c12',
],
[
'color' => '#e67e22',
],
[
'color' => '#d35400',
],
[
'color' => '#e74c3c',
],
[
'color' => '#c0392b',
],
[
'color' => '#ecf0f1',
],
[
'color' => '#bdc3c7',
],
[
'color' => '#95a5a6',
],
[
'color' => '#7f8c8d',
],
]);
$this->menu_mode = $config->get('brand.menuMode', self::INLINE_MENU);
// Attempt to load custom CSS
$brandCssPath = File::symbolizePath(Config::get('brand.customLessPath', ''));
if ($brandCssPath && File::exists($brandCssPath)) {
$this->custom_css = File::get($brandCssPath);
}
}
public function afterSave()
{
Cache::forget(self::instance()->cacheKey);
}
public static function getFavicon()
{
$settings = self::instance();
if ($settings->favicon) {
return $settings->favicon->getPath();
}
return self::getDefaultFavicon() ?: null;
}
public static function getLogo()
{
$settings = self::instance();
if ($settings->logo) {
return $settings->logo->getPath();
}
return self::getDefaultLogo() ?: null;
}
public static function renderCss()
{
$cacheKey = self::instance()->cacheKey;
if (Cache::has($cacheKey)) {
return strip_tags(Cache::get($cacheKey));
}
try {
$customCss = self::compileCss();
Cache::forever($cacheKey, $customCss);
} catch (Exception $ex) {
$customCss = '/* ' . e($ex->getMessage()) . ' */';
}
return strip_tags($customCss);
}
public static function compileCss()
{
$parser = new Less_Parser(['compress' => true]);
// Refuse every @import directive. The bundled custom.less ships no imports
// and the admin-supplied custom_css field has no legitimate use for them,
// so any @import here would be an attempt to disclose server files via the
// wikimedia/less.php raw-path fallback. See GHSA-58fp-mcx6-7qf9.
$parser->SetImportDirs(['' => LessImportResolver::makeResolver([], null)]);
$basePath = base_path('modules/backend/models/brandsetting');
$primaryColor = self::get('primary_color', self::PRIMARY_COLOR);
$secondaryColor = self::get('secondary_color', self::PRIMARY_COLOR);
$accentColor = self::get('accent_color', self::ACCENT_COLOR);
$parser->ModifyVars([
'logo-image' => "'".self::getLogo()."'",
'brand-primary' => $primaryColor,
'brand-secondary' => $secondaryColor,
'brand-accent' => $accentColor,
]);
$parser->parse(
File::get($basePath . '/custom.less') .
self::get('custom_css')
);
return $parser->getCss();
}
//
// Base line configuration
//
public static function isBaseConfigured()
{
return !!Config::get('brand');
}
public static function getDefaultFavicon()
{
$faviconPath = File::symbolizePath(Config::get('brand.faviconPath', ''));
if ($faviconPath && File::exists($faviconPath)) {
return Url::asset(File::localToPublic($faviconPath));
}
return Backend::skinAsset('assets/images/favicon.png');
}
public static function getDefaultLogo()
{
$logoPath = File::symbolizePath(Config::get('brand.logoPath', ''));
if ($logoPath && File::exists($logoPath)) {
return Url::asset(File::localToPublic($logoPath));
}
return null;
}
}

View File

@@ -0,0 +1,281 @@
<?php
namespace Backend\Models;
use Exception;
use Illuminate\Support\Facades\Cache;
use Less_Parser;
use Winter\Storm\Database\Model;
use Winter\Storm\Parse\Assetic\Filter\LessImportResolver;
use Winter\Storm\Support\Facades\File;
/**
* Editor settings that affect all users
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
class EditorSetting 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 = 'backend_editor_settings';
/**
* @var mixed Settings form field definitions
*/
public $settingsFields = 'fields.yaml';
/**
* @var string The key to store rendered CSS in the cache under
*/
public $cacheKey = 'backend::editor.custom_css';
protected $defaultHtmlAllowEmptyTags = 'textarea, a, iframe, object, video, style, script, .fa, .fr-emoticon, .fr-inner, path, line, hr, i';
protected $defaultHtmlAllowTags = 'a, abbr, address, area, article, aside, audio, b, bdi, bdo, blockquote, br, button, canvas, caption, cite, code, col, colgroup, datalist, dd, del, details, dfn, dialog, div, dl, dt, em, embed, fieldset, figcaption, figure, footer, form, h1, h2, h3, h4, h5, h6, header, hgroup, hr, i, iframe, img, input, ins, kbd, keygen, label, legend, li, link, main, map, mark, menu, menuitem, meter, nav, noscript, object, ol, optgroup, option, output, p, param, pre, progress, queue, rp, rt, ruby, s, samp, script, style, section, select, small, source, span, strike, strong, sub, summary, sup, table, tbody, td, textarea, tfoot, th, thead, time, title, tr, track, u, ul, var, video, wbr';
protected $defaultHtmlAllowAttributes = 'accept, accept-charset, accesskey, action, align, allowfullscreen, allowtransparency, alt, aria-.*, async, autocomplete, autofocus, autoplay, autosave, background, bgcolor, border, charset, cellpadding, cellspacing, checked, cite, class, color, cols, colspan, content, contenteditable, contextmenu, controls, coords, data, data-.*, datetime, default, defer, dir, dirname, disabled, download, draggable, dropzone, enctype, for, form, formaction, frameborder, headers, height, hidden, high, href, hreflang, http-equiv, icon, id, ismap, itemprop, keytype, kind, label, lang, language, list, loop, low, max, maxlength, media, method, min, mozallowfullscreen, multiple, muted, name, novalidate, open, optimum, pattern, ping, placeholder, playsinline, poster, preload, pubdate, radiogroup, readonly, rel, required, reversed, rows, rowspan, sandbox, scope, scoped, scrolling, seamless, selected, shape, size, sizes, span, src, srcdoc, srclang, srcset, start, step, summary, spellcheck, style, tabindex, target, title, type, translate, usemap, value, valign, webkitallowfullscreen, width, wrap';
protected $defaultHtmlNoWrapTags = 'figure, script, style';
protected $defaultHtmlRemoveTags = 'script, style, base';
protected $defaultHtmlLineBreakerTags = 'figure, table, hr, iframe, form, dl';
protected $defaultHtmlStyleImage = [
'oc-img-rounded' => 'Rounded',
'oc-img-bordered' => 'Bordered',
];
protected $defaultHtmlStyleLink = [
'oc-link-green' => 'Green',
'oc-link-strong' => 'Strong',
];
protected $defaultHtmlStyleParagraph = [
'oc-text-bordered' => 'Bordered',
'oc-text-gray' => 'Gray',
'oc-text-spaced' => 'Spaced',
'oc-text-uppercase' => 'Uppercase',
];
protected $defaultHtmlStyleTable = [
'oc-dashed-borders' => 'Dashed Borders',
'oc-alternate-rows' => 'Alternate Rows',
];
protected $defaultHtmlStyleTableCell = [
'oc-cell-highlighted' => 'Highlighted',
'oc-cell-thick-border' => 'Thick Border',
];
protected $defaultHtmlParagraphFormats = [
'N' => 'Normal',
'H1' => 'Heading 1',
'H2' => 'Heading 2',
'H3' => 'Heading 3',
'H4' => 'Heading 4',
'PRE' => 'Code',
];
/**
* Editor toolbar presets for Froala.
*/
protected $editorToolbarPresets = [
'default' => 'paragraphFormat, paragraphStyle, quote, bold, italic, align, formatOL, formatUL, insertTable,
insertLink, insertImage, insertVideo, insertAudio, insertFile, insertHR, html',
'minimal' => 'paragraphFormat, bold, italic, underline, |, insertLink, insertImage, |, html',
'full' => 'undo, redo, |, bold, italic, underline, |, paragraphFormat, paragraphStyle, inlineStyle, |,
strikeThrough, subscript, superscript, clearFormatting, |, fontFamily, fontSize, |, color,
emoticons, -, selectAll, |, align, formatOL, formatUL, outdent, indent, quote, |, insertHR,
insertLink, insertImage, insertVideo, insertAudio, insertFile, insertTable, |, selectAll,
html, fullscreen',
];
/**
* 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()
{
$this->html_allow_empty_tags = $this->defaultHtmlAllowEmptyTags;
$this->html_allow_tags = $this->defaultHtmlAllowTags;
$this->html_allow_attributes = $this->defaultHtmlAllowAttributes;
$this->html_no_wrap_tags = $this->defaultHtmlNoWrapTags;
$this->html_remove_tags = $this->defaultHtmlRemoveTags;
$this->html_line_breaker_tags = $this->defaultHtmlLineBreakerTags;
$this->html_custom_styles = File::get(base_path().'/modules/backend/models/editorsetting/default_styles.less');
$this->html_style_image = $this->makeStylesForTable($this->defaultHtmlStyleImage);
$this->html_style_link = $this->makeStylesForTable($this->defaultHtmlStyleLink);
$this->html_style_paragraph = $this->makeStylesForTable($this->defaultHtmlStyleParagraph);
$this->html_style_table = $this->makeStylesForTable($this->defaultHtmlStyleTable);
$this->html_style_table_cell = $this->makeStylesForTable($this->defaultHtmlStyleTableCell);
$this->html_paragraph_formats = $this->makeFormatsForTable($this->defaultHtmlParagraphFormats);
}
public function afterFetch()
{
if (!isset($this->value['html_paragraph_formats'])) {
$this->html_paragraph_formats = $this->makeFormatsForTable($this->defaultHtmlParagraphFormats);
$this->save();
}
}
public function afterSave()
{
Cache::forget(self::instance()->cacheKey);
}
protected function makeStylesForTable($arr)
{
$count = 0;
return array_build($arr, function ($key, $value) use (&$count) {
return [$count++, ['class_label' => $value, 'class_name' => $key]];
});
}
protected function makeFormatsForTable($arr)
{
$count = 0;
return array_build($arr, function ($key, $value) use (&$count) {
return [$count++, ['format_label' => $value, 'format_tag' => $key]];
});
}
/**
* Same as getConfigured but uses a special structure for styles.
* @return mixed
*/
public static function getConfiguredStyles($key, $default = null)
{
return static::getConfiguredArray($key, $default, function ($key, $value) {
if (array_has($value, ['class_name', 'class_label'])) {
return [
array_get($value, 'class_name'),
array_get($value, 'class_label')
];
}
});
}
/**
* Same as getConfigured but uses a special structure for paragraph formats.
* @return mixed
*/
public static function getConfiguredFormats($key, $default = null)
{
return static::getConfiguredArray($key, $default, function ($key, $value) {
if (array_has($value, ['format_tag', 'format_label'])) {
return [
array_get($value, 'format_tag'),
array_get($value, 'format_label')
];
}
});
}
protected static function getConfiguredArray($key, $default = null, $callback = null)
{
$instance = static::instance();
$value = $instance->get($key);
$defaultValue = $instance->getDefaultValue($key);
if (is_array($value) && is_callable($callback)) {
$value = array_filter(array_build($value, $callback));
}
return $value != $defaultValue ? $value : $default;
}
/**
* Returns the value only if it differs from the default value.
* @return mixed
*/
public static function getConfigured($key, $default = null)
{
$instance = static::instance();
$value = $instance->get($key);
$defaultValue = $instance->getDefaultValue($key);
return $value != $defaultValue ? $value : $default;
}
public function getDefaultValue($attribute)
{
$property = 'default'.studly_case($attribute);
return $this->$property;
}
/**
* Return the editor toolbar presets without line breaks.
* @return array
*/
public function getEditorToolbarPresets()
{
return array_map(function ($value) {
return preg_replace('/\s+/', ' ', $value);
}, $this->editorToolbarPresets);
}
public static function renderCss()
{
$cacheKey = self::instance()->cacheKey;
if (Cache::has($cacheKey)) {
return strip_tags(Cache::get($cacheKey));
}
try {
$customCss = self::compileCss();
Cache::forever($cacheKey, $customCss);
} catch (Exception $ex) {
$customCss = '/* ' . e($ex->getMessage()) . ' */';
}
return strip_tags($customCss);
}
public static function compileCss()
{
$parser = new Less_Parser(['compress' => true]);
// Refuse every @import directive. There is no bundled .less file to
// import here, and the admin-supplied html_custom_styles field has no
// legitimate use for @import. Without this gate, an @import (inline)
// directive in user CSS would disclose server files via the
// wikimedia/less.php raw-path fallback. See GHSA-58fp-mcx6-7qf9.
$parser->SetImportDirs(['' => LessImportResolver::makeResolver([], null)]);
$customStyles = '.fr-view {';
$customStyles .= self::get('html_custom_styles');
$customStyles .= '}';
$parser->parse($customStyles);
return $parser->getCss();
}
}

View File

@@ -0,0 +1,206 @@
<?php namespace Backend\Models;
use File;
use Lang;
use Model;
use Response;
use League\Csv\Writer as CsvWriter;
use League\Csv\EscapeFormula as CsvEscapeFormula;
use ApplicationException;
use SplTempFileObject;
/**
* Model used for exporting data
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
abstract class ExportModel extends Model
{
/**
* Called when data is being exported.
* The return value should be an array in the format of:
*
* [
* 'db_name1' => 'Some attribute value',
* 'db_name2' => 'Another attribute value'
* ],
* [...]
*
*/
abstract public function exportData($columns, $sessionKey = null);
/**
* Export data based on column names and labels.
* The $columns array should be in the format of:
*
* [
* 'db_name1' => 'Column label',
* 'db_name2' => 'Another label',
* ...
* ]
*
*/
public function export($columns, $options)
{
$sessionKey = array_get($options, 'sessionKey');
$data = $this->exportData(array_keys($columns), $sessionKey);
return $this->processExportData($columns, $data, $options);
}
/**
* Download a previously compiled export file.
* @return void
*/
public function download($name, $outputName = null)
{
if (!preg_match('/^oc[0-9a-z]*$/i', $name)) {
throw new ApplicationException(Lang::get('backend::lang.import_export.file_not_found_error'));
}
$csvPath = temp_path() . '/' . $name;
if (!file_exists($csvPath)) {
throw new ApplicationException(Lang::get('backend::lang.import_export.file_not_found_error'));
}
return Response::download($csvPath, $outputName, ['Content-Type' => 'text/csv'])->deleteFileAfterSend(true);
}
/**
* Converts a data collection to a CSV file.
*/
protected function processExportData($columns, $results, $options)
{
/*
* Validate
*/
if (!$results) {
throw new ApplicationException(Lang::get('backend::lang.import_export.empty_error'));
}
/*
* Parse options
*/
$defaultOptions = [
'firstRowTitles' => true,
'useOutput' => false,
'fileName' => 'export.csv',
'delimiter' => null,
'enclosure' => null,
'escape' => null
];
$options = array_merge($defaultOptions, $options);
$columns = $this->exportExtendColumns($columns);
/*
* Prepare CSV
*/
$csv = CsvWriter::createFromFileObject(new SplTempFileObject);
$csv->setOutputBOM(CsvWriter::BOM_UTF8);
if ($options['delimiter'] !== null) {
$csv->setDelimiter($options['delimiter']);
}
if ($options['enclosure'] !== null) {
$csv->setEnclosure($options['enclosure']);
}
if ($options['escape'] !== null) {
$csv->setEscape($options['escape']);
}
$csv->addFormatter(new CsvEscapeFormula());
/*
* Add headers
*/
if ($options['firstRowTitles']) {
$headers = $this->getColumnHeaders($columns);
$csv->insertOne($headers);
}
/*
* Add records
*/
foreach ($results as $result) {
$data = $this->matchDataToColumns($result, $columns);
$csv->insertOne($data);
}
/*
* Output
*/
if ($options['useOutput']) {
$csv->output($options['fileName']);
}
/*
* Save for download
*/
$csvName = uniqid('oc');
$csvPath = temp_path().'/'.$csvName;
$output = $csv->__toString();
File::put($csvPath, $output);
return $csvName;
}
/**
* Used to override column definitions at export time.
*/
protected function exportExtendColumns($columns)
{
return $columns;
}
/**
* Extracts the headers from the column definitions.
*/
protected function getColumnHeaders($columns)
{
$headers = [];
foreach ($columns as $column => $label) {
$headers[] = Lang::get($label);
}
return $headers;
}
/**
* Ensures the correct order of the column data.
*/
protected function matchDataToColumns($data, $columns)
{
$results = [];
foreach ($columns as $column => $label) {
$results[] = array_get($data, $column);
}
return $results;
}
/**
* Implodes a single dimension array using pipes (|)
* Multi dimensional arrays are not allowed.
* @return string
*/
protected function encodeArrayValue($data, $delimeter = '|')
{
$newData = [];
foreach ($data as $value) {
if (is_array($value)) {
$newData[] = 'Array';
} else {
$newData[] = str_replace($delimeter, '\\'.$delimeter, $value);
}
}
return implode($delimeter, $newData);
}
}

View File

@@ -0,0 +1,292 @@
<?php namespace Backend\Models;
use Backend\Behaviors\ImportExportController\TranscodeFilter;
use Str;
use Lang;
use Model;
use League\Csv\Reader as CsvReader;
use League\Csv\Statement as CsvStatement;
/**
* Model used for importing data
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
abstract class ImportModel extends Model
{
use \Winter\Storm\Database\Traits\Validation;
/**
* The attributes that aren't mass assignable.
* @var array
*/
protected $guarded = [];
/**
* Relations
*/
public $attachOne = [
'import_file' => [\System\Models\File::class, 'public' => false],
];
/**
* @var array Import statistics store.
*/
protected $resultStats = [
'updated' => 0,
'created' => 0,
'errors' => [],
'warnings' => [],
'skipped' => []
];
/**
* Called when data is being imported.
* The $results array should be in the format of:
*
* [
* 'db_name1' => 'Some value',
* 'db_name2' => 'Another value'
* ],
* [...]
*
*/
abstract public function importData($results, $sessionKey = null);
/**
* Import data based on column names matching header indexes in the CSV.
* The $matches array should be in the format of:
*
* [
* 0 => [db_name1, db_name2],
* 1 => [db_name3],
* ...
* ]
*
* The key (0, 1) is the column index in the CSV and the value
* is another array of target database column names.
*/
public function import($matches, $options = [])
{
$sessionKey = array_get($options, 'sessionKey');
$path = $this->getImportFilePath($sessionKey);
$data = $this->processImportData($path, $matches, $options);
return $this->importData($data, $sessionKey);
}
/**
* Converts column index to database column map to an array containing
* database column names and values pulled from the CSV file. Eg:
*
* [0 => [first_name], 1 => [last_name]]
*
* Will return:
*
* [first_name => Joe, last_name => Blogs],
* [first_name => Harry, last_name => Potter],
* [...]
*
* @return array
*/
protected function processImportData($filePath, $matches, $options)
{
/*
* Parse options
*/
$defaultOptions = [
'firstRowTitles' => true,
'delimiter' => null,
'enclosure' => null,
'escape' => null,
'encoding' => null
];
$options = array_merge($defaultOptions, $options);
/*
* Read CSV
*/
$reader = CsvReader::createFromPath($filePath, 'r');
if ($options['delimiter'] !== null) {
$reader->setDelimiter($options['delimiter']);
}
if ($options['enclosure'] !== null) {
$reader->setEnclosure($options['enclosure']);
}
if ($options['escape'] !== null) {
$reader->setEscape($options['escape']);
}
if (
$options['encoding'] !== null &&
$reader->supportsStreamFilter()
) {
$reader->addStreamFilter(sprintf(
'%s%s:%s',
TranscodeFilter::FILTER_NAME,
strtolower($options['encoding']),
'utf-8'
));
}
// Create reader statement
$stmt = (new CsvStatement)
->where(function (array $row) {
// Filter out empty rows
return count($row) > 1 || reset($row) !== null;
});
if ($options['firstRowTitles']) {
$stmt = $stmt->offset(1);
}
$result = [];
$contents = $stmt->process($reader);
foreach ($contents as $row) {
$result[] = $this->processImportRow($row, $matches);
}
return $result;
}
/**
* Converts a single row of CSV data to the column map.
* @return array
*/
protected function processImportRow($rowData, $matches)
{
$newRow = [];
foreach ($matches as $columnIndex => $dbNames) {
$value = array_get($rowData, $columnIndex);
foreach ((array) $dbNames as $dbName) {
$newRow[$dbName] = $value;
}
}
return $newRow;
}
/**
* Explodes a string using pipes (|) to a single dimension array
* @return array
*/
protected function decodeArrayValue($value, $delimeter = '|')
{
if (strpos($value, $delimeter) === false) {
return [$value];
}
$data = preg_split('~(?<!\\\)' . preg_quote($delimeter, '~') . '~', $value);
$newData = [];
foreach ($data as $_value) {
$newData[] = str_replace('\\'.$delimeter, $delimeter, $_value);
}
return $newData;
}
/**
* Returns an attached imported file local path, if available.
* @return string
*/
public function getImportFilePath($sessionKey = null)
{
$file = $this
->import_file()
->withDeferred($sessionKey)
->orderBy('id', 'desc')
->first()
;
if (!$file) {
return null;
}
return $file->getLocalPath();
}
/**
* Returns all available encodings values from the localization config
* @return array
*/
public function getFormatEncodingOptions()
{
$options = [
'utf-8',
'us-ascii',
'iso-8859-1',
'iso-8859-2',
'iso-8859-3',
'iso-8859-4',
'iso-8859-5',
'iso-8859-6',
'iso-8859-7',
'iso-8859-8',
'iso-8859-9',
'iso-8859-10',
'iso-8859-11',
'iso-8859-13',
'iso-8859-14',
'iso-8859-15',
'Windows-1250',
'Windows-1251',
'Windows-1252'
];
$translated = array_map(function ($option) {
return Lang::get('backend::lang.import_export.encodings.'.Str::slug($option, '_'));
}, $options);
return array_combine($options, $translated);
}
//
// Result logging
//
public function getResultStats()
{
$this->resultStats['errorCount'] = count($this->resultStats['errors']);
$this->resultStats['warningCount'] = count($this->resultStats['warnings']);
$this->resultStats['skippedCount'] = count($this->resultStats['skipped']);
$this->resultStats['hasMessages'] = (
$this->resultStats['errorCount'] > 0 ||
$this->resultStats['warningCount'] > 0 ||
$this->resultStats['skippedCount'] > 0
);
return (object) $this->resultStats;
}
protected function logUpdated()
{
$this->resultStats['updated']++;
}
protected function logCreated()
{
$this->resultStats['created']++;
}
protected function logError($rowIndex, $message)
{
$this->resultStats['errors'][$rowIndex] = $message;
}
protected function logWarning($rowIndex, $message)
{
$this->resultStats['warnings'][$rowIndex] = $message;
}
protected function logSkipped($rowIndex, $message)
{
$this->resultStats['skipped'][$rowIndex] = $message;
}
}

View File

@@ -0,0 +1,328 @@
<?php
namespace Backend\Models;
use Backend\Facades\BackendAuth;
use DateTime;
use DateTimeZone;
use DirectoryIterator;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Session;
use Winter\Storm\Database\Model;
use Winter\Storm\Support\Facades\Config;
/**
* Backend preferences for the backend user
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
class Preference extends Model
{
use \Winter\Storm\Database\Traits\Validation;
const DEFAULT_THEME = 'twilight.tmTheme';
/**
* @var array Behaviors implemented by this model.
*/
public $implement = [
\Backend\Behaviors\UserPreferencesModel::class
];
/**
* @var string Unique code
*/
public $settingsCode = 'backend::backend.preferences';
/**
* @var mixed Settings form field defitions
*/
public $settingsFields = 'fields.yaml';
/**
* @var array 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');
$this->locale = $config->get('app.locale', 'en');
$this->fallback_locale = $this->getFallbackLocale($this->locale);
$this->timezone = $config->get('cms.backendTimezone', $config->get('app.timezone'));
$this->editor_font_size = $config->get('editor.font_size', 12);
$this->editor_word_wrap = $config->get('editor.word_wrap', 'fluid');
$this->editor_code_folding = $config->get('editor.code_folding', 'manual');
// @deprecated v1.3.0
$this->editor_enable_folding = $config->get('editor.enable_folding', $config->get('editor.code_folding', 'manual') !== 'manual');
$this->editor_tab_size = $config->get('editor.tab_size', 4);
$this->editor_theme = $config->get('editor.theme', static::DEFAULT_THEME);
$this->editor_show_invisibles = $config->get('editor.show_invisibles', false);
$this->editor_highlight_active_line = $config->get('editor.highlight_active_line', true);
$this->editor_use_hard_tabs = $config->get('editor.use_hard_tabs', false);
$this->editor_show_gutter = $config->get('editor.show_gutter', true);
$this->editor_auto_closing = $config->get('editor.auto_closing', false);
$this->editor_autocompletion = $config->get('editor.editor_autocompletion', 'manual');
$this->editor_enable_snippets = $config->get('editor.enable_snippets', false);
$this->editor_display_indent_guides = $config->get('editor.display_indent_guides', false);
$this->editor_show_print_margin = $config->get('editor.show_print_margin', false);
$this->editor_show_minimap = $config->get('editor.show_minimap', true);
$this->editor_bracket_colors = $config->get('editor.bracket_colors', false);
$this->editor_show_colors = $config->get('editor.show_colors', true);
}
/**
* Set the application's locale based on the user preference.
* @return void
*/
public static function setAppLocale()
{
if (Session::has('locale')) {
App::setLocale(Session::get('locale'));
}
elseif (
($user = BackendAuth::getUser()) &&
($locale = static::get('locale'))
) {
Session::put('locale', $locale);
App::setLocale($locale);
}
}
/**
* Same as setAppLocale except for the fallback definition.
* @return void
*/
public static function setAppFallbackLocale()
{
if (Session::has('fallback_locale')) {
Lang::setFallback(Session::get('fallback_locale'));
}
elseif (
($user = BackendAuth::getUser()) &&
($locale = static::get('fallback_locale'))
) {
Session::put('fallback_locale', $locale);
Lang::setFallback($locale);
}
}
//
// Events
//
public function beforeValidate()
{
$this->fallback_locale = $this->getFallbackLocale($this->locale);
}
public function afterSave()
{
Session::put('locale', $this->locale);
Session::put('fallback_locale', $this->fallback_locale);
}
//
// Utils
//
/**
* Called when this model is reset to default by the user.
* @return void
*/
public function resetDefault()
{
parent::resetDefault();
Session::forget('locale');
Session::forget('fallback_locale');
}
/**
* Overrides the config with the user's preference.
* @return void
*/
public static function applyConfigValues()
{
$settings = self::instance();
Config::set('app.locale', $settings->locale);
Config::set('app.fallback_locale', $settings->fallback_locale);
}
//
// Getters
//
/**
* Attempt to extract the language from the locale,
* otherwise use the configuration.
* @return string
*/
protected function getFallbackLocale($locale)
{
if ($position = strpos($locale, '-')) {
$target = substr($locale, 0, $position);
$available = $this->getLocaleOptions();
if (isset($available[$target])) {
return $target;
}
}
return Config::get('app.fallback_locale');
}
/**
* Returns available options for the "locale" attribute.
* @return array
*/
public function getLocaleOptions()
{
$localeOptions = [
'ar' => [Lang::get('system::lang.locale.ar'), 'flag-sa'],
'be' => [Lang::get('system::lang.locale.be'), 'flag-by'],
'bg' => [Lang::get('system::lang.locale.bg'), 'flag-bg'],
'ca' => [Lang::get('system::lang.locale.ca'), 'flag-es-ct'],
'cs' => [Lang::get('system::lang.locale.cs'), 'flag-cz'],
'da' => [Lang::get('system::lang.locale.da'), 'flag-dk'],
'de' => [Lang::get('system::lang.locale.de'), 'flag-de'],
'el' => [Lang::get('system::lang.locale.el'), 'flag-gr'],
'en' => [Lang::get('system::lang.locale.en'), 'flag-us'],
'en-au' => [Lang::get('system::lang.locale.en-au'), 'flag-au'],
'en-ca' => [Lang::get('system::lang.locale.en-ca'), 'flag-ca'],
'en-gb' => [Lang::get('system::lang.locale.en-gb'), 'flag-gb'],
'es' => [Lang::get('system::lang.locale.es'), 'flag-es'],
'es-ar' => [Lang::get('system::lang.locale.es-ar'), 'flag-ar'],
'et' => [Lang::get('system::lang.locale.et'), 'flag-ee'],
'fa' => [Lang::get('system::lang.locale.fa'), 'flag-ir'],
'fi' => [Lang::get('system::lang.locale.fi'), 'flag-fi'],
'fr' => [Lang::get('system::lang.locale.fr'), 'flag-fr'],
'fr-ca' => [Lang::get('system::lang.locale.fr-ca'), 'flag-ca'],
'hu' => [Lang::get('system::lang.locale.hu'), 'flag-hu'],
'id' => [Lang::get('system::lang.locale.id'), 'flag-id'],
'it' => [Lang::get('system::lang.locale.it'), 'flag-it'],
'ja' => [Lang::get('system::lang.locale.ja'), 'flag-jp'],
'kr' => [Lang::get('system::lang.locale.kr'), 'flag-kr'],
'lt' => [Lang::get('system::lang.locale.lt'), 'flag-lt'],
'lv' => [Lang::get('system::lang.locale.lv'), 'flag-lv'],
'nb-no' => [Lang::get('system::lang.locale.nb-no'), 'flag-no'],
'nl' => [Lang::get('system::lang.locale.nl'), 'flag-nl'],
'pl' => [Lang::get('system::lang.locale.pl'), 'flag-pl'],
'pt-br' => [Lang::get('system::lang.locale.pt-br'), 'flag-br'],
'pt-pt' => [Lang::get('system::lang.locale.pt-pt'), 'flag-pt'],
'ro' => [Lang::get('system::lang.locale.ro'), 'flag-ro'],
'rs' => [Lang::get('system::lang.locale.rs'), 'flag-rs'],
'ru' => [Lang::get('system::lang.locale.ru'), 'flag-ru'],
'sk' => [Lang::get('system::lang.locale.sk'), 'flag-sk'],
'sl' => [Lang::get('system::lang.locale.sl'), 'flag-si'],
'sv' => [Lang::get('system::lang.locale.sv'), 'flag-se'],
'th' => [Lang::get('system::lang.locale.th'), 'flag-th'],
'tr' => [Lang::get('system::lang.locale.tr'), 'flag-tr'],
'uk' => [Lang::get('system::lang.locale.uk'), 'flag-ua'],
'vn' => [Lang::get('system::lang.locale.vn'), 'flag-vn'],
'zh-cn' => [Lang::get('system::lang.locale.zh-cn'), 'flag-cn'],
'zh-tw' => [Lang::get('system::lang.locale.zh-tw'), 'flag-tw'],
];
$locales = Config::get('app.localeOptions', $localeOptions);
// Sort locales alphabetically
asort($locales);
return $locales;
}
/**
* Returns all available timezone options.
* @return array
*/
public function getTimezoneOptions()
{
$timezoneIdentifiers = DateTimeZone::listIdentifiers();
$utcTime = new DateTime('now', new DateTimeZone('UTC'));
$tempTimezones = [];
foreach ($timezoneIdentifiers as $timezoneIdentifier) {
$currentTimezone = new DateTimeZone($timezoneIdentifier);
$tempTimezones[] = [
'offset' => (int) $currentTimezone->getOffset($utcTime),
'identifier' => $timezoneIdentifier
];
}
// Sort the array by offset, identifier ascending
usort($tempTimezones, function ($a, $b) {
return $a['offset'] === $b['offset']
? strcmp($a['identifier'], $b['identifier'])
: $a['offset'] - $b['offset'];
});
$timezoneList = [];
foreach ($tempTimezones as $tz) {
$sign = $tz['offset'] > 0 ? '+' : '-';
$offset = gmdate('H:i', abs($tz['offset']));
$timezoneList[$tz['identifier']] = '(UTC ' . $sign . $offset . ') ' . $tz['identifier'];
}
return $timezoneList;
}
/**
* Returns the theme options for the backend editor.
* Supports both legacy tmTheme (XML) and modern JSON theme formats.
*/
public function getEditorThemeOptions(): array
{
$themeDir = new DirectoryIterator('modules/backend/formwidgets/codeeditor/assets/themes/');
$themes = [];
// Iterate through the themes
foreach ($themeDir as $node) {
if (!$node->isFile()) {
continue;
}
$extension = $node->getExtension();
// Support both tmTheme (legacy) and JSON (modern) formats
if ($extension === 'tmTheme' || $extension === 'json') {
// Theme ID includes the file extension (e.g., "twilight.tmTheme", "one-dark-pro.json")
$themeId = $node->getBasename();
// Display name strips the extension for user-friendly presentation
$themeNameBase = $node->getBasename('.' . $extension);
$themeName = ucwords(str_replace(['_', '-'], ' ', $themeNameBase));
// Add the values to the themes array
if ($themeId != static::DEFAULT_THEME) {
$themes[$themeId] = $themeName;
}
}
}
// Sort the theme alphabetically, and push the default theme
asort($themes);
// Strip extension from default theme for display name
$defaultThemeName = ucwords(str_replace(['_', '-'], ' ', pathinfo(static::DEFAULT_THEME, PATHINFO_FILENAME)));
return [static::DEFAULT_THEME => $defaultThemeName] + $themes;
}
/**
* Returns the word wrap options for the backend editor.
*/
public function getEditorWordWrapOptions(): array
{
return [
'off' => Lang::get('backend::lang.editor.mode_off'),
'40' => Lang::get('backend::lang.editor.40_characters'),
'80' => Lang::get('backend::lang.editor.80_characters'),
'fluid' => Lang::get('backend::lang.editor.mode_fluid'),
];
}
}

View File

@@ -0,0 +1,456 @@
<?php namespace Backend\Models;
use Backend\Facades\Backend;
use Backend\Facades\BackendAuth;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Lang;
use Winter\Storm\Auth\AuthorizationException;
use Winter\Storm\Auth\Models\User as UserBase;
use Winter\Storm\Support\Facades\Mail;
/**
* Administrator user model
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
class User extends UserBase
{
use \Winter\Storm\Database\Traits\SoftDelete;
/**
* @var string The database table used by the model.
*/
protected $table = 'backend_users';
/**
* Validation rules
*/
public $rules = [
'email' => 'required|between:6,255|email|unique:backend_users',
'login' => 'required|between:2,255|unique:backend_users',
'password' => 'required:create|min:4|confirmed',
'password_confirmation' => 'required_with:password|min:4'
];
/**
* @var array Attributes that should be cast to dates
*/
protected $dates = [
'activated_at',
'last_login',
'created_at',
'updated_at',
'deleted_at',
];
/**
* Relations
*/
public $belongsToMany = [
'groups' => [UserGroup::class, 'table' => 'backend_users_groups', 'softDelete' => true]
];
public $belongsTo = [
'role' => UserRole::class
];
public $attachOne = [
'avatar' => \System\Models\File::class
];
public $hasMany = [
'throttle' => [UserThrottle::class, 'key' => 'user_id']
];
/**
* Purge attributes from data set.
*/
protected $purgeable = ['password_confirmation', 'send_invite'];
/**
* @var array List of attribute names which are json encoded and decoded from the database.
*/
protected $jsonable = ['permissions', 'metadata'];
/**
* @var string Login attribute
*/
public static $loginAttribute = 'login';
/**
* @var array<string> Relations on this model that require `backend.manage_users`
* to change on another user's record. Deliberately limited to the relations this
* model owns: authorization semantics for plugin-added relations belong to the
* plugin, which can append to this list or bind its own guard to the
* `model.relation.*` events.
*/
public array $permissionGuardedRelations = ['groups', 'avatar', 'throttle'];
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
// Guard relation writes at the point they actually happen: direct
// attach()/detach(), deferred binding commits and queued relation
// syncs all funnel through these relation events.
$guard = function (string $relationName) {
$this->authorizeRelationChange($relationName);
};
$this->bindEvent('model.relation.beforeAttach', $guard);
$this->bindEvent('model.relation.beforeDetach', $guard);
$this->bindEvent('model.relation.beforeAdd', $guard);
$this->bindEvent('model.relation.beforeRemove', $guard);
}
/**
* @return string Returns the user's full name.
*/
public function getFullNameAttribute()
{
return trim($this->first_name . ' ' . $this->last_name);
}
/**
* Gets a code for when the user is persisted to a cookie or session which identifies the user.
* @return string
*/
public function getPersistCode()
{
// Option A: @todo config
// return parent::getPersistCode();
// Option B:
if (!$this->persist_code) {
return parent::getPersistCode();
}
return $this->persist_code;
}
/**
* Returns the public image file path to this user's avatar.
*/
public function getAvatarThumb($size = 25, $options = null)
{
if (is_string($options)) {
$options = ['default' => $options];
}
elseif (!is_array($options)) {
$options = [];
}
// Default is "mm" (Mystery man)
$default = array_get($options, 'default', 'mm');
if ($this->avatar) {
return $this->avatar->getThumb($size, $size, $options);
}
return '//www.gravatar.com/avatar/' .
md5(strtolower(trim($this->email))) .
'?s='. $size .
'&d='. urlencode($default);
}
/**
* Determine whether the given user (or the currently authenticated user)
* is authorized to manage this user record.
*
* Returns true when no user is provided and no user is authenticated (CLI/queue),
* or when the user has `backend.manage_users` and (if this record is a superuser)
* the user is also a superuser.
*/
public function canBeManagedByUser(?User $user = null): bool
{
$user = $user ?? BackendAuth::getUser();
if (!$user) {
return true;
}
if (!$user->hasAccess('backend.manage_users')) {
return false;
}
if (!$user->isSuperUser() && ($this->is_superuser || $this->getOriginal('is_superuser'))) {
return false;
}
return true;
}
/**
* Before create event enforce authorization rules to prevent privilege escalation.
*/
public function beforeCreate()
{
$this->authorizeChange();
}
/**
* Before update event enforce authorization rules to prevent privilege escalation.
*
* Bound to update rather than save so that a save with nothing to write (e.g. a
* pivot form submission re-saving an untouched related record) requires no
* permission: the update event only fires when attributes have actually changed,
* after purgeable attributes have been stripped. Relation writes (group
* membership, avatar) are guarded separately by authorizeRelationChange().
*/
public function beforeUpdate()
{
$this->authorizeChange();
}
/**
* Enforce authorization rules for a change to this record's attributes.
*
* @throws AuthorizationException if the current user lacks permission
*/
protected function authorizeChange(): void
{
$actor = BackendAuth::getUser();
if (!$actor) {
return;
}
$isCurrentUser = $this->exists && $actor->getKey() === $this->getKey();
if ($isCurrentUser && $this->isDirty(['role_id', 'is_superuser', 'permissions'])) {
throw new AuthorizationException(Lang::get('backend::lang.user.self_escalation_denied'));
}
if (!$isCurrentUser && !$this->canBeManagedByUser($actor)) {
throw new AuthorizationException(Lang::get('backend::lang.user.cannot_manage_user'));
}
}
/**
* Enforce authorization rules for a change to one of this record's relations.
*
* Only the relations listed in $permissionGuardedRelations are guarded, so
* plugin-added relations (e.g. records that reference a user) behave normally.
*
* Changes to your own record's guarded relations are allowed: groups do not
* carry permissions out of the box, so they are not an escalation vector.
*
* @throws AuthorizationException if the current user lacks permission
*/
protected function authorizeRelationChange(string $relationName): void
{
if (!in_array($relationName, $this->permissionGuardedRelations)) {
return;
}
$actor = BackendAuth::getUser();
if (!$actor) {
return;
}
$isCurrentUser = $this->exists && $actor->getKey() === $this->getKey();
if (!$isCurrentUser && !$this->canBeManagedByUser($actor)) {
throw new AuthorizationException(Lang::get('backend::lang.user.cannot_manage_user'));
}
}
/**
* Before delete event enforce authorization rules.
*/
public function beforeDelete()
{
if (!$this->canBeManagedByUser()) {
throw new AuthorizationException(Lang::get('backend::lang.user.cannot_manage_user'));
}
}
/**
* Before restore event enforce authorization rules.
*/
public function beforeRestore()
{
if (!$this->canBeManagedByUser()) {
throw new AuthorizationException(Lang::get('backend::lang.user.cannot_manage_user'));
}
}
/**
* After create event
* @return void
*/
public function afterCreate()
{
$this->restorePurgedValues();
if ($this->send_invite) {
$this->sendInvitation();
}
}
/**
* After login event
* @return void
*/
public function afterLogin()
{
parent::afterLogin();
/**
* @event backend.user.login
* Provides an opportunity to interact with the Backend User model after the user has logged in
*
* Example usage:
*
* Event::listen('backend.user.login', function ((\Backend\Models\User) $user) {
* Flash::success(sprintf('Welcome %s!', $user->getFullNameAttribute()));
* });
*
*/
Event::fire('backend.user.login', [$this]);
}
/**
* Sends an invitation to the user using template "backend::mail.invite".
* @return void
*/
public function sendInvitation()
{
$data = [
'name' => $this->full_name,
'login' => $this->login,
'password' => $this->getOriginalHashValue('password'),
'link' => Backend::url('backend'),
];
Mail::send('backend::mail.invite', $data, function ($message) {
$message->to($this->email, $this->full_name);
});
}
public function getGroupsOptions()
{
$result = [];
foreach (UserGroup::all() as $group) {
$result[$group->id] = [$group->name, $group->description];
}
return $result;
}
public function getRoleOptions()
{
$result = [];
foreach (UserRole::all() as $role) {
$result[$role->id] = [$role->name, $role->description];
}
return $result;
}
/**
* Check if the user is suspended.
* @return bool
*/
public function isSuspended()
{
return BackendAuth::findThrottleByUserId($this->id)->checkSuspended();
}
/**
* Remove the suspension on this user.
*
* @throws AuthorizationException if the current user lacks permission
*/
public function unsuspend()
{
if (!$this->canBeManagedByUser()) {
throw new AuthorizationException(Lang::get('backend::lang.user.cannot_manage_user'));
}
BackendAuth::findThrottleByUserId($this->id)->unsuspend();
}
/**
* Get a reset password code for this user.
*
* When called by an authenticated user targeting a different account,
* the actor must have `backend.manage_users` permission.
* Self-service resets (Auth controller restore flow) are allowed.
*
* @throws AuthorizationException if the current user lacks permission
*/
public function getResetPasswordCode()
{
$actor = BackendAuth::getUser();
if ($actor && $actor->getKey() !== $this->getKey() && !$this->canBeManagedByUser($actor)) {
throw new AuthorizationException(Lang::get('backend::lang.user.cannot_manage_user'));
}
return parent::getResetPasswordCode();
}
//
// Impersonation
//
/**
* Returns an array of merged permissions based on the user's individual permissions
* and their role permissions filtering out any permissions the impersonator doesn't
* have access to (if the current user is being impersonated)
*
* @return array
*/
public function getMergedPermissions()
{
if (!$this->mergedPermissions) {
$permissions = parent::getMergedPermissions();
// If the user is being impersonated filter out any permissions the impersonator doesn't have access to already
if (BackendAuth::isImpersonator()) {
$impersonator = BackendAuth::getImpersonator();
if ($impersonator && $impersonator !== $this) {
foreach ($permissions as $permission => $status) {
if (!$impersonator->hasAccess($permission)) {
unset($permissions[$permission]);
}
}
$this->mergedPermissions = $permissions;
}
}
}
return $this->mergedPermissions;
}
/**
* Check if this user can be impersonated by the provided impersonator
* Super users cannot be impersonated and all users cannot be impersonated unless there is an impersonator
* present and the impersonator has access to `backend.impersonate_users`, and the impersonator is not the
* user being impersonated
*
* @param \Winter\Storm\Auth\Models\User|false $impersonator The user attempting to impersonate this user, false when not available
* @return boolean
*/
public function canBeImpersonated($impersonator = false)
{
if (
$this->isSuperUser() ||
!$impersonator ||
!($impersonator instanceof static) ||
!$impersonator->hasAccess('backend.impersonate_users') ||
$impersonator === $this
) {
return false;
}
// Clear the merged permissions before the impersonation starts
// so that they are correct even if they had been loaded prior
// to the impersonation starting
$this->mergedPermissions = null;
return true;
}
}

View File

@@ -0,0 +1,46 @@
<?php namespace Backend\Models;
use Winter\Storm\Auth\Models\Group as GroupBase;
/**
* Administrator group
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
class UserGroup extends GroupBase
{
const CODE_OWNERS = 'owners';
/**
* @var string The database table used by the model.
*/
protected $table = 'backend_user_groups';
/**
* @var array Validation rules
*/
public $rules = [
'name' => 'required|between:2,128|unique:backend_user_groups',
];
/**
* @var array Relations
*/
public $belongsToMany = [
'users' => [User::class, 'table' => 'backend_users_groups'],
'users_count' => [User::class, 'table' => 'backend_users_groups', 'count' => true]
];
public function afterCreate()
{
if ($this->is_new_user_default) {
$this->addAllUsersToGroup();
}
}
public function addAllUsersToGroup()
{
$this->users()->sync(User::lists('id'));
}
}

View File

@@ -0,0 +1,35 @@
<?php namespace Backend\Models;
use BackendAuth;
use SystemException;
use Winter\Storm\Auth\Models\Preferences as PreferencesBase;
/**
* All preferences for the backend user
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
class UserPreference extends PreferencesBase
{
/**
* @var string The database table used by the model.
*/
protected $table = 'backend_user_preferences';
public $timestamps = false;
protected static $cache = [];
/**
* Checks for a supplied user or uses the default logged in. You should override this method.
* @param mixed $user An optional back-end user object.
* @return User object
*/
public function resolveUser($user)
{
$user = BackendAuth::getUser();
return $user;
}
}

View File

@@ -0,0 +1,101 @@
<?php namespace Backend\Models;
use Backend\Classes\AuthManager;
use Winter\Storm\Auth\Models\Role as RoleBase;
/**
* Administrator role
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
class UserRole extends RoleBase
{
const CODE_DEVELOPER = 'developer';
const CODE_PUBLISHER = 'publisher';
/**
* @var string The database table used by the model.
*/
protected $table = 'backend_user_roles';
/**
* @var array Validation rules
*/
public $rules = [
'name' => 'required|between:2,128|unique',
'code' => 'unique',
];
/**
* @var array Relations
*/
public $hasMany = [
'users' => [User::class, 'key' => 'role_id'],
'users_count' => [User::class, 'key' => 'role_id', 'count' => true]
];
public function filterFields($fields)
{
// System roles cannot have their code or permissions changed
if ($this->isSystemRole()) {
$fields->code->disabled = true;
$fields->permissions->disabled = true;
}
}
public function afterFetch()
{
// System role permissions are determined by the permissions that attach
// themselves to the given role's code via the `roles` property.
if ($this->isSystemRole()) {
$this->permissions = $this->getDefaultPermissions();
}
}
public function beforeSave()
{
// System roles cannot have their code or permissions changed
if ($this->isSystemRole()) {
$this->is_system = true;
$this->permissions = [];
if ($this->exists) {
$this->code = $this->getOriginal('code');
}
}
}
public function isSystemRole()
{
// System roles must have a valid code property
if (!$this->code || !strlen(trim($this->code))) {
return false;
}
// Winter default system roles
if ($this->is_system || in_array($this->code, [
self::CODE_DEVELOPER,
self::CODE_PUBLISHER
])) {
return true;
}
// If any permission attaches itself to a given role's code
// that role is now considered a system role
return AuthManager::instance()->hasPermissionsForRole($this->code);
}
/**
* Get the permissions that have attached themselves to the current role
*/
public function getDefaultPermissions(): array
{
// Only the Develper role inherits all "orphaned" / unassigned permissions by default
$includeOrphanedPermissions = false;
if ($this->code === self::CODE_DEVELOPER) {
$includeOrphanedPermissions = true;
}
return AuthManager::instance()->listPermissionsForRole($this->code, $includeOrphanedPermissions);
}
}

View File

@@ -0,0 +1,36 @@
<?php namespace Backend\Models;
use Config;
use Winter\Storm\Auth\Models\Throttle as ThrottleBase;
/**
* Administrator throttling model
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
class UserThrottle extends ThrottleBase
{
/**
* @var string The database table used by the model.
*/
protected $table = 'backend_user_throttle';
/**
* @var array Relations
*/
public $belongsTo = [
'user' => User::class
];
/**
* @inheritDoc
*/
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
static::$attemptLimit = Config::get('auth.throttle.attemptLimit', 5);
static::$suspensionTime = Config::get('auth.throttle.suspensionTime', 15);
}
}

View File

@@ -0,0 +1,53 @@
# ===================================
# Column Definitions
# ===================================
columns:
id:
label: backend::lang.access_log.id
searchable: yes
invisible: true
width: 75px
created_at:
label: backend::lang.access_log.created_at
searchable: yes
type: timetense
width: 160px
type:
label: backend::lang.access_log.type
invisible: true
ip_address:
label: backend::lang.access_log.ip_address
searchable: yes
login:
label: backend::lang.access_log.login
relation: user
select: login
searchable: yes
sortable: false
first_name:
label: backend::lang.access_log.first_name
relation: user
select: first_name
searchable: yes
sortable: false
last_name:
label: backend::lang.access_log.last_name
relation: user
select: last_name
searchable: yes
sortable: false
email:
label: backend::lang.access_log.email
relation: user
select: email
searchable: yes
sortable: false

View File

@@ -0,0 +1,234 @@
//
// Coded variables
//
// @logo-image
// @brand-primary
// @brand-secondary
// @brand-accent
//
.br-p { color: @brand-primary; }
.br-s { color: @brand-secondary; }
.br-a { color: @brand-accent; }
.br-p-s10 { color: saturate(@brand-primary, 10%); }
.br-s-s10 { color: saturate(@brand-secondary, 10%); }
.br-a-s10 { color: saturate(@brand-accent, 10%); }
.br-p-s20 { color: saturate(@brand-primary, 20%); }
.br-s-s20 { color: saturate(@brand-secondary, 20%); }
.br-a-s20 { color: saturate(@brand-accent, 20%); }
.bg-p { background-color: @brand-primary; }
.bg-s { background-color: @brand-secondary; }
.bg-a { background-color: @brand-accent; }
.bg-p-s10 { background-color: saturate(@brand-primary, 10%); }
.bg-s-s10 { background-color: saturate(@brand-secondary, 10%); }
.bg-a-s10 { background-color: saturate(@brand-accent, 10%); }
.bg-p-s20 { background-color: saturate(@brand-primary, 20%); }
.bg-s-s20 { background-color: saturate(@brand-secondary, 20%); }
.bg-a-s20 { background-color: saturate(@brand-accent, 20%); }
@custom-dark-accent: mix(black, desaturate(@brand-accent, 35%), 20%);
@custom-dark-secondary: mix(black, saturate(@brand-secondary, 20%), 25%);
@custom-dark-primary: mix(black, saturate(@brand-primary, 5%), 15%);
//
// Sidenav Tree
//
.sidenav-tree ul.top-level > li {
> div.group:before {
border-top-color: @brand-primary;
}
> ul li.active {
border-color: @brand-secondary;
}
}
//
// Side panel
//
#layout-side-panel {
.sidepanel-content-header {
background: @custom-dark-secondary;
&::after {
border-top-color: @custom-dark-secondary;
}
}
}
//
// Asset List
//
.control-assetlist ul li.active a:after,
.control-assetlist ul li.active a.link:after {
background: @brand-secondary;
}
//
// Pages List
//
.control-treeview ol > li.active > div::after {
background: @brand-secondary;
}
//
// Outside Layout
//
body.outer {
background: @custom-dark-primary;
}
//
// Logos
//
.wn-logo-transparent when not (@logo-image = '') {
background-image: url('@{logo-image}') !important;
}
.wn-logo when not (@logo-image = '') {
background-image: url('@{logo-image}');
}
.oc-logo-transparent when not (@logo-image = '') {
background-image: url('@{logo-image}') !important;
}
.oc-logo when not (@logo-image = '') {
background-image: url('@{logo-image}');
}
//
// List
//
table.table.data {
tbody {
tr.active td {
&:first-child {
border-left: 3px solid @brand-secondary;
}
}
}
}
//
// Fancy Layout
//
.fancy-layout *:not(.nested-form):not(.modal-body) > .form-widget > .layout-row > .form-tabless-fields {
background: @brand-secondary;
.loading-indicator-container .loading-indicator {
background: @brand-secondary;
}
}
body.fancy-layout .master-tabs.control-tabs,
.master-tabs.control-tabs.fancy-layout {
> div > div.tabs-container > ul.nav-tabs > li.active a > span.title {
&, &:before, &:after {
background: @brand-secondary;
}
}
> div > div.tabs-container > ul.nav-tabs > li a > span.title {
&, &:before, &:after {
background-color: mix(black, saturate(@brand-secondary, 20%), 31%);
}
}
> div > div.tabs-container {
background: @custom-dark-secondary;
}
}
.fancy-layout *:not(.nested-form):not(.modal-body) > .form-widget > .layout-row > .control-tabs.primary-tabs,
*:not(.nested-form):not(.modal-body) > .form-widget > .layout-row > .control-tabs.fancy-layout.primary-tabs {
&.master-area > div > ul.nav-tabs {
background: @brand-secondary;
}
}
.fancy-layout *:not(.nested-form):not(.modal-body) > .form-widget > .layout-row > .control-tabs.secondary-tabs.secondary-content-tabs.primary-collapsed {
> div > ul.nav-tabs {
background: @brand-secondary;
}
}
.control-filelist ul li.active > a:after {
background: @brand-secondary;
}
//
// Component List
//
div.control-componentlist {
&.droppable {
background-color: lighten(@brand-secondary, 20%);
}
}
//
// Stripe Indicator
//
.stripe-loading-indicator {
.stripe, .stripe-loaded {
background: @brand-accent;
}
}
//
// Balloon Selector
//
.control-balloon-selector {
ul {
li.active {
background: @brand-secondary !important;
}
}
}
//
// Media
//
.nav.selector-group li.active {
border-left-color: @brand-secondary;
}
//
// Fancy breadcrumb
//
body.breadcrumb-fancy .control-breadcrumb,
.control-breadcrumb.breadcrumb-fancy {
background-color: mix(black, saturate(@brand-secondary, 20%), 16%);
li {
background-color: mix(black, saturate(@brand-secondary, 20%), 31%);
&:last-child {
background-color: mix(black, saturate(@brand-secondary, 20%), 16%);
&::before {
border-left-color: mix(black, saturate(@brand-secondary, 20%), 16%);
}
}
&::after {
border-left-color: mix(black, saturate(@brand-secondary, 20%), 31%);
}
&:not(:last-child)::before {
border-left-color: @brand-secondary;
}
}
}

View File

@@ -0,0 +1,112 @@
# ===================================
# Field Definitions
# ===================================
# Light palette: [#1abc9c, #6cc551, #b1dbef, #2da7c7, #b281c5, #103141, #f8e095, #de8754, #b33f32, #95a5a6]
# Dark palette: [#16a085, #52a838, #88c9e7, #227f96, #7b4e8e, #081821, #dcb22d, #d66829, #ab2a1c, #7f8c8d]
tabs:
fields:
logo:
label: backend::lang.branding.logo
type: fileupload
commentAbove: backend::lang.branding.logo_description
mode: image
imageHeight: 170
tab: backend::lang.branding.brand
span: right
fileTypes: jpg,jpeg,bmp,png,webp,gif,svg
app_name:
label: backend::lang.branding.app_name
commentAbove: backend::lang.branding.app_name_description
tab: backend::lang.branding.brand
span: left
app_tagline:
label: backend::lang.branding.app_tagline
commentAbove: backend::lang.branding.app_tagline_description
tab: backend::lang.branding.brand
span: left
favicon:
label: backend::lang.branding.favicon
type: fileupload
commentAbove: backend::lang.branding.favicon_description
mode: image
imageHeight: 32
tab: backend::lang.branding.brand
span: right
fileTypes: jpg,jpeg,bmp,png,webp,gif,svg,ico
_branding_colors:
label: backend::lang.branding.branding_colors
comment: backend::lang.branding.branding_colors_comment
type: section
tab: backend::lang.branding.colors
primary_color:
label: backend::lang.branding.primary_color
type: colorpicker
span: storm
cssClass: row col-lg-4
tab: backend::lang.branding.colors
availableColors: ['#16a085', '#52a838', '#88c9e7', '#2da7c7', '#7b4e8e', '#081821', '#dcb22d', '#d66829', '#ab2a1c', '#7f8c8d']
secondary_color:
label: backend::lang.branding.secondary_color
type: colorpicker
span: storm
cssClass: row col-lg-4
tab: backend::lang.branding.colors
availableColors: ['#16a085', '#52a838', '#88c9e7', '#2da7c7', '#7b4e8e', '#081821', '#dcb22d', '#d66829', '#ab2a1c', '#7f8c8d']
accent_color:
label: backend::lang.branding.accent_color
type: colorpicker
span: storm
cssClass: row col-lg-4
tab: backend::lang.branding.colors
availableColors: ['#16a085', '#6cc551', '#88c9e7', '#2da7c7', '#7b4e8e', '#081821', '#dcb22d', '#d66829', '#ab2a1c', '#7f8c8d']
_default_colors:
label: backend::lang.branding.default_colors
comment: backend::lang.branding.default_colors_comment
type: section
span: storm
cssClass: row col-lg-12
tab: backend::lang.branding.colors
default_colors:
label: false
type: repeater
tab: backend::lang.branding.colors
prompt: backend::lang.branding.add_default_color
titleFrom: color
form:
fields:
color:
label: false
type: colorpicker
availableColors: []
allowCustom: true
allowAlpha: true
formats: all
span: full
menu_mode:
label: backend::lang.branding.menu_mode
tab: backend::lang.branding.navigation
type: radio
options:
inline: backend::lang.branding.menu_mode_inline
inline_no_icons: backend::lang.branding.menu_mode_inline_no_icons
tile: backend::lang.branding.menu_mode_tile
collapse: backend::lang.branding.menu_mode_collapsed
custom_css:
label: backend::lang.branding.custom_stylesheet
type: codeeditor
tab: backend::lang.branding.styles
size: giant
language: less

View File

@@ -0,0 +1,10 @@
<div>
<?php foreach ($this->formWidget->model->getEditorToolbarPresets() as $name => $preset): ?>
<button type="button" class="btn btn-default btn-sm"
data-preset="<?= e($preset) ?>"
onclick="document.querySelector('#Form-field-EditorSetting-html_toolbar_buttons').value = this.dataset.preset"
>
<?= e(trans('backend::lang.editor.toolbar_buttons_presets.' . $name)) ?>
</button>
<?php endforeach; ?>
</div>

View File

@@ -0,0 +1,77 @@
/*
* Text
*/
.wn-text-gray,
.oc-text-gray {
color: #AAA !important;
}
.wn-text-bordered,
.oc-text-bordered {
border-top: solid 1px #222;
border-bottom: solid 1px #222;
padding: 10px 0;
}
.wn-text-spaced,
.oc-text-spaced {
letter-spacing: 1px;
}
.wn-text-uppercase,
.oc-text-uppercase {
text-transform: uppercase;
}
/*
* Links
*/
a.wn-link-strong,
a.oc-link-strong {
font-weight: 700;
}
a.wn-link-green,
a.oc-link-green {
color: green;
}
/*
* Table
*/
table.wn-dashed-borders td,
table.wn-dashed-borders th
table.oc-dashed-borders td,
table.oc-dashed-borders th {
border-style: dashed;
}
table.wn-alternate-rows tbody tr:nth-child(2n),
table.oc-alternate-rows tbody tr:nth-child(2n) {
background: #f5f5f5;
}
/*
* Table cell
*/
table td.wn-cell-highlighted,
table th.wn-cell-highlighted,
table td.oc-cell-highlighted,
table th.oc-cell-highlighted {
border: 1px double red;
}
table td.wn-cell-thick-border,
table th.wn-cell-thick-border,
table td.oc-cell-thick-border,
table th.oc-cell-thick-border {
border-width: 2px;
}
/*
* Images
*/
img.wn-img-rounded,
img.oc-img-rounded {
border-radius: 100%;
background-clip: padding-box;
}
img.wn-img-bordered,
img.oc-img-bordered {
border: solid 10px #CCC;
box-sizing: content-box;
}

View File

@@ -0,0 +1,139 @@
# ===================================
# Field Definitions
# ===================================
tabs:
fields:
html_custom_styles:
label: backend::lang.editor.custom_styles
commentAbove: backend::lang.editor.custom_styles_comment
tab: backend::lang.editor.markup_styles
type: codeeditor
size: giant
language: css
html_style_paragraph:
label: backend::lang.editor.paragraph
tab: backend::lang.editor.markup_classes
span: auto
type: datatable
columns:
class_label:
title: backend::lang.editor.label
class_name:
title: backend::lang.editor.class_name
html_style_link:
label: backend::lang.editor.link
tab: backend::lang.editor.markup_classes
span: auto
type: datatable
columns:
class_label:
title: backend::lang.editor.label
class_name:
title: backend::lang.editor.class_name
html_style_table:
label: backend::lang.editor.table
tab: backend::lang.editor.markup_classes
span: auto
type: datatable
columns:
class_label:
title: backend::lang.editor.label
class_name:
title: backend::lang.editor.class_name
html_style_table_cell:
label: backend::lang.editor.table_cell
tab: backend::lang.editor.markup_classes
span: auto
type: datatable
columns:
class_label:
title: backend::lang.editor.label
class_name:
title: backend::lang.editor.class_name
html_style_image:
label: backend::lang.editor.image
tab: backend::lang.editor.markup_classes
span: auto
type: datatable
columns:
class_label:
title: backend::lang.editor.label
class_name:
title: backend::lang.editor.class_name
html_allow_tags:
label: backend::lang.editor.allowed_tags
comment: backend::lang.editor.allowed_tags_comment
tab: backend::lang.editor.markup_tags
type: textarea
html_allow_attributes:
label: backend::lang.editor.allowed_attributes
comment: backend::lang.editor.allowed_attributes_comment
tab: backend::lang.editor.markup_tags
type: textarea
html_allow_empty_tags:
label: backend::lang.editor.allowed_empty_tags
comment: backend::lang.editor.allowed_empty_tags_comment
tab: backend::lang.editor.markup_tags
type: textarea
size: small
span: auto
html_no_wrap_tags:
label: backend::lang.editor.no_wrap
comment: backend::lang.editor.no_wrap_comment
tab: backend::lang.editor.markup_tags
type: textarea
size: small
span: auto
html_remove_tags:
label: backend::lang.editor.remove_tags
comment: backend::lang.editor.remove_tags_comment
tab: backend::lang.editor.markup_tags
type: textarea
size: small
span: auto
html_line_breaker_tags:
label: backend::lang.editor.line_breaker_tags
comment: backend::lang.editor.line_breaker_tags_comment
tab: backend::lang.editor.markup_tags
type: textarea
size: small
span: auto
html_paragraph_formats:
label: backend::lang.editor.paragraph_formats
comment: backend::lang.editor.paragraph_formats_comment
tab: backend::lang.editor.toolbar_options
type: datatable
span: right
columns:
format_tag:
title: backend::lang.editor.markup_tag
format_label:
title: backend::lang.editor.label
html_toolbar_buttons:
label: backend::lang.editor.toolbar_buttons
comment: backend::lang.editor.toolbar_buttons_comment
tab: backend::lang.editor.toolbar_options
type: textarea
span: left
_html_toolbar_buttons_presets:
label: backend::lang.editor.toolbar_buttons_preset
tab: backend::lang.editor.toolbar_options
type: partial
path: ~/modules/backend/models/editorsetting/_toolbar_presets.php
span: left

View File

@@ -0,0 +1,136 @@
# ===================================
# Field Definitions
# ===================================
tabs:
fields:
locale:
tab: backend::lang.backend_preferences.region
label: backend::lang.backend_preferences.locale
comment: backend::lang.backend_preferences.locale_comment
type: dropdown
span: left
timezone:
tab: backend::lang.backend_preferences.region
label: backend::lang.backend_preferences.timezone
comment: backend::lang.backend_preferences.timezone_comment
type: dropdown
span: left
editor_theme:
label: backend::lang.editor.theme
tab: backend::lang.backend_preferences.code_editor
span: auto
type: dropdown
editor_word_wrap:
label: backend::lang.editor.word_wrap
tab: backend::lang.backend_preferences.code_editor
type: balloon-selector
span: auto
editor_font_size:
label: backend::lang.editor.font_size
tab: backend::lang.backend_preferences.code_editor
span: auto
type: dropdown
options:
11: 11px
12: 12px
13: 13px
14: 14px
15: 15px
16: 16px
editor_tab_size:
label: backend::lang.editor.tab_size
tab: backend::lang.backend_preferences.code_editor
span: auto
type: dropdown
options:
2: 2
3: 3
4: 4
5: 5
6: 6
7: 7
8: 8
editor_show_gutter:
label: backend::lang.editor.show_gutter
tab: backend::lang.backend_preferences.code_editor
type: checkbox
span: auto
editor_show_minimap:
label: backend::lang.editor.show_minimap
tab: backend::lang.backend_preferences.code_editor
type: checkbox
span: auto
editor_bracket_colors:
label: backend::lang.editor.bracket_colors
tab: backend::lang.backend_preferences.code_editor
type: checkbox
span: auto
editor_enable_folding:
label: backend::lang.editor.enable_folding
tab: backend::lang.backend_preferences.code_editor
type: checkbox
span: auto
editor_highlight_active_line:
label: backend::lang.editor.highlight_active_line
tab: backend::lang.backend_preferences.code_editor
type: checkbox
span: auto
editor_use_hard_tabs:
label: backend::lang.editor.use_hard_tabs
tab: backend::lang.backend_preferences.code_editor
type: checkbox
span: auto
editor_display_indent_guides:
label: backend::lang.editor.display_indent_guides
tab: backend::lang.backend_preferences.code_editor
type: checkbox
span: auto
editor_show_invisibles:
label: backend::lang.editor.show_invisibles
tab: backend::lang.backend_preferences.code_editor
type: checkbox
span: auto
editor_show_colors:
label: backend::lang.editor.show_colors
tab: backend::lang.backend_preferences.code_editor
type: checkbox
span: auto
editor_show_print_margin:
label: backend::lang.editor.show_print_margin
tab: backend::lang.backend_preferences.code_editor
type: checkbox
span: auto
editor_auto_closing:
label: backend::lang.editor.auto_closing
tab: backend::lang.backend_preferences.code_editor
type: checkbox
span: auto
_editor_preview_lang:
type: partial
tab: backend::lang.backend_preferences.code_editor
path: field_editor_preview_lang
_editor_preview:
type: codeeditor
tab: backend::lang.backend_preferences.code_editor
size: giant
span: full

View File

@@ -0,0 +1,76 @@
# ===================================
# Column Definitions
# ===================================
columns:
first_name:
label: backend::lang.user.first_name
searchable: true
invisible: true
last_name:
label: backend::lang.user.last_name
searchable: true
invisible: true
full_name:
label: backend::lang.user.full_name
select: concat(first_name, ' ', last_name)
searchable: true
invisible: true
login:
label: backend::lang.user.login
searchable: true
width: 15%
email:
label: backend::lang.user.email
searchable: true
groups:
label: backend::lang.user.groups
relation: groups
select: name
sortable: false
role:
label: backend::lang.user.role.name
relation: role
select: name
sortable: true
searchable: true
last_login:
label: backend::lang.user.last_login
searchable: true
type: datetime
created_at:
label: backend::lang.user.created_at
searchable: true
invisible: true
type: datetime
updated_at:
label: backend::lang.user.updated_at
searchable: true
invisible: true
type: datetime
deleted_at:
label: backend::lang.user.deleted_at
searchable: true
invisible: true
type: datetime
is_activated:
label: backend::lang.user.activated
invisible: true
type: switch
is_superuser:
label: backend::lang.user.superuser
invisible: true
type: switch

View File

@@ -0,0 +1,109 @@
# ===================================
# Field Definitions
# ===================================
fields:
first_name:
span: left
label: backend::lang.user.first_name
last_name:
span: right
label: backend::lang.user.last_name
login:
span: left
label: backend::lang.user.login
email:
span: right
type: email
label: backend::lang.user.email
tabs:
defaultTab: backend::lang.user.account
icons:
backend::lang.user.account: icon-user
backend::lang.user.groups: icon-users
backend::lang.user.permissions: icon-key
backend::lang.user.throttle_tab: icon-ban
fields:
send_invite:
context: create
type: checkbox
label: backend::lang.user.send_invite
comment: backend::lang.user.send_invite_comment
default: true
_auto_generate_password:
context: create
type: checkbox
label: backend::lang.user.auto_generate_password
comment: backend::lang.user.auto_generate_password_comment
default: true
trigger:
action: show
field: send_invite
condition: checked
password:
type: password
span: left
label: backend::lang.user.password
trigger:
action: disable
field: _auto_generate_password
condition: checked
password_confirmation:
type: password
span: right
label: backend::lang.user.password_confirmation
trigger:
action: disable
field: _auto_generate_password
condition: checked
role:
context: [create, update]
label: backend::lang.user.role_field
commentAbove: backend::lang.user.role_comment
type: radio
groups:
context: [create, update]
label: backend::lang.user.groups
commentAbove: backend::lang.user.groups_comment
type: checkboxlist
tab: backend::lang.user.groups
throttle:
label: backend::lang.user.throttle_tab_label
type: relationmanager
tab: backend::lang.user.throttle_tab
context: [update]
commentAbove: backend::lang.user.throttle_comment
secondaryTabs:
fields:
btn_impersonate:
label: ""
context: [update]
type: partial
btn_unsuspend:
label: ""
context: [update]
type: partial
btn_password_reset:
label: ""
context: [update]
type: partial
avatar:
label: backend::lang.user.avatar
type: fileupload
mode: image
imageHeight: 250
imageWidth: 250
is_superuser:
context: [create, update]
label: backend::lang.user.superuser
type: switch
comment: backend::lang.user.superuser_comment

View File

@@ -0,0 +1,36 @@
# ===================================
# Column Definitions
# ===================================
columns:
name:
label: backend::lang.user.group.name_field
searchable: true
code:
label: backend::lang.user.group.code_field
searchable: true
invisible: true
description:
label: backend::lang.user.group.description_field
searchable: true
users_count:
label: backend::lang.user.group.users_count
relation: users_count
valueFrom: count
default: 0
sortable: false
created_at:
label: backend::lang.user.created_at
searchable: true
invisible: true
type: datetime
updated_at:
label: backend::lang.user.updated_at
searchable: true
invisible: true
type: datetime

View File

@@ -0,0 +1,24 @@
# ===================================
# Field Definitions
# ===================================
fields:
is_new_user_default:
label: backend::lang.user.group.is_new_user_default_field_label
comment: backend::lang.user.group.is_new_user_default_field_comment
type: switch
name:
label: backend::lang.user.group.name_field
commentAbove: backend::lang.user.group.name_comment
span: auto
code:
label: backend::lang.user.group.code_field
commentAbove: backend::lang.user.group.code_comment
span: auto
description:
label: backend::lang.user.group.description_field
type: textarea
size: tiny

View File

@@ -0,0 +1,36 @@
# ===================================
# Column Definitions
# ===================================
columns:
name:
label: backend::lang.user.role.name_field
searchable: true
code:
label: backend::lang.user.role.code_field
searchable: true
invisible: true
description:
label: backend::lang.user.role.description_field
searchable: true
users_count:
label: backend::lang.user.role.users_count
relation: users_count
valueFrom: count
default: 0
sortable: false
created_at:
label: backend::lang.user.created_at
searchable: true
invisible: true
type: datetime
updated_at:
label: backend::lang.user.updated_at
searchable: true
invisible: true
type: datetime

View File

@@ -0,0 +1,34 @@
# ===================================
# Field Definitions
# ===================================
fields:
name:
label: backend::lang.user.role.name_field
commentAbove: backend::lang.user.role.name_comment
span: auto
code:
label: backend::lang.user.role.code_field
commentAbove: backend::lang.user.role.code_comment
span: auto
preset:
type: slug
field: name
description:
label: backend::lang.user.role.description_field
type: textarea
size: tiny
tabs:
stretch: true
fields:
permissions:
tab: 'backend::lang.user.permissions'
type: 'Backend\FormWidgets\PermissionEditor'
mode: 'checkbox'
_users@update:
label: ''
tab: backend::lang.user.menu_label
type: partial