feat: VivesPOS landing on Winter CMS 1.2 — theme + plugin + Dockerfile
Some checks are pending
Module sub-split / Sub-split (push) Waiting to run
Some checks are pending
Module sub-split / Sub-split (push) Waiting to run
- Base: wintercms/winter branch 1.2 (full framework) - Theme vivespos: Canvas 7 + Bootstrap 5 CDN, custom CSS - Layout: deferred GTM/GA4 tracking, JSON-LD SoftwareApplication - Partials: hero (offline-first), features, modes (offline/nube toggle), screenshots, pricing (3 planes), comparison, FAQ, CTA - Plugin VivesPOS.Site with ContactForm - Dockerfile: PHP 8.2 Apache, port 80, healthcheck - Added winter/wn-pages, blog, sitemap, seo plugins - Active theme set to vivespos
This commit is contained in:
61
modules/backend/models/AccessLog.php
Normal file
61
modules/backend/models/AccessLog.php
Normal 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();
|
||||
}
|
||||
}
|
||||
262
modules/backend/models/BrandSetting.php
Normal file
262
modules/backend/models/BrandSetting.php
Normal 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;
|
||||
}
|
||||
}
|
||||
281
modules/backend/models/EditorSetting.php
Normal file
281
modules/backend/models/EditorSetting.php
Normal 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();
|
||||
}
|
||||
}
|
||||
206
modules/backend/models/ExportModel.php
Normal file
206
modules/backend/models/ExportModel.php
Normal 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);
|
||||
}
|
||||
}
|
||||
292
modules/backend/models/ImportModel.php
Normal file
292
modules/backend/models/ImportModel.php
Normal 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;
|
||||
}
|
||||
}
|
||||
328
modules/backend/models/Preference.php
Normal file
328
modules/backend/models/Preference.php
Normal 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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
456
modules/backend/models/User.php
Normal file
456
modules/backend/models/User.php
Normal 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;
|
||||
}
|
||||
}
|
||||
46
modules/backend/models/UserGroup.php
Normal file
46
modules/backend/models/UserGroup.php
Normal 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'));
|
||||
}
|
||||
}
|
||||
35
modules/backend/models/UserPreference.php
Normal file
35
modules/backend/models/UserPreference.php
Normal 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;
|
||||
}
|
||||
}
|
||||
101
modules/backend/models/UserRole.php
Normal file
101
modules/backend/models/UserRole.php
Normal 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);
|
||||
}
|
||||
}
|
||||
36
modules/backend/models/UserThrottle.php
Normal file
36
modules/backend/models/UserThrottle.php
Normal 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);
|
||||
}
|
||||
}
|
||||
53
modules/backend/models/accesslog/columns.yaml
Normal file
53
modules/backend/models/accesslog/columns.yaml
Normal 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
|
||||
234
modules/backend/models/brandsetting/custom.less
Normal file
234
modules/backend/models/brandsetting/custom.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
112
modules/backend/models/brandsetting/fields.yaml
Normal file
112
modules/backend/models/brandsetting/fields.yaml
Normal 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
|
||||
10
modules/backend/models/editorsetting/_toolbar_presets.php
Normal file
10
modules/backend/models/editorsetting/_toolbar_presets.php
Normal 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>
|
||||
77
modules/backend/models/editorsetting/default_styles.less
Normal file
77
modules/backend/models/editorsetting/default_styles.less
Normal 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;
|
||||
}
|
||||
139
modules/backend/models/editorsetting/fields.yaml
Normal file
139
modules/backend/models/editorsetting/fields.yaml
Normal 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
|
||||
136
modules/backend/models/preference/fields.yaml
Normal file
136
modules/backend/models/preference/fields.yaml
Normal 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
|
||||
76
modules/backend/models/user/columns.yaml
Normal file
76
modules/backend/models/user/columns.yaml
Normal 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
|
||||
109
modules/backend/models/user/fields.yaml
Normal file
109
modules/backend/models/user/fields.yaml
Normal 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
|
||||
36
modules/backend/models/usergroup/columns.yaml
Normal file
36
modules/backend/models/usergroup/columns.yaml
Normal 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
|
||||
24
modules/backend/models/usergroup/fields.yaml
Normal file
24
modules/backend/models/usergroup/fields.yaml
Normal 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
|
||||
36
modules/backend/models/userrole/columns.yaml
Normal file
36
modules/backend/models/userrole/columns.yaml
Normal 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
|
||||
34
modules/backend/models/userrole/fields.yaml
Normal file
34
modules/backend/models/userrole/fields.yaml
Normal 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
|
||||
Reference in New Issue
Block a user