Merge pull request #5359 from octobercms/develop

v1.1.1
This commit is contained in:
Luke Towers 2020-11-22 04:25:36 -06:00 committed by GitHub
commit f45ca422a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
88 changed files with 1577 additions and 311 deletions

View File

@ -3,7 +3,8 @@ name: Code Quality
on:
push:
branches:
- master
- 1.0
- 1.1
- develop
jobs:

View File

@ -3,7 +3,8 @@ name: Tests
on:
push:
branches:
- master
- 1.0
- 1.1
- develop
pull_request:
@ -62,7 +63,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.phpVersion }}
tools: composer
tools: composer:v1
extensions: ${{ env.extensions }}
- name: Setup dependency cache

View File

@ -1,12 +1,12 @@
<p align="center">
<img src="https://github.com/octobercms/october/blob/master/themes/demo/assets/images/october.png?raw=true" alt="October" width="25%" height="25%" />
<img src="https://github.com/octobercms/october/blob/develop/themes/demo/assets/images/october.png?raw=true" alt="October" width="25%" height="25%" />
</p>
[October](https://octobercms.com) is a Content Management System (CMS) and web platform whose sole purpose is to make your development workflow simple again. It was born out of frustration with existing systems. We feel building websites has become a convoluted and confusing process that leaves developers unsatisfied. We want to turn you around to the simpler side and get back to basics.
October's mission is to show the world that web development is not rocket science.
![Stable Build](https://github.com/octobercms/october/workflows/Tests/badge.svg?branch=master)
![Stable Build](https://github.com/octobercms/october/workflows/Tests/badge.svg?branch=1.1)
[![License](https://poser.pugx.org/october/october/license.svg)](https://packagist.org/packages/october/october)
## Installing October
@ -73,7 +73,7 @@ Before sending or reviewing Pull Requests, be sure to review the [Contributing G
Please follow the following guides and code standards:
* [PSR 4 Coding Standards](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md)
* [PSR 4 Autoloader](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md)
* [PSR 2 Coding Style Guide](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)
* [PSR 1 Coding Standards](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md)

View File

@ -40,7 +40,8 @@
"wikimedia/composer-merge-plugin": "1.4.1"
},
"require-dev": {
"phpunit/phpunit": "^8.0|^9.0",
"phpunit/phpunit": "^8.4|^9.3.3",
"mockery/mockery": "~1.3.3|^1.4.2",
"fzaninotto/faker": "~1.9",
"squizlabs/php_codesniffer": "3.*",
"php-parallel-lint/php-parallel-lint": "^1.0",

View File

@ -19,7 +19,7 @@ return [
| Failed Authentication Attempt Limit
|--------------------------------------------------------------------------
|
| Number of failed attemps allowed while trying to authenticate a user.
| Number of failed attempts allowed while trying to authenticate a user.
|
*/
'attemptLimit' => 5,

View File

@ -169,17 +169,31 @@ return [
|--------------------------------------------------------------------------
|
| This option determines how your cookies behave when cross-site requests
| take place, and can be used to mitigate CSRF attacks. By default, we
| do not enable this as other CSRF protection services are in place.
| take place and can be used to mitigate CSRF attacks.
|
| In the strict mode, the cookie is not sent with any cross-site usage
| even if the user follows a link to another website. Lax cookies are
| only sent with a top-level get request.
| Cookies that match the domain of the current site, i.e. what's displayed
| in the browser's address bar, are referred to as first-party cookies.
| Similarly, cookies from domains other than the current site are referred
| to as third-party cookies.
|
| Supported: "lax", "strict"
| Cookies without a SameSite attribute will be treated as `SameSite=Lax`,
| meaning the default behaviour will be to restrict cookies to first party
| contexts only.
|
| Cookies for cross-site usage must specify `same_site` as 'None' and `secure`
| as `true` to work correctly.
|
| Lax - Cookies are allowed to be sent with top-level navigations and will
| be sent along with GET request initiated by third party website.
| This is the default value in modern browsers.
|
| Strict - Cookies will only be sent in a first-party context and not be
| sent along with requests initiated by third party websites.
|
| Supported: "Lax", "Strict" and "None"
|
*/
'same_site' => null,
'same_site' => 'Lax',
];

View File

@ -141,31 +141,40 @@ class ServiceProvider extends ModuleServiceProvider
$manager->registerPermissions('October.Backend', [
'backend.access_dashboard' => [
'label' => 'system::lang.permissions.view_the_dashboard',
'tab' => 'system::lang.permissions.name'
'tab' => 'system::lang.permissions.name',
],
'backend.manage_default_dashboard' => [
'label' => 'system::lang.permissions.manage_default_dashboard',
'tab' => 'system::lang.permissions.name',
'roles' => UserRole::CODE_DEVELOPER,
],
'backend.manage_users' => [
'label' => 'system::lang.permissions.manage_other_administrators',
'tab' => 'system::lang.permissions.name'
'tab' => 'system::lang.permissions.name',
'roles' => UserRole::CODE_DEVELOPER,
],
'backend.impersonate_users' => [
'label' => 'system::lang.permissions.impersonate_users',
'tab' => 'system::lang.permissions.name',
'roles' => UserRole::CODE_DEVELOPER,
],
'backend.manage_preferences' => [
'label' => 'system::lang.permissions.manage_preferences',
'tab' => 'system::lang.permissions.name'
'tab' => 'system::lang.permissions.name',
],
'backend.manage_editor' => [
'label' => 'system::lang.permissions.manage_editor',
'tab' => 'system::lang.permissions.name'
'tab' => 'system::lang.permissions.name',
'roles' => UserRole::CODE_DEVELOPER,
],
'backend.manage_own_editor' => [
'label' => 'system::lang.permissions.manage_own_editor',
'tab' => 'system::lang.permissions.name',
],
'backend.manage_branding' => [
'label' => 'system::lang.permissions.manage_branding',
'tab' => 'system::lang.permissions.name'
'tab' => 'system::lang.permissions.name',
'roles' => UserRole::CODE_DEVELOPER,
],
'media.manage_media' => [
'label' => 'backend::lang.permissions.manage_media',

View File

@ -679,9 +679,10 @@ nav#layout-mainmenu .toolbar-item:before {left:-12px}
nav#layout-mainmenu .toolbar-item:after {right:-12px}
nav#layout-mainmenu .toolbar-item.scroll-active-before:before {color:#fff}
nav#layout-mainmenu .toolbar-item.scroll-active-after:after {color:#fff}
nav#layout-mainmenu ul.mainmenu-toolbar li.mainmenu-preview {margin:0 0 0 21px}
nav#layout-mainmenu ul.mainmenu-toolbar li.mainmenu-preview i {font-size:20px}
nav#layout-mainmenu ul.mainmenu-toolbar li.mainmenu-preview a {position:relative;padding:0 10px;top:-1px}
nav#layout-mainmenu ul.mainmenu-toolbar li.mainmenu-quick-action {margin:0}
nav#layout-mainmenu ul.mainmenu-toolbar li.mainmenu-quick-action:first-child {margin-left:21px}
nav#layout-mainmenu ul.mainmenu-toolbar li.mainmenu-quick-action i {font-size:20px}
nav#layout-mainmenu ul.mainmenu-toolbar li.mainmenu-quick-action a {position:relative;padding:0 10px;top:-1px}
nav#layout-mainmenu ul.mainmenu-toolbar li.mainmenu-account {margin-right:0}
nav#layout-mainmenu ul.mainmenu-toolbar li.mainmenu-account >a {padding:0 15px 0 10px;font-size:13px;position:relative}
nav#layout-mainmenu ul.mainmenu-toolbar li.mainmenu-account.highlight >a {z-index:600}
@ -706,8 +707,8 @@ nav#layout-mainmenu ul li .mainmenu-accountmenu li:first-child a:active:after {c
nav#layout-mainmenu ul li .mainmenu-accountmenu li.divider {height:1px;width:100%;background-color:#e0e0e0}
nav#layout-mainmenu.navbar-mode-inline,
nav#layout-mainmenu.navbar-mode-inline_no_icons {height:60px}
nav#layout-mainmenu.navbar-mode-inline ul.mainmenu-toolbar li.mainmenu-preview a,
nav#layout-mainmenu.navbar-mode-inline_no_icons ul.mainmenu-toolbar li.mainmenu-preview a {height:60px;line-height:60px}
nav#layout-mainmenu.navbar-mode-inline ul.mainmenu-toolbar li.mainmenu-quick-action a,
nav#layout-mainmenu.navbar-mode-inline_no_icons ul.mainmenu-toolbar li.mainmenu-quick-action a {height:60px;line-height:60px}
nav#layout-mainmenu.navbar-mode-inline ul.mainmenu-toolbar li.mainmenu-account >a,
nav#layout-mainmenu.navbar-mode-inline_no_icons ul.mainmenu-toolbar li.mainmenu-account >a {height:60px;line-height:60px}
nav#layout-mainmenu.navbar-mode-inline ul li .mainmenu-accountmenu,
@ -730,7 +731,7 @@ nav#layout-mainmenu.navbar-mode-inline ul.mainmenu-nav li:last-child,
nav#layout-mainmenu.navbar-mode-inline_no_icons ul.mainmenu-nav li:last-child {margin-right:0}
nav#layout-mainmenu.navbar-mode-inline_no_icons .nav-icon {display:none !important}
nav#layout-mainmenu.navbar-mode-tile {height:78px}
nav#layout-mainmenu.navbar-mode-tile ul.mainmenu-toolbar li.mainmenu-preview a {height:78px;line-height:78px}
nav#layout-mainmenu.navbar-mode-tile ul.mainmenu-toolbar li.mainmenu-quick-action a {height:78px;line-height:78px}
nav#layout-mainmenu.navbar-mode-tile ul.mainmenu-toolbar li.mainmenu-account >a {height:78px;line-height:78px}
nav#layout-mainmenu.navbar-mode-tile ul li .mainmenu-accountmenu {top:88px}
nav#layout-mainmenu.navbar-mode-tile ul.mainmenu-nav li a {position:relative;width:65px;height:65px}
@ -749,14 +750,14 @@ nav#layout-mainmenu .menu-toggle .menu-toggle-title {margin-left:10px}
nav#layout-mainmenu .menu-toggle:hover .menu-toggle-icon {opacity:1}
body.mainmenu-open nav#layout-mainmenu .menu-toggle-icon {opacity:1}
nav#layout-mainmenu.navbar-mode-collapse {padding-left:0;height:45px}
nav#layout-mainmenu.navbar-mode-collapse ul.mainmenu-toolbar li.mainmenu-preview a {height:45px;line-height:45px}
nav#layout-mainmenu.navbar-mode-collapse ul.mainmenu-toolbar li.mainmenu-quick-action a {height:45px;line-height:45px}
nav#layout-mainmenu.navbar-mode-collapse ul.mainmenu-toolbar li.mainmenu-account >a {height:45px;line-height:45px}
nav#layout-mainmenu.navbar-mode-collapse ul li .mainmenu-accountmenu {top:55px}
nav#layout-mainmenu.navbar-mode-collapse ul.mainmenu-toolbar li.mainmenu-account >a {padding-right:0}
nav#layout-mainmenu.navbar-mode-collapse ul li .mainmenu-accountmenu:after {right:13px}
nav#layout-mainmenu.navbar-mode-collapse ul.nav {display:none}
nav#layout-mainmenu.navbar-mode-collapse .menu-toggle {display:inline-block;color:#fff !important}
@media (max-width:769px) {nav#layout-mainmenu.navbar {padding-left:0;height:45px }nav#layout-mainmenu.navbar ul.mainmenu-toolbar li.mainmenu-preview a {height:45px;line-height:45px }nav#layout-mainmenu.navbar ul.mainmenu-toolbar li.mainmenu-account >a {height:45px;line-height:45px }nav#layout-mainmenu.navbar ul li .mainmenu-accountmenu {top:55px }nav#layout-mainmenu.navbar ul.mainmenu-toolbar li.mainmenu-account >a {padding-right:0 }nav#layout-mainmenu.navbar ul li .mainmenu-accountmenu:after {right:13px }nav#layout-mainmenu.navbar ul.nav {display:none }nav#layout-mainmenu.navbar .menu-toggle {display:inline-block;color:#fff !important }}
@media (max-width:769px) {nav#layout-mainmenu.navbar {padding-left:0;height:45px }nav#layout-mainmenu.navbar ul.mainmenu-toolbar li.mainmenu-quick-action a {height:45px;line-height:45px }nav#layout-mainmenu.navbar ul.mainmenu-toolbar li.mainmenu-account >a {height:45px;line-height:45px }nav#layout-mainmenu.navbar ul li .mainmenu-accountmenu {top:55px }nav#layout-mainmenu.navbar ul.mainmenu-toolbar li.mainmenu-account >a {padding-right:0 }nav#layout-mainmenu.navbar ul li .mainmenu-accountmenu:after {right:13px }nav#layout-mainmenu.navbar ul.nav {display:none }nav#layout-mainmenu.navbar .menu-toggle {display:inline-block;color:#fff !important }}
.mainmenu-collapsed {position:absolute;height:100%;top:0;left:0;margin:0;background:#000}
.mainmenu-collapsed >div {display:block;height:100%}
.mainmenu-collapsed >div ul.mainmenu-nav li a {position:relative;width:65px;height:65px}

View File

@ -47,7 +47,7 @@ body.mainmenu-open {
height: @height;
ul.mainmenu-toolbar {
li.mainmenu-preview {
li.mainmenu-quick-action {
a {
height: @height;
line-height: @height;
@ -191,8 +191,12 @@ nav#layout-mainmenu {
//
ul.mainmenu-toolbar {
li.mainmenu-preview {
margin: 0 0 0 21px;
li.mainmenu-quick-action {
margin: 0;
&:first-child {
margin-left: 21px;
}
i {
font-size: 20px;

View File

@ -803,9 +803,9 @@ class ImportExportController extends ControllerBehavior
if (
$options['encoding'] !== null &&
$reader->isActiveStreamFilter()
$reader->supportsStreamFilter()
) {
$reader->appendStreamFilter(sprintf(
$reader->addStreamFilter(sprintf(
'%s%s:%s',
TranscodeFilter::FILTER_NAME,
strtolower($options['encoding']),

View File

@ -297,16 +297,6 @@ class ListController extends ControllerBehavior
return call_user_func_array([$this->controller, 'onDelete'], func_get_args());
}
/*
* Validate checked identifiers
*/
$checkedIds = post('checked');
if (!$checkedIds || !is_array($checkedIds) || !count($checkedIds)) {
Flash::error(Lang::get('backend::lang.list.delete_selected_empty'));
return $this->controller->listRefresh();
}
/*
* Establish the list definition
*/
@ -318,6 +308,20 @@ class ListController extends ControllerBehavior
$listConfig = $this->controller->listGetConfig($definition);
/*
* Validate checked identifiers
*/
$checkedIds = post('checked');
if (!$checkedIds || !is_array($checkedIds) || !count($checkedIds)) {
Flash::error(Lang::get(
(!empty($listConfig->noRecordsDeletedMessage))
? $listConfig->noRecordsDeletedMessage
: 'backend::lang.list.delete_selected_empty'
));
return $this->controller->listRefresh();
}
/*
* Create the model
*/
@ -344,10 +348,18 @@ class ListController extends ControllerBehavior
$record->delete();
}
Flash::success(Lang::get('backend::lang.list.delete_selected_success'));
Flash::success(Lang::get(
(!empty($listConfig->deleteMessage))
? $listConfig->deleteMessage
: 'backend::lang.list.delete_selected_success'
));
}
else {
Flash::error(Lang::get('backend::lang.list.delete_selected_empty'));
Flash::error(Lang::get(
(!empty($listConfig->noRecordsDeletedMessage))
? $listConfig->noRecordsDeletedMessage
: 'backend::lang.list.delete_selected_empty'
));
}
return $this->controller->listRefresh($definition);

View File

@ -669,8 +669,9 @@ class RelationController extends ControllerBehavior
$config->defaultSort = $this->getConfig('view[defaultSort]');
$config->recordsPerPage = $this->getConfig('view[recordsPerPage]');
$config->showCheckboxes = $this->getConfig('view[showCheckboxes]', !$this->readOnly);
$config->recordUrl = $this->getConfig('view[recordUrl]', null);
$config->customViewPath = $this->getConfig('view[customViewPath]', null);
$config->recordUrl = $this->getConfig('view[recordUrl]');
$config->customViewPath = $this->getConfig('view[customViewPath]');
$config->noRecordsMessage = $this->getConfig('view[noRecordsMessage]');
$defaultOnClick = sprintf(
"$.oc.relationBehavior.clickViewListRecord(':%s', '%s', '%s')",
@ -818,6 +819,7 @@ class RelationController extends ControllerBehavior
$config->showSorting = $this->getConfig('manage[showSorting]', !$isPivot);
$config->defaultSort = $this->getConfig('manage[defaultSort]');
$config->recordsPerPage = $this->getConfig('manage[recordsPerPage]');
$config->noRecordsMessage = $this->getConfig('manage[noRecordsMessage]');
if ($this->viewMode == 'single') {
$config->showCheckboxes = false;

View File

@ -214,7 +214,10 @@ class ReorderController extends ControllerBehavior
$model = $this->controller->reorderGetModel();
$modelTraits = class_uses($model);
if (isset($modelTraits[\October\Rain\Database\Traits\Sortable::class])) {
if (
isset($modelTraits[\October\Rain\Database\Traits\Sortable::class]) ||
$model->isClassExtendedWith(\October\Rain\Database\Behaviors\Sortable::class)
) {
$this->sortMode = 'simple';
}
elseif (isset($modelTraits[\October\Rain\Database\Traits\NestedTree::class])) {
@ -222,7 +225,7 @@ class ReorderController extends ControllerBehavior
$this->showTree = true;
}
else {
throw new ApplicationException('The model must implement the NestedTree or Sortable traits.');
throw new ApplicationException('The model must implement the Sortable trait/behavior or the NestedTree trait.');
}
return $model;

View File

@ -28,6 +28,11 @@ class NavigationManager
*/
protected $items;
/**
* @var QuickActionItem[] List of registered quick actions.
*/
protected $quickActions;
protected $contextSidenavPartials = [];
protected $contextOwner;
@ -54,6 +59,9 @@ class NavigationManager
*/
protected function loadItems()
{
$this->items = [];
$this->quickActions = [];
/*
* Load module items
*/
@ -68,11 +76,18 @@ class NavigationManager
foreach ($plugins as $id => $plugin) {
$items = $plugin->registerNavigation();
if (!is_array($items)) {
$quickActions = $plugin->registerQuickActions();
if (!is_array($items) && !is_array($quickActions)) {
continue;
}
$this->registerMenuItems($id, $items);
if (is_array($items)) {
$this->registerMenuItems($id, $items);
}
if (is_array($quickActions)) {
$this->registerQuickActions($id, $quickActions);
}
}
/**
@ -91,17 +106,21 @@ class NavigationManager
Event::fire('backend.menu.extendItems', [$this]);
/*
* Sort menu items
* Sort menu items and quick actions
*/
uasort($this->items, static function ($a, $b) {
return $a->order - $b->order;
});
uasort($this->quickActions, static function ($a, $b) {
return $a->order - $b->order;
});
/*
* Filter items user lacks permission for
* Filter items and quick actions that the user lacks permission for
*/
$user = BackendAuth::getUser();
$this->items = $this->filterItemPermissions($user, $this->items);
$this->quickActions = $this->filterItemPermissions($user, $this->quickActions);
foreach ($this->items as $item) {
if (!$item->sideMenu || !count($item->sideMenu)) {
@ -183,10 +202,6 @@ class NavigationManager
*/
public function registerMenuItems($owner, array $definitions)
{
if (!$this->items) {
$this->items = [];
}
$validator = Validator::make($definitions, [
'*.label' => 'required',
'*.icon' => 'required_without:*.iconSvg',
@ -320,6 +335,21 @@ class NavigationManager
return true;
}
/**
* Remove multiple side menu items
*
* @param string $owner
* @param string $code
* @param array $sideCodes
* @return void
*/
public function removeSideMenuItems($owner, $code, $sideCodes)
{
foreach ($sideCodes as $sideCode) {
$this->removeSideMenuItem($owner, $code, $sideCode);
}
}
/**
* Removes a single main menu item
* @param string $owner
@ -346,10 +376,14 @@ class NavigationManager
*/
public function listMainMenuItems()
{
if ($this->items === null) {
if ($this->items === null && $this->quickActions === null) {
$this->loadItems();
}
if ($this->items === null) {
return [];
}
foreach ($this->items as $item) {
if ($item->badge) {
$item->counter = (string) $item->badge;
@ -429,6 +463,137 @@ class NavigationManager
return $items;
}
/**
* Registers quick actions in the main navigation.
*
* Quick actions are single purpose links displayed to the left of the user menu in the
* backend main navigation.
*
* The argument is an array of the quick action items. The array keys represent the
* quick action item codes, specific for the plugin/module. Each element in the
* array should be an associative array with the following keys:
* - label - specifies the action label localization string key, used as a tooltip, required.
* - icon - an icon name from the Font Awesome icon collection, required if iconSvg is unspecified.
* - iconSvg - a custom SVG icon to use for the icon, required if icon is unspecified.
* - url - the back-end relative URL the quick action item should point to, required.
* - permissions - an array of permissions the back-end user should have, optional.
* The item will be displayed if the user has any of the specified permissions.
* - order - a position of the item in the menu, optional.
*
* @param string $owner Specifies the quick action items owner plugin or module in the format Author.Plugin.
* @param array $definitions An array of the quick action item definitions.
* @return void
* @throws SystemException If the validation of the quick action configuration fails
*/
public function registerQuickActions($owner, array $definitions)
{
$validator = Validator::make($definitions, [
'*.label' => 'required',
'*.icon' => 'required_without:*.iconSvg',
'*.url' => 'required'
]);
if ($validator->fails()) {
$errorMessage = 'Invalid quick action item detected in ' . $owner . '. Contact the plugin author to fix (' . $validator->errors()->first() . ')';
if (Config::get('app.debug', false)) {
throw new SystemException($errorMessage);
}
Log::error($errorMessage);
}
$this->addQuickActionItems($owner, $definitions);
}
/**
* Dynamically add an array of quick action items
*
* @param string $owner
* @param array $definitions
* @return void
*/
public function addQuickActionItems($owner, array $definitions)
{
foreach ($definitions as $code => $definition) {
$this->addQuickActionItem($owner, $code, $definition);
}
}
/**
* Dynamically add a single quick action item
*
* @param string $owner
* @param string $code
* @param array $definition
* @return void
*/
public function addQuickActionItem($owner, $code, array $definition)
{
$itemKey = $this->makeItemKey($owner, $code);
if (isset($this->quickActions[$itemKey])) {
$definition = array_merge((array) $this->quickActions[$itemKey], $definition);
}
$item = array_merge($definition, [
'code' => $code,
'owner' => $owner
]);
$this->quickActions[$itemKey] = QuickActionItem::createFromArray($item);
}
/**
* Gets the instance of a specified quick action item.
*
* @param string $owner
* @param string $code
* @return QuickActionItem
* @throws SystemException
*/
public function getQuickActionItem(string $owner, string $code)
{
$itemKey = $this->makeItemKey($owner, $code);
if (!array_key_exists($itemKey, $this->quickActions)) {
throw new SystemException('No quick action item found with key ' . $itemKey);
}
return $this->quickActions[$itemKey];
}
/**
* Removes a single quick action item
*
* @param $owner
* @param $code
* @return void
*/
public function removeQuickActionItem($owner, $code)
{
$itemKey = $this->makeItemKey($owner, $code);
unset($this->quickActions[$itemKey]);
}
/**
* Returns a list of quick action items.
*
* @return array
* @throws SystemException
*/
public function listQuickActionItems()
{
if ($this->items === null && $this->quickActions === null) {
$this->loadItems();
}
if ($this->quickActions === null) {
return [];
}
return $this->quickActions;
}
/**
* Sets the navigation context.
* The function sets the navigation owner, main menu item code and the side menu item code.

View File

@ -0,0 +1,105 @@
<?php namespace Backend\Classes;
/**
* Class QuickActionItem
*
* @package Backend\Classes
*/
class QuickActionItem
{
/**
* @var string
*/
public $code;
/**
* @var string
*/
public $owner;
/**
* @var string
*/
public $label;
/**
* @var null|string
*/
public $icon;
/**
* @var null|string
*/
public $iconSvg;
/**
* @var string
*/
public $url;
/**
* @var int
*/
public $order = -1;
/**
* @var array
*/
public $attributes = [];
/**
* @var array
*/
public $permissions = [];
/**
* @param null|string|int $attribute
* @param null|string|array $value
*/
public function addAttribute($attribute, $value)
{
$this->attributes[$attribute] = $value;
}
public function removeAttribute($attribute)
{
unset($this->attributes[$attribute]);
}
/**
* @param string $permission
* @param array $definition
*/
public function addPermission(string $permission, array $definition)
{
$this->permissions[$permission] = $definition;
}
/**
* @param string $permission
* @return void
*/
public function removePermission(string $permission)
{
unset($this->permissions[$permission]);
}
/**
* @param array $data
* @return static
*/
public static function createFromArray(array $data)
{
$instance = new static();
$instance->code = $data['code'];
$instance->owner = $data['owner'];
$instance->label = $data['label'];
$instance->url = $data['url'];
$instance->icon = $data['icon'] ?? null;
$instance->iconSvg = $data['iconSvg'] ?? null;
$instance->attributes = $data['attributes'] ?? $instance->attributes;
$instance->permissions = $data['permissions'] ?? $instance->permissions;
$instance->order = $data['order'] ?? $instance->order;
return $instance;
}
}

View File

@ -57,7 +57,7 @@ class Preferences extends Controller
*/
public function formExtendFields($form)
{
if (!$this->user->hasAccess('backend.manage_editor')) {
if (!$this->user->hasAccess('backend.manage_own_editor')) {
$form->removeTab('backend::lang.backend_preferences.code_editor');
}
}

View File

@ -8,8 +8,8 @@ scopes:
label: backend::lang.user.superuser
type: switch
conditions:
- is_superuser = false
- is_superuser = true
- is_superuser = 0
- is_superuser = 1
login_date:
label: backend::lang.user.last_login

View File

@ -1,5 +1,6 @@
<?php namespace Backend\Database\Seeds;
use Str;
use Seeder;
use Eloquent;
@ -8,12 +9,22 @@ class DatabaseSeeder extends Seeder
/**
* Run the database seeds.
*
* @return void
* @return string
*/
public function run()
{
Eloquent::unguarded(function () {
$this->call('Backend\Database\Seeds\SeedSetupAdmin');
$adminPassword = Str::random(22);
Eloquent::unguarded(function () use ($adminPassword) {
// Generate a random password for the seeded admin account
$adminSeeder = new \Backend\Database\Seeds\SeedSetupAdmin;
$adminSeeder->setDefaults([
'password' => $adminPassword
]);
$this->call($adminSeeder);
});
return 'The following password has been automatically generated for the "admin" account: '
. "<fg=yellow;options=bold>${adminPassword}</>";
}
}

View File

@ -2,12 +2,24 @@
use October\Rain\Support\Facade;
/**
* @method static string uri()
* @method static string url(string $path = null, array $parameters = [], bool $secure = null)
* @method static string baseUrl(string $path = null)
* @method static string skinAsset(string $path = null)
* @method static \Illuminate\Http\RedirectResponse redirect(string $path, int $status = 302, array $headers = [], bool $secure = null)
* @method static \Illuminate\Http\RedirectResponse redirectGuest(string $path, int $status = 302, array $headers = [], bool $secure = null)
* @method static \Illuminate\Http\RedirectResponse redirectIntended(string $path, int $status = 302, array $headers = [], bool $secure = null)
* @method static string date($dateTime, array $options = [])
* @method static string dateTime($dateTime, array $options = [])
*
* @see \Backend\Helpers\Backend
*/
class Backend extends Facade
{
/**
* Get the registered name of the component.
*
* @see \Backend\Helpers\Backend
* @return string
*/
protected static function getFacadeAccessor()

View File

@ -2,14 +2,22 @@
use October\Rain\Support\Facade;
/**
* @method static void registerCallback(callable $callback)
* @method static void registerPermissions(string $owner, array $definitions)
* @method static void removePermission(string $owner, string $code)
* @method static array listPermissions()
* @method static array listTabbedPermissions()
* @method static array listPermissionsForRole(string $role, bool $includeOrphans = true)
* @method static boolean hasPermissionsForRole(string $role)
*
* @see \Backend\Classes\AuthManager
*/
class BackendAuth extends Facade
{
/**
* Get the registered name of the component.
*
* Resolves to:
* - Backend\Classes\AuthManager
*
* @return string
*/
protected static function getFacadeAccessor()

View File

@ -2,14 +2,36 @@
use October\Rain\Support\Facade;
/**
* @method static void registerCallback(callable $callback)
* @method static void registerMenuItems(string $owner, array $definitions)
* @method static void addMainMenuItems(string $owner, array $definitions)
* @method static void addMainMenuItem(string $owner, $code, array $definition)
* @method static \Backend\Classes\MainMenuItem getMainMenuItem(string $owner, string $code)
* @method static void removeMainMenuItem(string $owner, string $code)
* @method static void addSideMenuItems(string $owner, string $code, array $definitions)
* @method static bool addSideMenuItem(string $owner, string $code, string $sideCode, array $definition)
* @method static bool removeSideMenuItem(string $owner, string $code, string $sideCode)
* @method static \Backend\Classes\MainMenuItem[] listMainMenuItems()
* @method static \Backend\Classes\SideMenuItem[] listSideMenuItems(string|null $owner = null, string|null $code = null)
* @method static void setContext(string $owner, string $mainMenuItemCode, string|null $sideMenuItemCode = null)
* @method static void setContextOwner(string $owner)
* @method static void setContextMainMenu(string $mainMenuItemCode)
* @method static object getContext()
* @method static void setContextSideMenu(string $sideMenuItemCode)
* @method static bool isMainMenuItemActive(\Backend\Classes\MainMenuItem $item)
* @method static \Backend\Classes\MainMenuItem|null getActiveMainMenuItem()
* @method static bool isSideMenuItemActive(\Backend\Classes\SideMenuItem $item)
* @method static void registerContextSidenavPartial(string $owner, string $mainMenuItemCode, string $partial)
* @method static mixed getContextSidenavPartial(string $owner, string $mainMenuItemCode)
*
* @see \Backend\Classes\NavigationManager
*/
class BackendMenu extends Facade
{
/**
* Get the registered name of the component.
*
* Resolves to:
* - Backend\Classes\NavigationManager
*
* @return string
*/
protected static function getFacadeAccessor()

View File

@ -248,16 +248,6 @@ class Repeater extends FormWidgetBase
}
}
if (!$this->childAddItemCalled && $currentValue === null) {
$this->formWidgets = [];
return;
}
if ($this->childAddItemCalled && !isset($currentValue[$this->childIndexCalled])) {
// If no value is available but a child repeater has added an item, add a "stub" repeater item
$this->makeItemFormWidget($this->childIndexCalled);
}
// Ensure that the minimum number of items are preinitialized
// ONLY DONE WHEN NOT IN GROUP MODE
if (!$this->useGroups && $this->minItems > 0) {
@ -273,6 +263,16 @@ class Repeater extends FormWidgetBase
}
}
if (!$this->childAddItemCalled && $currentValue === null) {
$this->formWidgets = [];
return;
}
if ($this->childAddItemCalled && !isset($currentValue[$this->childIndexCalled])) {
// If no value is available but a child repeater has added an item, add a "stub" repeater item
$this->makeItemFormWidget($this->childIndexCalled);
}
if (!is_array($currentValue)) {
return;
}

View File

@ -186,6 +186,19 @@ class RichEditor extends FormWidgetBase
{
$result = [];
/**
* @event backend.richeditor.listTypes
* Register additional "page link types" to the RichEditor FormWidget
*
* Example usage:
*
* Event::listen('backend.richeditor.listTypes', function () {
* return [
* 'my-identifier' => 'author.plugin::lang.richeditor.link_types.my_identifier',
* ];
* });
*
*/
$apiResult = Event::fire('backend.richeditor.listTypes');
if (is_array($apiResult)) {
foreach ($apiResult as $typeList) {
@ -205,6 +218,28 @@ class RichEditor extends FormWidgetBase
protected function getPageLinks($type)
{
$result = [];
/**
* @event backend.richeditor.getTypeInfo
* Register additional "page link types" to the RichEditor FormWidget
*
* Example usage:
*
* Event::listen('backend.richeditor.getTypeInfo', function ($type) {
* if ($type === 'my-identifier') {
* return [
* 'https://example.com/page1' => 'Page 1',
* 'https://example.com/parent-page' => [
* 'title' => 'Parent Page',
* 'links' => [
* 'https://example.com/child-page' => 'Child Page',
* ],
* ],
* ];
* }
* });
*
*/
$apiResult = Event::fire('backend.richeditor.getTypeInfo', [$type]);
if (is_array($apiResult)) {
foreach ($apiResult as $typeInfo) {

View File

@ -2,12 +2,15 @@
return [
'auth' => [
'title' => 'Admin-Bereich'
'title' => 'Admin-Bereich',
'invalid_login' => 'Die Angaben stimmen nicht mit unseren Aufzeichnungen überein. Überprüfen Sie diese und versuchen Sie es noch einmal.',
],
'field' => [
'invalid_type' => 'Ungültiger Feldtyp :type.',
'options_method_invalid_model' => 'Das Attribut ":field" löst sich nicht zu einen gültigen Model auf. Probiere die options Methode der Model-Klasse :model explicit zu definieren.',
'options_method_not_exists' => 'Die Model-Klasse :model muss eine Methode :method() mit Rückgabe der Werte von ":field" besitzen.',
'options_method_not_exists' => 'Die Modell-Klasse :model muss eine Methode :method() mit Rückgabe der Werte von ":field" besitzen.',
'options_static_method_invalid_value' => "Die statische Methode ':method()' in der Klasse :class hat kein valides Optionsarray zurückgegeben.",
'colors_method_not_exists' => "Die Modellklasse :model muss eine Methode :method() definieren, welche html color (HEX) codes für das ':field' Formularfeld zurückgibt.",
],
'widget' => [
'not_registered' => "Ein Widget namens ':name' wurde nicht registriert",
@ -15,6 +18,11 @@ return [
],
'page' => [
'untitled' => "Unbenannt",
'404' => [
'label' => 'Seite nicht gefunden',
'help' => "Die von Ihnen angeforderte Seite konnte nicht gefunden werden.",
'back_link' => 'Zurück zur vorigen Seite',
],
'access_denied' => [
'label' => "Zugriff verweigert",
'help' => "Sie haben nicht die erforderlichen Berechtigungen, um diese Seite zu sehen.",
@ -28,14 +36,23 @@ return [
],
'partial' => [
'not_found_name' => "Das Partial ':name' wurde nicht gefunden.",
'invalid_name' => 'Ungültiger Partial: :name.',
],
'ajax_handler' => [
'invalid_name' => 'Ungültiger AJAX handler: :name.',
'not_found' => "AJAX handler ':name' wurde nicht gefunden.",
],
'account' => [
'impersonate_confirm' => 'Sind Sie sicher, dass Sie sich als dieser Benutzer anmelden wollen? Sie können zu Ihrem ursprünglichen Zustand zurückkehren, indem Sie sich abmelden.',
'impersonate_success' => 'Sie sind jetzt als dieser Benutzer angemeldet',
'signed_in_as' => 'Angemeldet als :full_name',
'sign_out' => 'Abmelden',
'login' => 'Anmelden',
'reset' => 'Zurücksetzen',
'restore' => 'Wiederherstellen',
'login_placeholder' => 'Benutzername',
'password_placeholder' => 'Passwort',
'remember_me' => 'Angemeldet bleiben',
'forgot_password' => "Passwort vergessen?",
'enter_email' => "Bitte E-Mail-Adresse eingeben",
'enter_login' => "Bitte Benutzernamen eingeben",
@ -112,6 +129,8 @@ return [
'last_name' => 'Nachname',
'full_name' => 'Kompletter Name',
'email' => 'E-Mail',
'role_field' => 'Rolle',
'role_comment' => 'Rollen definieren Benutzerberechtigungen, die auf Benutzerebene auf der Registerkarte Berechtigungen überschrieben werden können.',
'groups' => 'Gruppen',
'groups_comment' => 'Geben Sie hier die Gruppenzugehörigkeit an',
'avatar' => 'Avatar',
@ -148,9 +167,25 @@ return [
'return' => 'Zurück zur Gruppen-Übersicht',
'users_count' => 'Benutzer',
],
'role' => [
'name' => 'Rolle',
'name_field' => 'Name',
'name_comment' => 'Der Name wird in der Rollenliste auf dem Administratorformular angezeigt.',
'description_field' => 'Beschreibung',
'code_field' => 'Code',
'code_comment' => 'Geben Sie einen eindeutigen Code an, wenn Sie mit der API auf das Rollenobjekt zugreifen möchten.',
'menu_label' => 'Rollen verwalten',
'list_title' => 'Rollen verwalten',
'new' => 'Neue Rolle',
'delete_confirm' => 'Diese Administratorrolle löschen?',
'return' => 'Zurück zur Rollenliste',
'users_count' => 'Benutzer',
],
'preferences' => [
'not_authenticated' => 'Zum Speichern oder Anzeigen dieser Einstellungen liegt kein Nutzerkonto vor'
]
],
'trashed_hint_title' => 'Dieses Konto wurde gelöscht.',
'trashed_hint_desc' => 'Dieses Konto wurde gelöscht und kann nicht mehr angemeldet werden. Um es wiederherzustellen, klicken Sie auf das Symbol "Benutzer wiederherstellen" unten rechts',
],
'list' => [
'default_title' => 'Auflisten',
@ -195,6 +230,11 @@ return [
'remove_confirm' => 'Sind Sie sicher?',
'remove_file' => 'Datei entfernen',
],
'repeater' => [
'add_new_item' => 'Neues Element hinzufügen',
'min_items_failed' => ':name erfordert ein Minimum an :min Elementen, aber es wurden nur :items bereitgestellt',
'max_items_failed' => ':name lässt nur bis zu :max Elemente zu, :items wurden bereitgestellt',
],
'form' => [
'create_title' => "Neuer :name",
'update_title' => "Bearbeite :name",
@ -312,6 +352,8 @@ return [
'permissions' => 'Verzeichnis :name oder ein Unterverzeichnis kann nicht von PHP beschrieben werden. Bitte setzen Sie die korrekten Rechte für den Webserver in diesem Verzeichnis.',
'extension' => 'Die PHP Erweiterung :name ist nicht installiert. Bitte installieren Sie diese Library und aktivieren Sie die Erweiterung.',
'plugin_missing' => 'Das Plugin :name hat eine Abhängigkeit die nicht installiert ist. Bitte installieren Sie alle benötigten Plugins.',
'debug' => 'Der Debug-Modus ist aktiviert. Dies wird für Produktionsinstallationen nicht empfohlen.',
'decompileBackendAssets' => 'Assets im Backend sind derzeit dekompiliert. Dies wird für Produktionsinstallationen nicht empfohlen.',
],
'editor' => [
'menu_label' => 'Editor Einstellungen',
@ -367,6 +409,8 @@ return [
'minimal' => 'Minimal',
'full' => 'Vollständig',
],
'paragraph_formats' => 'Absatzformatierungen',
'paragraph_formats_comment' => 'Die Optionen, welche in der Dropdown-Liste für Absatzformatierungen angezeigt werden.',
],
'tooltips' => [
'preview_website' => 'Vorschau der Webseite'
@ -386,6 +430,8 @@ return [
'brand' => 'Brand',
'logo' => 'Logo',
'logo_description' => 'Lade ein eigenes Logo hoch, das im Backend verwendet werden soll.',
'favicon' => 'Favicon',
'favicon_description' => 'Laden Sie ein benutzerdefiniertes Favicon zur Verwendung im Back-End hoch',
'app_name' => 'App-Name',
'app_name_description' => 'Dieser Name wird als Titel des Backends angezeigt.',
'app_tagline' => 'App-Tagline',
@ -399,6 +445,7 @@ return [
'navigation' => 'Navigation',
'menu_mode' => 'Menustyles',
'menu_mode_inline' => 'Inline',
'menu_mode_inline_no_icons' => 'Inline (ohne Icons)',
'menu_mode_tile' => 'Tiles',
'menu_mode_collapsed' => 'Collapsed'
],
@ -503,6 +550,7 @@ return [
],
'permissions' => [
'manage_media' => 'Medien verwalten',
'allow_unsafe_markdown' => 'Unsicheres Markdown verwenden (kann Javascript enthalten)',
],
'mediafinder' => [
'label' => 'Media Finder',
@ -534,7 +582,12 @@ return [
'multiple_selected' => 'Mehrere Dateien ausgewählt.',
'uploading_file_num' => 'Lade :number Datei(en)...',
'uploading_complete' => 'Upload vollständig',
'uploading_error' => 'Upload fehlgeschlagen',
'type_blocked' => 'Der verwendete Dateityp ist aus Sicherheitsgründen gesperrt.',
'order_by' => 'Sortieren nach',
'direction' => 'Direction',
'direction_asc' => 'Aufsteigend',
'direction_desc' => 'Absteigend',
'folder' => 'Ordner',
'no_files_found' => 'Keine entsprechenden Dateien gefunden.',
'delete_empty' => 'Bitte Wählen Sie Dateien zum Löschen aus.',
@ -557,11 +610,11 @@ return [
'restore' => 'Alle Änderungen rückgängig machen',
'resize' => 'Größe anpassen...',
'selection_mode_normal' => 'Normal',
'selection_mode_fixed_ratio' => 'Fixes Verhältnis',
'selection_mode_fixed_size' => 'Fixe Größe',
'selection_mode_fixed_ratio' => 'Festes Verhältnis',
'selection_mode_fixed_size' => 'Feste Größe',
'height' => 'Höhe',
'width' => 'Breite',
'selection_mode' => 'Selection mode',
'selection_mode' => 'Auswahlmodus',
'resize_image' => 'Bildgröße anpassen',
'image_size' => 'Dimensionen:',
'selected_size' => 'Ausgewählt:'

View File

@ -9,6 +9,7 @@ return [
'invalid_type' => 'Invalid field type used :type.',
'options_method_invalid_model' => "The attribute ':field' does not resolve to a valid model. Try specifying the options method for model class :model explicitly.",
'options_method_not_exists' => "The model class :model must define a method :method() returning options for the ':field' form field.",
'options_static_method_invalid_value' => "The static method ':method()' on :class did not return a valid options array.",
'colors_method_not_exists' => "The model class :model must define a method :method() returning html color HEX codes for the ':field' form field.",
],
'widget' => [
@ -372,6 +373,7 @@ return [
'editor' => [
'menu_label' => 'Editor settings',
'menu_description' => 'Customize the global editor preferences, such as font size and color scheme.',
'preview' => 'Preview',
'font_size' => 'Font size',
'tab_size' => 'Tab size',
'use_hard_tabs' => 'Indent using tabs',

View File

@ -228,7 +228,7 @@ return [
'preview_no_record_message' => 'Nessun record selezionato.',
'select' => 'Seleziona',
'select_all' => 'seleziona tutto',
'select_none' => 'non selezionare niente',
'select_none' => 'deseleziona tutto',
'select_placeholder' => 'seleziona',
'insert_row' => 'Inserisci riga',
'insert_row_below' => 'Inserisci riga sotto',

View File

@ -9,6 +9,7 @@ return [
'invalid_type' => 'Ongeldig type veld: :type.',
'options_method_invalid_model' => "Het attribuut ':field' levert geen geldig model op. Probeer de opties methode expliciet te specifieren voor modelklasse :model.",
'options_method_not_exists' => 'De modelklasse :model moet de methode :method() definiëren met daarin opties voor het veld ":field".',
'options_static_method_invalid_value' => "De statische methode ':method()' in :class leverde geen geldige array met opties op.",
'colors_method_not_exists' => 'De modelklasse :model moet de methode :method() definiëren met daarin html HEX kleurcodes voor het veld ":field".',
],
'widget' => [
@ -372,6 +373,7 @@ return [
'editor' => [
'menu_label' => 'Editor instellingen',
'menu_description' => 'Beheer editor instellingen, zoals lettergrootte en kleurschema.',
'preview' => 'Voorbeeldweergave',
'font_size' => 'Lettergrootte',
'tab_size' => 'Tab grootte',
'use_hard_tabs' => 'Inspringen met tabs',
@ -406,6 +408,7 @@ return [
'label' => 'Label',
'class_name' => 'Class naam',
'markup_tags' => 'Opmaak HTML-tags',
'markup_tag' => 'Opmaak HTML-tag',
'allowed_empty_tags' => 'Toegestane lege HTML-tags',
'allowed_empty_tags_comment' => 'Een lijst van HTML-tags die niet worden verwijderd als ze leeg zijn.',
'allowed_tags' => 'Toegestane HTML-tags',
@ -416,6 +419,7 @@ return [
'remove_tags_comment' => 'Een lijst van HTML-tags die samen met hun inhoud worden verwijderd.',
'line_breaker_tags' => 'Line breaker tags',
'line_breaker_tags_comment' => 'Een lijst van HTML-tags waartussen een line breaker element wordt geplaatst.',
'toolbar_options' => 'Toolbar opties',
'toolbar_buttons' => 'Toolbar knoppen',
'toolbar_buttons_comment' => 'De toolbar knoppen die standaard getoond worden door de Rich Editor.',
'toolbar_buttons_preset' => 'Voeg preset toe voor toolbar knoppen:',
@ -424,9 +428,11 @@ return [
'minimal' => 'Minimaal',
'full' => 'Volledig',
],
'paragraph_formats' => 'Paragraaf formaten',
'paragraph_formats_comment' => 'De opties die in de "Paragraaf formaat" lijst zullen verschijnen.',
],
'tooltips' => [
'preview_website' => 'Voorvertoning website',
'preview_website' => 'Voorbeeldweergave website',
],
'mysettings' => [
'menu_label' => 'Mijn instellingen',

View File

@ -6,10 +6,11 @@ return [
'invalid_login' => 'Podatki, ki ste jih vnesli, se ne ujemajo z našimi zapisi. Prosimo, ponovno preverite podatke in poskusite znova.',
],
'field' => [
'invalid_type' => 'Uporabljen je neveljaven tip polja :type.',
'options_method_invalid_model' => "Atribut ':field' ne ustreza veljavnemu modelu. Poskusite natančno določiti možnosti metode za model :model.",
'options_method_not_exists' => "Model :model mora vsebovati metodo :method(), ki vrača možnosti za polje ':field' na obrazcu.",
'colors_method_not_exists' => "Model :model mora vsebovati metodo :method(), ki vrača HTML barvne kode v HEX formatu za polje ':field' na obrazcu.",
'invalid_type' => 'Uporabljen je neveljaven tip polja :type.',
'options_method_invalid_model' => "Atribut ':field' ne ustreza veljavnemu modelu. Poskusite natančno določiti možnosti metode za model :model.",
'options_method_not_exists' => "Model :model mora vsebovati metodo :method(), ki vrača možnosti za polje ':field' na obrazcu.",
'options_static_method_invalid_value' => "Statična metoda ':method()' v razredu :class ni vrnila veljavnih možnosti.",
'colors_method_not_exists' => "Model :model mora vsebovati metodo :method(), ki vrača HTML barvne kode v HEX formatu za polje ':field' na obrazcu.",
],
'widget' => [
'not_registered' => "Ime vtičnika ':name' ni bilo registrirano.",
@ -48,6 +49,10 @@ return [
'impersonate_working' => 'Impersoniram...',
'impersonating' => 'Impersonacija uporabnika :full_name',
'stop_impersonating' => 'Prekliči impersonacijo',
'unsuspend' => 'Odsuspendiraj',
'unsuspend_confirm' => 'Ali ste prepričani, da želite odsuspendirati tega uporabnika?',
'unsuspend_success' => 'Uporabnik je odsuspendiran.',
'unsuspend_working' => 'Odsuspendiram...',
'signed_in_as' => 'Prijavljeni ste kot :full_name',
'sign_out' => 'Odjava',
'login' => 'Prijava',
@ -402,6 +407,7 @@ return [
'label' => 'Opis',
'class_name' => 'Oznaka razreda',
'markup_tags' => 'Označevalne oznake',
'markup_tag' => 'Označevalna oznaka',
'allowed_empty_tags' => 'Dovoljene prazne oznake',
'allowed_empty_tags_comment' => 'Seznam oznak, ki niso odstranjene, če v njih ni vsebine.',
'allowed_tags' => 'Dovoljene oznake',
@ -412,6 +418,7 @@ return [
'remove_tags_comment' => 'Seznam oznak, ki so odstranjene skupaj z njihovo vsebino.',
'line_breaker_tags' => 'Oznake prekinitve vrstic',
'line_breaker_tags_comment' => 'Seznam oznak, ki se uporabljajo za postavitev elementa prekinitve med vrstice.',
'toolbar_options' => 'Nastavitve orodne vrstice',
'toolbar_buttons' => 'Gumbi orodne vrstice',
'toolbar_buttons_comment' => 'Gumbi orodne vrstice, ki se privzeto prikažejo v urejevalniku. [fullscreen, bold, italic, underline, strikeThrough, subscript, superscript, fontFamily, fontSize, |, color, emoticons, inlineStyle, paragraphStyle, |, paragraphFormat, align, formatOL, formatUL, outdent, indent, quote, insertHR, -, insertLink, insertImage, insertVideo, insertAudio, insertFile, insertTable, undo, redo, clearFormatting, selectAll, html]',
'toolbar_buttons_preset' => 'Vstavite prednastavljeno konfiguracijo gumbov orodne vrstice:',
@ -420,6 +427,8 @@ return [
'minimal' => 'Minimalno',
'full' => 'Polno',
],
'paragraph_formats' => 'Formati odstavkov',
'paragraph_formats_comment' => 'Možnosti, ki se prikažejo v spustnem seznamu Format odstavka.',
],
'tooltips' => [
'preview_website' => 'Ogled spletne strani',
@ -470,7 +479,7 @@ return [
],
'access_log' => [
'hint' => 'Ta dnevnik beleži seznam uspešnih prijav administratorjev. Zapisi se hranijo :days dni.',
'menu_label' => 'Dnevnik dostopa',
'menu_label' => 'Dnevnik dostopov',
'menu_description' => 'Prikaz seznama uspešnih prijav administratorjev.',
'id' => 'ID',
'created_at' => 'Datum in čas',
@ -558,11 +567,12 @@ return [
'iso_8859_15' => 'ISO-8859-15 (Latin-9, Western European revision with euro sign)',
'windows_1250' => 'Windows-1250 (CP1250, Central and Eastern European)',
'windows_1251' => 'Windows-1251 (CP1251)',
'windows_1252' => 'Windows-1252 (CP1252)'
]
'windows_1252' => 'Windows-1252 (CP1252)',
],
],
'permissions' => [
'manage_media' => 'Nalaganje in upravljanje z media vsebinami - slike, video posnetki, zvočni posnetki, dokumenti',
'manage_media' => 'Nalaganje in upravljanje z media vsebinami - slike, video posnetki, zvočni posnetki, dokumenti',
'allow_unsafe_markdown' => 'Dovoli uporabo nevarnih označb (lahko vključi Javascript)',
],
'mediafinder' => [
'label' => 'Media brskalnik',

View File

@ -52,6 +52,7 @@ return [
'widget_width' => '寬度',
'full_width' => '全部寬度',
'add_widget' => '新增元件',
'manage_widgets' => '管理元件',
'widget_inspector_title' => '元件設定',
'widget_inspector_description' => '設定報表元件',
'widget_columns_label' => '寬度 :columns',
@ -62,6 +63,12 @@ return [
'widget_new_row_description' => '把元件放到新列',
'widget_title_label' => '元件標題',
'widget_title_error' => '需要元件標題',
'reset_layout' => '重置版面',
'reset_layout_confirm' => '確定重置為預設版面?',
'reset_layout_success' => '版面已重置。',
'make_default' => '設定為預設',
'make_default_confirm' => '確定將此版面設定為預設?',
'make_default_success' => '已設定此版面為預設。',
'status' => [
'widget_title_default' => '系統狀態',
'update_available' => '{0} 更新可用!|{1} 更新可用!|[2,Inf] 更新可用!'
@ -170,7 +177,7 @@ return [
'field_off' => '關',
'field_on' => '開',
'add' => '增加',
'apply' => '應用',
'apply' => '確定',
'cancel' => '取消',
'close' => '關閉',
'confirm' => '確認',
@ -297,7 +304,9 @@ return [
'email' => 'Email'
],
'filter' => [
'all' => '全部'
'all' => '全部',
'date_all' => '全部區間',
'number_all' => '全部數目',
],
'permissions' => [
'manage_media' => 'Upload and manage media contents - images, videos, sounds, documents'
@ -316,10 +325,10 @@ return [
'display' => '顯示',
'filter_everything' => '所有',
'filter_images' => '圖片',
'filter_video' => '視頻',
'filter_audio' => '音',
'filter_video' => '影片',
'filter_audio' => '音',
'filter_documents' => '文檔',
'library' => '庫',
'library' => '媒體庫',
'size' => '大小',
'title' => '標題',
'last_modified' => '最近修改',
@ -332,7 +341,7 @@ return [
'multiple_selected' => '多選.',
'uploading_file_num' => '上傳 :number 檔案...',
'uploading_complete' => '上傳完畢',
'order_by' => '排',
'order_by' => '排列方式',
'folder' => '檔案夾',
'no_files_found' => '沒找到您請求的檔案.',
'delete_empty' => '請選擇刪除項.',
@ -362,6 +371,9 @@ return [
'selection_mode' => '選擇模式',
'resize_image' => '調整圖片',
'image_size' => '圖片大小:',
'selected_size' => '選中:'
'selected_size' => '選中:',
'direction' => '順序',
'direction_asc' => '升冪',
'direction_desc' => '降冪',
]
];

View File

@ -9,6 +9,6 @@
<?php endif ?>
<?php if (EditorSetting::isConfigured()): ?>
<style>
<?= EditorSetting::renderCss() ?>
<?= strip_tags(EditorSetting::renderCss()) ?>
</style>
<?php endif ?>

View File

@ -49,15 +49,24 @@
</div>
<div class="toolbar-item toolbar-item-account">
<ul class="mainmenu-toolbar">
<li class="mainmenu-preview with-tooltip">
<a
href="<?= Url::to('/') ?>"
target="_blank"
rel="noopener noreferrer"
title="<?= e(trans('backend::lang.tooltips.preview_website')) ?>">
<i class="icon-crosshairs"></i>
</a>
</li>
<?php foreach (BackendMenu::listQuickActionItems() as $item): ?>
<li class="mainmenu-quick-action with-tooltip">
<a
href="<?= $item->url ?>"
title="<?= e(trans($item->label)) ?>"
<?= Html::attributes($item->attributes) ?>
>
<?php if ($item->iconSvg): ?>
<img
src="<?= Url::asset($item->iconSvg) ?>"
class="svg-icon" loading="lazy" width="20" height="20" />
<?php endif ?>
<i class="<?= $item->iconSvg ? 'svg-replace' : null ?> <?= $item->icon ?>"></i>
</a>
</li>
<?php endforeach ?>
<li class="mainmenu-account with-tooltip">
<a
href="javascript:;" onclick="$.oc.layout.toggleAccountMenu(this)"

View File

@ -48,8 +48,7 @@ class AccessLog extends Model
$records = static::where('user_id', $user->id)
->orderBy('created_at', 'desc')
->limit(2)
->get()
;
->get();
if (!count($records)) {
return null;

View File

@ -193,3 +193,31 @@ div.control-componentlist {
.nav.selector-group li.active {
border-left-color: @brand-secondary;
}
//
// Fancy breadcrumb
//
body.breadcrumb-fancy .control-breadcrumb,
.control-breadcrumb.breadcrumb-fancy {
background-color: mix(black, saturate(@brand-secondary, 20%), 16%);
li {
background-color: mix(black, saturate(@brand-secondary, 20%), 31%);
&:last-child {
background-color: mix(black, saturate(@brand-secondary, 20%), 16%);
&::before {
border-left-color: mix(black, saturate(@brand-secondary, 20%), 16%);
}
}
&::after {
border-left-color: mix(black, saturate(@brand-secondary, 20%), 31%);
}
&:not(:last-child)::before {
border-left-color: @brand-secondary;
}
}
}

View File

@ -21,6 +21,7 @@ tabs:
editor_preview:
type: partial
label: backend::lang.editor.preview
tab: backend::lang.backend_preferences.code_editor
path: field_editor_preview

View File

@ -1283,6 +1283,7 @@ class Form extends WidgetBase
{
/*
* Advanced usage, supplied options are callable
* [\Path\To\Class, methodName]
*/
if (is_array($fieldOptions) && is_callable($fieldOptions)) {
$fieldOptions = call_user_func($fieldOptions, $this, $field);
@ -1328,6 +1329,22 @@ class Form extends WidgetBase
* Field options are an explicit method reference
*/
elseif (is_string($fieldOptions)) {
// \Path\To\Class::staticMethodOptions
if (str_contains($fieldOptions, '::')) {
$options = explode('::', $fieldOptions);
if (count($options) === 2 && class_exists($options[0]) && method_exists($options[0], $options[1])) {
$result = $options[0]::{$options[1]}();
if (!is_array($result)) {
throw new ApplicationException(Lang::get('backend::lang.field.options_static_method_invalid_value', [
'class' => $options[0],
'method' => $options[1]
]));
}
return $result;
}
}
// $model->{$fieldOptions}()
if (!$this->objectMethodExists($this->model, $fieldOptions)) {
throw new ApplicationException(Lang::get('backend::lang.field.options_method_not_exists', [
'model' => get_class($this->model),

View File

@ -1,11 +1,15 @@
<?php namespace Cms;
use App;
use Url;
use Lang;
use File;
use Event;
use Backend;
use BackendMenu;
use BackendAuth;
use Backend\Models\UserRole;
use Cms\Classes\Theme as CmsTheme;
use Backend\Classes\WidgetManager;
use October\Rain\Support\ModuleServiceProvider;
use System\Classes\SettingsManager;
@ -54,6 +58,10 @@ class ServiceProvider extends ModuleServiceProvider
$this->bootMenuItemEvents();
$this->bootRichEditorEvents();
if (App::runningInBackend()) {
$this->bootBackendLocalization();
}
}
/**
@ -167,6 +175,19 @@ class ServiceProvider extends ModuleServiceProvider
]
]
]);
$manager->registerQuickActions('October.Cms', [
'preview' => [
'label' => 'backend::lang.tooltips.preview_website',
'icon' => 'icon-crosshairs',
'url' => Url::to('/'),
'order' => 10,
'attributes' => [
'target' => '_blank',
'rel' => 'noopener noreferrer',
],
],
]);
});
}
@ -284,6 +305,20 @@ class ServiceProvider extends ModuleServiceProvider
});
}
/**
* Boots localization from an active theme for backend items.
*/
protected function bootBackendLocalization()
{
$theme = CmsTheme::getActiveTheme();
$langPath = $theme->getPath() . '/lang';
if (File::isDirectory($langPath)) {
Lang::addNamespace('themes.' . $theme->getId(), $langPath);
}
}
/**
* Registers events for menu items.
*/

View File

@ -1,6 +1,7 @@
<?php namespace Cms\Classes;
use Lang;
use BackendAuth;
use ApplicationException;
use October\Rain\Filesystem\Definitions as FileDefinitions;
@ -197,6 +198,12 @@ class Page extends CmsCompoundObject
}
$page = self::loadCached($theme, $item->reference);
// Remove hidden CMS pages from menus when backend user is logged out
if ($page && $page->is_hidden && !BackendAuth::getUser()) {
return;
}
$controller = Controller::getController() ?: new Controller;
$pageUrl = $controller->pageUrl($item->reference, [], false);

View File

@ -103,8 +103,11 @@ class ThemeOptions extends Controller
/**
* Default to the active theme if user doesn't have access to manage all themes
*
* @param string $dirName
* @return string
*/
protected function getDirName($dirName = null)
protected function getDirName(string $dirName = null)
{
/*
* Only the active theme can be managed without this permission

View File

@ -2,12 +2,16 @@
use October\Rain\Support\Facade;
/**
* @method static string url(string $path = null)
*
* @see \Cms\Helpers\Cms
*/
class Cms extends Facade
{
/**
* Get the registered name of the component.
*
* @see \Cms\Helpers\Cms
* @return string
*/
protected static function getFacadeAccessor()

View File

@ -10,12 +10,24 @@ return [
'invalid_file_extension'=>'Ungültige Dateiendung: :invalid. Erlaubt sind: :allowed.',
'error_deleting' => 'Fehler beim Löschen der Template-Datei ":name".',
'delete_success' => 'Templates wurden erfolgreich gelöscht: :count.',
'file_name_required' => 'Ein Dateiname ist erforderlich.'
'file_name_required' => 'Ein Dateiname ist erforderlich.',
'safe_mode_enabled' => 'Der abgesicherte Modus ist derzeit aktiviert. Die Bearbeitung des PHP-Codes von CMS-Templates ist deaktiviert. Um den abgesicherten Modus zu deaktivieren, setzen Sie den Konfigurationswert `cms.enableSafeMode` auf `false`.',
],
'dashboard' => [
'active_theme' => [
'widget_title_default' => 'Website',
'online' => 'Online',
'maintenance' => 'In der Wartung',
'manage_themes' => 'Themes Verwalten',
'customize_theme' => 'Theme anpassen',
],
],
'theme' => [
'not_found_name' => "Das Theme ':name' konnte nicht gefunden werden.",
'by_author' => 'Von :name',
'active' => [
'not_set' => "Aktives Theme nicht definiert",
'not_found' => 'Aktives Theme wurde nicht gefunden.',
],
'edit' => [
'not_set' => "Edit Theme nicht definiert",
@ -35,6 +47,8 @@ return [
'homepage_placeholder' => 'Webseiten URL',
'code_label' => 'Code',
'code_placeholder' => 'Ein einzigartiger Code genutzt bei der Weiterverbreitung dieses Themes',
'preview_image_label' => 'Bildvorschau',
'preview_image_placeholder' => 'Der Pfad des Theme-Vorschaubildes.',
'dir_name_label' => 'Verzeichnisname',
'dir_name_create_label' => 'Name des Zielverzeichnisses',
'theme_label' => 'Theme',
@ -84,7 +98,8 @@ return [
'settings_menu' => 'Wartungsmodus',
'settings_menu_description' => 'Konfigurieren Sie den Wartungsmodus.',
'is_enabled' => 'Wartungsmodus aktivieren',
'is_enabled_comment' => 'Sobald aktiviert, werden Besucher die unten ausgewählte Seite sehen.'
'is_enabled_comment' => 'Sobald aktiviert, werden Besucher die unten ausgewählte Seite sehen.',
'hint' => 'Im Wartungsmodus wird die Wartungsseite für Besucher angezeigt, die nicht im Back-End-Bereich angemeldet sind.',
],
'page' => [
'not_found_name' => "Die Seite ':name' konnte nicht gefunden werden",
@ -97,16 +112,22 @@ return [
'help' => "Entschuldigung, ein Fehler trat auf, sodass die gewünschte Seite nicht angezeigt werden kann.",
],
'menu_label' => 'Seiten',
'unsaved_label' => 'Ungespeicherte Seite(n)',
'no_list_records' => 'Keine Seiten gefunden',
'new' => 'Neue Seite',
'invalid_url' => 'Ungültiges URL-Format. Die URL muss mit einem Slash beginnen und darf nur Ziffern, lateinische Zeichen und die folgenden Symbole beinhalten: ._-[]:?|/+*^$',
'delete_confirm_multiple' => 'Wollen Sie die ausgewählten Seiten wirklich löschen?',
'delete_confirm_single' => 'Wollen Sie diese Seite wirklich löschen?',
'no_layout' => '-- Kein Layout --'
'no_layout' => '-- Kein Layout --',
'cms_page' => 'CMS Seite',
'title' => 'Seitentitel',
'url' => 'Seiten-URL',
'file_name' => 'Seiten-Dateiname',
],
'layout' => [
'not_found_name' => "Das Layout ':name' wurde nicht gefunden",
'menu_label' => 'Layouts',
'unsaved_label' => 'Ungespeicherte(s) Layout(s)',
'no_list_records' => 'Keine Layouts gefunden',
'new' => 'Neues Layout',
'delete_confirm_multiple' => 'Wollen Sie die ausgewählten Layouts wirklich löschen?',
@ -116,6 +137,7 @@ return [
'not_found_name' => "Das Partial ':name' wurde nicht gefunden.",
'invalid_name' => "Ungültiger Partial-Name: :name.",
'menu_label' => 'Partials',
'unsaved_label' => 'Ungespeicherte(s) Partial(s)',
'no_list_records' => 'Keine Partials gefunden',
'delete_confirm_multiple' => 'Wollen Sie die ausgewählten Partials wirklich löschen?',
'delete_confirm_single' => 'Wollen Sie das ausgewählte Partial wirklich löschen?',
@ -124,6 +146,7 @@ return [
'content' => [
'not_found_name' => "Die Inhaltsdatei ':name' wurde nicht gefunden.",
'menu_label' => 'Inhalt',
'unsaved_label' => 'Ungespeicherter Inhalt',
'no_list_records' => 'Keine Inhaltsdateien gefunden',
'delete_confirm_multiple' => 'Wollen Sie die ausgewählten Inhalte und Verzeichnisse wirklich löschen?',
'delete_confirm_single' => 'Wollen Sie diese Inhaltsdatei wirklich löschen?',
@ -158,18 +181,34 @@ return [
'hidden' => 'Versteckt',
'hidden_comment' => 'Versteckte Seiten können nur von eingeloggten Backend-Benutzern genutzt werden.',
'enter_fullscreen' => 'In den Vollbildmodus wechseln',
'exit_fullscreen' => 'Vollbildmodus beenden'
'exit_fullscreen' => 'Vollbildmodus beenden',
'open_searchbox' => 'Suchfeld öffnen',
'close_searchbox' => 'Suchfeld schließen',
'open_replacebox' => 'Ersetzen-Feld öffnen',
'close_replacebox' => 'Ersetzen-Feld schließen',
'commit' => 'Commit',
'reset' => 'Zurücksetzen',
'commit_confirm' => 'Sind Sie sicher, dass Sie Ihre Änderungen an dieser Datei auf das Dateisystem übertragen wollen? Dies wird die bestehende Datei auf dem Dateisystem überschreiben',
'reset_confirm' => 'Sind Sie sicher, dass Sie diese Datei auf die Kopie zurücksetzen wollen, die sich auf dem Dateisystem befindet? Dadurch wird sie vollständig durch die Datei ersetzt, die sich auf dem Dateisystem befindet.',
'committing' => 'Committing',
'resetting' => 'Zurücksetzen',
'commit_success' => 'Der :type wurde auf das Dateisystem übertragen',
'reset_success' => 'Der :type wurde auf die Dateisystemversion zurückgesetzt',
],
'asset' => [
'menu_label' => "Assets",
'unsaved_label' => 'Ungespeicherte(s) Asset(s)',
'drop_down_add_title' => 'Hinzufügen...',
'drop_down_operation_title' => 'Aktion...',
'upload_files' => 'Datei(en) hochladen',
'create_file' => 'Datei erstellen',
'create_directory' => 'Verzeichnis erstellen',
'directory_popup_title' => 'Neues Verzeichnis',
'directory_name' => 'Verzeichnisname',
'rename' => 'Umbenennen',
'delete' => 'Löschen',
'move' => 'Verschieben',
'select' => 'Auswählen',
'new' => 'Neue Datei',
'rename_popup_title' => 'Umbenennen',
'rename_new_name' => 'Neuer Name',
@ -196,6 +235,8 @@ return [
'error_moving_file' => 'Fehler beim Verschieben der Datei :file',
'error_moving_directory' => 'Fehler beim Verschieben des Verzeichnisses :dir',
'error_deleting_directory' => 'Fehler beim Löschen des Originalverzeichnisses :dir',
'no_list_records' => 'Keine Dateien gefunden',
'delete_confirm' => 'Ausgewählte Dateien oder Verzeichnisse löschen?',
'path' => 'Pfad'
],
'component' => [
@ -208,12 +249,17 @@ return [
'invalid_request' => "Aufgrund ungültiger Komponentendaten kann das Template nicht gespeichert werden.",
'no_records' => 'Keine Komponenten gefunden',
'not_found' => "Die Komponente ':name' wurde nicht gefunden.",
'no_default_partial' => "Diese Komponente hat keinen 'default' Partial",
'method_not_found' => "Die Komponente ':name' enthält keine Methode mit Namen ':method'.",
'soft_component_description' => 'Diese Komponente fehlt, aber Sie ist optional.',
],
'template' => [
'invalid_type' => "Unbekannter Template-Typ.",
'not_found' => "Das angeforderte Template wurde nicht gefunden.",
'saved'=> "Das Template wurde erfolgreich gespeichert."
'saved'=> "Das Template wurde erfolgreich gespeichert.",
'no_list_records' => 'Keine Einträge gefunden',
'delete_confirm' => 'Ausgewählte Templates löschen?',
'order_by' => 'Sortieren nach',
],
'permissions' => [
'name' => 'Cms',
@ -223,5 +269,35 @@ return [
'manage_layouts' => 'Layouts verwalten',
'manage_partials' => 'Partials verwalten',
'manage_themes' => 'Themes verwalten',
'manage_theme_options' => 'Konfigurieren Sie Anpassungsoptionen für das aktive Theme',
],
'theme_log' => [
'hint' => 'Dieses Protokoll zeigt alle Änderungen an, die von Administratoren im Back-End-Bereich am Theme vorgenommen wurden.',
'menu_label' => 'Theme Protokoll',
'menu_description' => 'Zeigen Sie die am aktiven Theme vorgenommenen Änderungen an.',
'empty_link' => 'Leeres Theme Protokoll',
'empty_loading' => 'Leeren des Theme Protokolls...',
'empty_success' => 'Theme Protokoll',
'return_link' => 'Zurück zum Theme Protokoll',
'id' => 'ID',
'id_label' => 'Protokoll ID',
'created_at' => 'Datum & Zeit',
'user' => 'Benutzer',
'type' => 'Typ',
'type_create' => 'Erstellen',
'type_update' => 'Aktualisieren',
'type_delete' => 'Löschen',
'theme_name' => 'Theme',
'theme_code' => 'Theme Code',
'old_template' => 'Template (Alt)',
'new_template' => 'Template (Neu)',
'template' => 'Template',
'diff' => 'Änderungen',
'old_value' => 'Alter Wert',
'new_value' => 'Neuer Wert',
'preview_title' => 'Templateänderungen',
'template_updated' => 'Template wurde aktualisiert',
'template_created' => 'Template wurde erstellt',
'template_deleted' => 'Template wurde gelöscht',
],
];

View File

@ -11,7 +11,7 @@ return [
'error_deleting' => 'Fout bij het verwijderen van template: ":name". Controleer de schrijfrechten.',
'delete_success' => 'Templates zijn succesvol verwijderd: :count.',
'file_name_required' => 'Het invullen van een bestandsnaam is verplicht.',
'safe_mode_enabled' => 'Beveiligde modus is op dit moment ingeschakeld.',
'safe_mode_enabled' => 'Veilige modus is op dit moment ingeschakeld.',
],
'dashboard' => [
'active_theme' => [
@ -245,11 +245,12 @@ return [
'no_description' => 'Geen beschrijving opgegeven',
'alias' => 'Alias',
'alias_description' => 'Een unieke naam voor dit component voor gebruik in de code van een pagina of layout.',
'validation_message' => 'Een alias voor het component is verplicht en mag alleen bestaan uit letters, cijfers en underscores. De alias moet beginnen met een letter.',
'validation_message' => 'Een alias voor de component is verplicht en mag alleen bestaan uit letters, cijfers en underscores. De alias moet beginnen met een letter.',
'invalid_request' => 'De template kan niet worden opgeslagen vanwege ongeldige componentgegevens.',
'no_records' => 'Geen componenten gevonden',
'not_found' => 'Het component \':name\' is niet gevonden.',
'method_not_found' => 'Het component \':name\' bevat geen \':method\' methode.',
'not_found' => 'De component \':name\' is niet gevonden.',
'no_default_partial' => "De component bevat geen 'standaard' sjabloon",
'method_not_found' => 'De component \':name\' bevat geen \':method\' methode.',
'soft_component' => 'Soft Component',
'soft_component_description' => 'Dit component ontbreekt, maar is optioneel.',
],

View File

@ -46,8 +46,8 @@ return [
'dir_name_create_label' => '目標主題目錄',
'theme_label' => '主題',
'theme_title' => '主題',
'activate_button' => '激活',
'active_button' => '激活',
'activate_button' => '啟用',
'active_button' => '啟用',
'customize_theme' => '自訂主題',
'customize_button' => '自訂',
'duplicate_button' => '複製',
@ -110,7 +110,10 @@ return [
'invalid_url' => '不合法的URL格式. URL可以正斜槓開頭, 包含數字, 拉丁字母和下面的字元: ._-[]:?|/+*^$',
'delete_confirm_multiple' => '真的想要刪除選擇的頁面嗎?',
'delete_confirm_single' => '真的想要刪除這個頁面嗎?',
'no_layout' => '-- 沒有佈局 --'
'no_layout' => '-- 沒有佈局 --',
'title' => '頁面標題',
'url' => '頁面網址',
'file_name' => '頁面檔案名稱',
],
'layout' => [
'not_found_name' => "佈局 ':name' 找不到",
@ -168,8 +171,8 @@ return [
'content' => '內容',
'hidden' => '隱藏',
'hidden_comment' => '隱藏頁面只能被登錄的後台使用者訪問.',
'enter_fullscreen' => '進入全模式',
'exit_fullscreen' => '退出全屏模式'
'enter_fullscreen' => '進入全螢幕模式',
'exit_fullscreen' => '退出全螢幕模式',
],
'asset' => [
'menu_label' => '資源',
@ -181,12 +184,12 @@ return [
'create_directory' => '新建目錄',
'directory_popup_title' => '新目錄',
'directory_name' => '目錄名',
'rename' => '重命名',
'rename' => '重命名',
'delete' => '刪除',
'move' => '移動',
'select' => '選擇',
'new' => '新檔案',
'rename_popup_title' => '重命名',
'rename_popup_title' => '重命名',
'rename_new_name' => '新名稱',
'invalid_path' => '路徑名稱只能包含數字, 拉丁字母和以下字元: _-/',
'error_deleting_file' => '刪除檔案 :name 錯誤.',
@ -195,7 +198,7 @@ return [
'invalid_name' => '名稱只能包含數字, 拉丁字母, 空格和以下字元: _-',
'original_not_found' => '原始檔案或目錄找不到',
'already_exists' => '檔案或目錄已存在',
'error_renaming' => '重命名檔案或目錄錯誤',
'error_renaming' => '重命名檔案或目錄錯誤',
'name_cant_be_empty' => '名稱不能為空',
'too_large' => '上傳的檔案太大. 最大檔案大小是 :max_size',
'type_not_allowed' => '只有下面的檔案類型是允許的: :allowed_types',

View File

@ -27,7 +27,9 @@ class Loader extends LoaderBase implements TwigLoaderInterface
/**
* Sets a CMS object to load the template from.
*
* @param \Cms\Contracts\CmsObject $obj Specifies the CMS object.
* @return void
*/
public function setObject(CmsObject $obj)
{
@ -37,6 +39,9 @@ class Loader extends LoaderBase implements TwigLoaderInterface
/**
* Returns the Twig content string.
* This step is cached internally by Twig.
*
* @param string $name The template name
* @return TwigSource
*/
public function getSourceContext($name)
{
@ -65,6 +70,9 @@ class Loader extends LoaderBase implements TwigLoaderInterface
/**
* Returns the Twig cache key.
*
* @param string $name The template name
* @return string
*/
public function getCacheKey($name)
{
@ -77,6 +85,10 @@ class Loader extends LoaderBase implements TwigLoaderInterface
/**
* Determines if the content is fresh.
*
* @param string $name The template name
* @param mixed $time The time to check against the template
* @return bool
*/
public function isFresh($name, $time)
{
@ -89,6 +101,9 @@ class Loader extends LoaderBase implements TwigLoaderInterface
/**
* Returns the file name of the loaded template.
*
* @param string $name The template name
* @return string
*/
public function getFilename($name)
{
@ -101,6 +116,9 @@ class Loader extends LoaderBase implements TwigLoaderInterface
/**
* Checks that the template exists.
*
* @param string $name The template name
* @return bool
*/
public function exists($name)
{
@ -115,11 +133,12 @@ class Loader extends LoaderBase implements TwigLoaderInterface
* Internal method that checks if the template name matches
* the loaded object, with fallback support to partials.
*
* @param string $name The template name to validate
* @return bool
*/
protected function validateCmsObject($name)
{
if ($name === $this->obj->getFilePath()) {
if ($this->obj && $name === $this->obj->getFilePath()) {
return true;
}
@ -133,18 +152,29 @@ class Loader extends LoaderBase implements TwigLoaderInterface
/**
* Looks up a fallback CMS partial object.
* @return Cms\Classes\Partial
*
* @param string $name The filename to attempt to load a fallback CMS partial for
* @return Cms\Classes\Partial|bool Returns false if a CMS partial can't be found
*/
protected function findFallbackObject($name)
{
// Ignore Laravel views
if (strpos($name, '::') !== false) {
return false;
}
// Check the cache
if (array_key_exists($name, $this->fallbackCache)) {
return $this->fallbackCache[$name];
}
return $this->fallbackCache[$name] = CmsPartial::find($name);
// Attempt to load the path as a CMS Partial object
try {
$partial = CmsPartial::find($name);
} catch (\Exception $e) {
return false;
}
return $this->fallbackCache[$name] = $partial;
}
}

View File

@ -9,6 +9,7 @@ use Backend;
use Request;
use BackendMenu;
use BackendAuth;
use Backend\Models\UserRole;
use Twig\Extension\SandboxExtension;
use Twig\Environment as TwigEnvironment;
use System\Classes\MailManager;
@ -96,6 +97,13 @@ class ServiceProvider extends ModuleServiceProvider
}
}
/*
* Set a default samesite config value for invalid values
*/
if (!in_array(strtolower(Config::get('session.same_site')), ['lax', 'strict', 'none'])) {
Config::set('session.same_site', 'Lax');
}
Paginator::useBootstrapThree();
Paginator::defaultSimpleView('system::pagination.simple-default');
@ -135,7 +143,7 @@ class ServiceProvider extends ModuleServiceProvider
protected function registerPrivilegedActions()
{
$requests = ['/combine/', '@/system/updates', '@/system/install', '@/backend/auth'];
$commands = ['october:up', 'october:update', 'october:version'];
$commands = ['october:up', 'october:update', 'october:env', 'october:version'];
/*
* Requests
@ -420,19 +428,23 @@ class ServiceProvider extends ModuleServiceProvider
$manager->registerPermissions('October.System', [
'system.manage_updates' => [
'label' => 'system::lang.permissions.manage_software_updates',
'tab' => 'system::lang.permissions.name'
'tab' => 'system::lang.permissions.name',
'roles' => UserRole::CODE_DEVELOPER,
],
'system.access_logs' => [
'label' => 'system::lang.permissions.access_logs',
'tab' => 'system::lang.permissions.name'
'tab' => 'system::lang.permissions.name',
'roles' => UserRole::CODE_DEVELOPER,
],
'system.manage_mail_settings' => [
'label' => 'system::lang.permissions.manage_mail_settings',
'tab' => 'system::lang.permissions.name'
'tab' => 'system::lang.permissions.name',
'roles' => UserRole::CODE_DEVELOPER,
],
'system.manage_mail_templates' => [
'label' => 'system::lang.permissions.manage_mail_templates',
'tab' => 'system::lang.permissions.name'
'tab' => 'system::lang.permissions.name',
'roles' => UserRole::CODE_DEVELOPER,
]
]);
});

View File

@ -69,7 +69,8 @@ var fieldElement=$form.find('[name="'+fieldName+'"], [name="'+fieldName+'[]"], [
if(fieldElement.length>0){var _event=jQuery.Event('ajaxInvalidField')
$(window).trigger(_event,[fieldElement.get(0),fieldName,fieldMessages,isFirstInvalidField])
if(isFirstInvalidField){if(!_event.isDefaultPrevented())fieldElement.focus()
isFirstInvalidField=false}}})},handleFlashMessage:function(message,type){},handleRedirectResponse:function(url){window.location.assign(url)},handleUpdateResponse:function(data,textStatus,jqXHR){var updatePromise=$.Deferred().done(function(){for(var partial in data){var selector=(options.update[partial])?options.update[partial]:partial
isFirstInvalidField=false}}})},handleFlashMessage:function(message,type){},handleRedirectResponse:function(url){window.location.assign(url)
$el.trigger('ajaxDone')},handleUpdateResponse:function(data,textStatus,jqXHR){var updatePromise=$.Deferred().done(function(){for(var partial in data){var selector=(options.update[partial])?options.update[partial]:partial
if($.type(selector)=='string'&&selector.charAt(0)=='@'){$(selector.substring(1)).append(data[partial]).trigger('ajaxUpdate',[context,data,textStatus,jqXHR])}
else if($.type(selector)=='string'&&selector.charAt(0)=='^'){$(selector.substring(1)).prepend(data[partial]).trigger('ajaxUpdate',[context,data,textStatus,jqXHR])}
else{$(selector).trigger('ajaxBeforeReplace')

View File

@ -69,7 +69,8 @@ var fieldElement=$form.find('[name="'+fieldName+'"], [name="'+fieldName+'[]"], [
if(fieldElement.length>0){var _event=jQuery.Event('ajaxInvalidField')
$(window).trigger(_event,[fieldElement.get(0),fieldName,fieldMessages,isFirstInvalidField])
if(isFirstInvalidField){if(!_event.isDefaultPrevented())fieldElement.focus()
isFirstInvalidField=false}}})},handleFlashMessage:function(message,type){},handleRedirectResponse:function(url){window.location.assign(url)},handleUpdateResponse:function(data,textStatus,jqXHR){var updatePromise=$.Deferred().done(function(){for(var partial in data){var selector=(options.update[partial])?options.update[partial]:partial
isFirstInvalidField=false}}})},handleFlashMessage:function(message,type){},handleRedirectResponse:function(url){window.location.assign(url)
$el.trigger('ajaxDone')},handleUpdateResponse:function(data,textStatus,jqXHR){var updatePromise=$.Deferred().done(function(){for(var partial in data){var selector=(options.update[partial])?options.update[partial]:partial
if($.type(selector)=='string'&&selector.charAt(0)=='@'){$(selector.substring(1)).append(data[partial]).trigger('ajaxUpdate',[context,data,textStatus,jqXHR])}
else if($.type(selector)=='string'&&selector.charAt(0)=='^'){$(selector.substring(1)).prepend(data[partial]).trigger('ajaxUpdate',[context,data,textStatus,jqXHR])}
else{$(selector).trigger('ajaxBeforeReplace')

View File

@ -288,6 +288,11 @@ if (window.jQuery.request !== undefined) {
*/
handleRedirectResponse: function(url) {
window.location.assign(url)
// Indicate that the AJAX request is finished if we're still on the current page
// so that the loading indicator for redirects that cause a browser to download
// a file instead of leave the page will properly stop.
// @see https://github.com/octobercms/october/issues/5055
$el.trigger('ajaxDone')
},
/*
@ -974,4 +979,4 @@ if (window.jQuery.request !== undefined) {
return sanitize(html)
};
}(window);
}(window);

View File

@ -5,7 +5,7 @@ if ($.oc === undefined) $.oc = {}
if ($.oc.langMessages === undefined) $.oc.langMessages = {}
$.oc.langMessages['zh-tw'] = $.extend(
$.oc.langMessages['zh-tw'] || {},
{"markdowneditor":{"formatting":"Formatting","quote":"Quote","code":"Code","header1":"Header 1","header2":"Header 2","header3":"Header 3","header4":"Header 4","header5":"Header 5","header6":"Header 6","bold":"Bold","italic":"Italic","unorderedlist":"Unordered List","orderedlist":"Ordered List","video":"Video","image":"Image","link":"Link","horizontalrule":"Insert Horizontal Rule","fullscreen":"Full screen","preview":"Preview"},"mediamanager":{"insert_link":"Insert Media Link","insert_image":"Insert Media Image","insert_video":"Insert Media Video","insert_audio":"Insert Media Audio","invalid_file_empty_insert":"Please select file to insert a links to.","invalid_file_single_insert":"Please select a single file.","invalid_image_empty_insert":"Please select image(s) to insert.","invalid_video_empty_insert":"Please select a video file to insert.","invalid_audio_empty_insert":"Please select an audio file to insert."},"alert":{"confirm_button_text":"OK","cancel_button_text":"Cancel","widget_remove_confirm":"Remove this widget?"},"datepicker":{"previousMonth":"Previous Month","nextMonth":"Next Month","months":["January","February","March","April","May","June","July","August","September","October","November","December"],"weekdays":["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],"weekdaysShort":["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]},"colorpicker":{"choose":"Ok"},"filter":{"group":{"all":"all"},"scopes":{"apply_button_text":"Apply","clear_button_text":"Clear"},"dates":{"all":"all","filter_button_text":"Filter","reset_button_text":"Reset","date_placeholder":"Date","after_placeholder":"After","before_placeholder":"Before"},"numbers":{"all":"all","filter_button_text":"Filter","reset_button_text":"Reset","min_placeholder":"Min","max_placeholder":"Max"}},"eventlog":{"show_stacktrace":"Show the stacktrace","hide_stacktrace":"Hide the stacktrace","tabs":{"formatted":"Formatted","raw":"Raw"},"editor":{"title":"Source code editor","description":"Your operating system should be configured to listen to one of these URL schemes.","openWith":"Open with","remember_choice":"Remember selected option for this session","open":"Open","cancel":"Cancel"}}}
{"markdowneditor":{"formatting":"\u683c\u5f0f","quote":"\u5f15\u8a00","code":"\u7a0b\u5f0f\u78bc","header1":"\u6a19\u984c\u4e00","header2":"\u6a19\u984c\u4e8c","header3":"\u6a19\u984c\u4e09","header4":"\u6a19\u984c\u56db","header5":"\u6a19\u984c\u4e94","header6":"\u6a19\u984c\u516d","bold":"\u7c97\u9ad4","italic":"\u659c\u9ad4","unorderedlist":"\u9805\u76ee\u6e05\u55ae","orderedlist":"\u6578\u5b57\u6e05\u55ae","video":"\u5f71\u7247","image":"\u5716\u7247","link":"\u9023\u7d50","horizontalrule":"\u63d2\u5165\u6c34\u5e73\u7dda","fullscreen":"\u5168\u87a2\u5e55","preview":"\u9810\u89bd"},"mediamanager":{"insert_link":"\u63d2\u5165\u5a92\u9ad4\u6ac3\u9023\u7d50","insert_image":"\u63d2\u5165\u5a92\u9ad4\u6ac3\u5716\u7247","insert_video":"\u63d2\u5165\u5a92\u9ad4\u6ac3\u5f71\u7247","insert_audio":"\u63d2\u5165\u5a92\u9ad4\u6ac3\u97f3\u8a0a","invalid_file_empty_insert":"\u8acb\u9078\u64c7\u6a94\u6848\u4ee5\u63d2\u5165\u9023\u7d50\u3002","invalid_file_single_insert":"\u8acb\u9078\u64c7\u4e00\u500b\u6a94\u6848\u3002","invalid_image_empty_insert":"\u8acb\u9078\u64c7\u63d2\u5165\u7684\u5716\u7247\u3002","invalid_video_empty_insert":"\u8acb\u9078\u64c7\u63d2\u5165\u7684\u5f71\u7247\u3002","invalid_audio_empty_insert":"\u8acb\u9078\u64c7\u63d2\u5165\u7684\u97f3\u8a0a\u3002"},"alert":{"confirm_button_text":"\u78ba\u8a8d","cancel_button_text":"\u53d6\u6d88","widget_remove_confirm":"\u78ba\u5b9a\u79fb\u9664\u6b64\u5143\u4ef6\uff1f"},"datepicker":{"previousMonth":"\u4e0a\u500b\u6708","nextMonth":"\u4e0b\u500b\u6708","months":["\u4e00\u6708","\u4e8c\u6708","\u4e09\u6708","\u56db\u6708","\u4e94\u6708","\u516d\u6708","\u4e03\u6708","\u516b\u6708","\u4e5d\u6708","\u5341\u6708","\u5341\u4e00\u6708","\u5341\u4e8c\u6708"],"weekdays":["\u661f\u671f\u65e5","\u661f\u671f\u4e00","\u661f\u671f\u4e8c","\u661f\u671f\u4e09","\u661f\u671f\u56db","\u661f\u671f\u4e94","\u661f\u671f\u516d"],"weekdaysShort":["\u9031\u65e5","\u9031\u4e00","\u9031\u4e8c","\u9031\u4e09","\u9031\u56db","\u9031\u4e94","\u9031\u516d"]},"colorpicker":{"choose":"\u78ba\u5b9a"},"filter":{"group":{"all":"\u5168\u90e8"},"scopes":{"apply_button_text":"\u78ba\u5b9a","clear_button_text":"\u6e05\u9664"},"dates":{"all":"\u5168\u90e8","filter_button_text":"\u7be9\u9078","reset_button_text":"\u91cd\u7f6e","date_placeholder":"\u65e5\u671f","after_placeholder":"\u5728\u6b64\u4e4b\u5f8c","before_placeholder":"\u5728\u6b64\u4e4b\u524d"},"numbers":{"all":"\u5168\u90e8","filter_button_text":"\u7be9\u9078","reset_button_text":"\u91cd\u7f6e","min_placeholder":"\u6700\u5c0f\u503c","max_placeholder":"\u6700\u5927\u503c"}},"eventlog":{"show_stacktrace":"Show the stacktrace","hide_stacktrace":"Hide the stacktrace","tabs":{"formatted":"Formatted","raw":"Raw"},"editor":{"title":"Source code editor","description":"Your operating system should be configured to listen to one of these URL schemes.","openWith":"Open with","remember_choice":"Remember selected option for this session","open":"Open","cancel":"Cancel"}}}
);
//! moment.js locale configuration v2.22.2

View File

@ -149,22 +149,22 @@ class ImageResizer
'themes' => [
'disk' => 'system',
'folder' => config('cms.themesPathLocal', base_path('themes')),
'path' => config('cms.themesPath', '/themes'),
'path' => rtrim(config('cms.themesPath', '/themes'), '/'),
],
'plugins' => [
'disk' => 'system',
'folder' => config('cms.pluginsPathLocal', base_path('plugins')),
'path' => config('cms.pluginsPath', '/plugins'),
'path' => rtrim(config('cms.pluginsPath', '/plugins'), '/'),
],
'resized' => [
'disk' => config('cms.storage.resized.disk', 'local'),
'folder' => config('cms.storage.resized.folder', 'resized'),
'path' => config('cms.storage.resized.path', '/storage/app/resized'),
'path' => rtrim(config('cms.storage.resized.path', '/storage/app/resized'), '/'),
],
'media' => [
'disk' => config('cms.storage.media.disk', 'local'),
'folder' => config('cms.storage.media.folder', 'media'),
'path' => config('cms.storage.media.path', '/storage/app/media'),
'path' => rtrim(config('cms.storage.media.path', '/storage/app/media'), '/'),
],
'modules' => [
'disk' => 'system',
@ -174,7 +174,7 @@ class ImageResizer
'filemodel' => [
'disk' => config('cms.storage.uploads.disk', 'local'),
'folder' => config('cms.storage.uploads.folder', 'uploads'),
'path' => config('cms.storage.uploads.path', '/storage/app/uploads'),
'path' => rtrim(config('cms.storage.uploads.path', '/storage/app/uploads'), '/'),
],
];
@ -219,9 +219,29 @@ class ImageResizer
*/
public function getConfig()
{
$disk = $this->image['disk'];
// Normalize local disk adapters with symlinked paths to their target path
// to support atomic deployments where the base application path changes
// each deployment but the realpath of the storage directory does not
if (FileHelper::isLocalDisk($disk)) {
$realPath = realpath($disk->getAdapter()->getPathPrefix());
if ($realPath) {
$disk->getAdapter()->setPathPrefix($realPath);
}
}
// Handle disks that can't be serialized by referencing them by their
// filesystems.php config name
try {
serialize($disk);
} catch (Exception $ex) {
$disk = Storage::identify($disk);
}
$config = [
'image' => [
'disk' => $this->image['disk'],
'disk' => $disk,
'path' => $this->image['path'],
'source' => $this->image['source'],
],
@ -471,7 +491,7 @@ class ImageResizer
{
// Slashes in URL params have to be double encoded to survive Laravel's router
// @see https://github.com/octobercms/october/issues/3592#issuecomment-671017380
$resizedUrl = urlencode(urlencode($this->getResizedUrl()));
$resizedUrl = rawurlencode(rawurlencode($this->getResizedUrl()));
// Get the current configuration's identifier
$identifier = $this->getIdentifier();
@ -500,7 +520,10 @@ class ImageResizer
$url = $resizedDisk->url($this->getPathToResizedImage());
}
return $url;
// Ensure that a properly encoded URL is returned
$segments = explode('/', $url);
$lastSegment = array_pop($segments);
return implode('/', $segments) . '/' . rawurlencode(rawurldecode($lastSegment));
}
/**
@ -527,6 +550,11 @@ class ImageResizer
$path = $image['path'];
$selectedSource = $image['source'];
// Handle disks that couldn't be serialized
if (is_string($disk)) {
$disk = Storage::disk($disk);
}
// Verify that the source file exists
if (empty(FileHelper::extension($path)) || !$disk->exists($path)) {
$disk = null;
@ -556,14 +584,14 @@ class ImageResizer
// Process a string
} elseif (is_string($image)) {
// Parse the provided image path into a filesystem ready relative path
$relativePath = static::normalizePath(urldecode(parse_url($image, PHP_URL_PATH)));
$relativePath = static::normalizePath(rawurldecode(parse_url($image, PHP_URL_PATH)));
// Loop through the sources available to the application to pull from
// to identify the source most likely to be holding the image
$resizeSources = static::getAvailableSources();
foreach ($resizeSources as $source => $details) {
// Normalize the source path
$sourcePath = static::normalizePath(urldecode(parse_url($details['path'], PHP_URL_PATH)));
$sourcePath = static::normalizePath(rawurldecode(parse_url($details['path'], PHP_URL_PATH)));
// Identify if the current source is a match
if (starts_with($relativePath, $sourcePath)) {
@ -609,7 +637,7 @@ class ImageResizer
}
}
if (!$disk || !$path || !$selectedSource) {
if (!$disk || !$path || !$selectedSource || (!in_array(FileHelper::extension($path), ['jpg', 'jpeg', 'png', 'webp', 'gif']))) {
if (is_object($image)) {
$image = get_class($image);
}
@ -712,7 +740,7 @@ class ImageResizer
{
// Slashes in URL params have to be double encoded to survive Laravel's router
// @see https://github.com/octobercms/october/issues/3592#issuecomment-671017380
$decodedUrl = urldecode(urldecode($encodedUrl));
$decodedUrl = rawurldecode($encodedUrl);
$url = null;
// The identifier should be the signed version of the decoded URL

View File

@ -536,7 +536,7 @@ class MediaLibrary
*/
public function getPathUrl($path)
{
$path = $this->validatePath($path);
$path = $this->validatePath($path, true);
$fullPath = $this->storagePath . implode("/", array_map("rawurlencode", explode("/", $path)));

View File

@ -120,6 +120,29 @@ class PluginBase extends ServiceProviderBase
}
}
/**
* Registers back-end quick actions for this plugin.
*
* @return array
*/
public function registerQuickActions()
{
$configuration = $this->getConfigurationFromYaml();
if (array_key_exists('quickActions', $configuration)) {
$quickActions = $configuration['quickActions'];
if (is_array($quickActions)) {
array_walk_recursive($quickActions, function (&$item, $key) {
if ($key === 'url') {
$item = Backend::url($item);
}
});
}
return $quickActions;
}
}
/**
* Registers any back-end permissions used by this plugin.
*

View File

@ -1,6 +1,7 @@
<?php namespace System\Classes;
use Lang;
use Config;
use Response;
use Exception;
use SystemException;
@ -63,6 +64,11 @@ class SystemController extends ControllerBase
} catch (SystemException $ex) {
// If the resizing failed with a SystemException, it was most
// likely because it is in progress or has already finished
// although it could also be because the cache system used to store
// configuration data is broken
if (Config::get('cache.default', 'file') === 'array') {
throw new Exception('Image resizing requires a persistent cache driver, "array" is not supported. Try changing config/cache.php -> default to a persistent cache driver.');
}
} catch (Exception $ex) {
// If it failed for any other reason, restore the config so that
// the resizer route will continue to work until it succeeds

View File

@ -89,6 +89,11 @@ class UpdateManager
*/
protected $repository;
/**
* @var array An array of messages returned by migrations / seeders. Returned at the end of the update process.
*/
protected $messages = [];
/**
* Initialize this singleton.
*/
@ -162,6 +167,9 @@ class UpdateManager
}
}
// Print messages returned by migrations / seeders
$this->printMessages();
return $this;
}
@ -440,7 +448,11 @@ class UpdateManager
}
$seeder = App::make($className);
$seeder->run();
$return = $seeder->run();
if (isset($return) && (is_string($return) || is_array($return))) {
$this->addMessage($className, $return);
}
$this->note(sprintf('<info>Seeded %s</info> ', $module));
return $this;
@ -1003,4 +1015,50 @@ class UpdateManager
{
return Config::get('database.migrations', 'migrations');
}
/**
* Adds a message from a specific migration or seeder.
*
* @param string|object $class
* @param string|array $message
* @return void
*/
protected function addMessage($class, $message)
{
if (is_object($class)) {
$class = get_class($class);
}
if (!isset($this->messages[$class])) {
$this->messages[$class] = [];
}
if (is_string($message)) {
$this->messages[$class][] = $message;
} else if (is_array($message)) {
array_merge($this->messages[$class], $message);
}
}
/**
* Prints collated messages from the migrations and seeders
*
* @return void
*/
protected function printMessages()
{
if (!count($this->messages)) {
return;
}
// Add a line break
$this->note('');
foreach ($this->messages as $class => $messages) {
$this->note(sprintf('<info>%s reported:</info>', $class));
foreach ($messages as $message) {
$this->note(' - ' . (string) $message);
}
}
}
}

View File

@ -83,7 +83,7 @@ class VersionManager
// No updates needed
if ($currentVersion == $databaseVersion) {
$this->note('- <info>Nothing to update.</info>');
$this->note(' - <info>Nothing to update.</info>');
return;
}
@ -140,7 +140,7 @@ class VersionManager
foreach ($comments as $comment) {
$this->applyDatabaseComment($code, $version, $comment);
$this->note(sprintf('- <info>v%s: </info> %s', $version, $comment));
$this->note(sprintf(' - <info>v%s: </info> %s', $version, $comment));
}
}

View File

@ -1,5 +1,6 @@
<?php namespace System\Console;
use App;
use Illuminate\Console\Command;
/**
@ -327,7 +328,7 @@ class OctoberEnv extends Command
*/
protected function writeToConfigFile($content)
{
file_put_contents(config_path($this->config . '.php'), $content);
file_put_contents(rtrim(App::make('path.config'), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $this->config . '.php', $content);
}
/**
@ -335,7 +336,7 @@ class OctoberEnv extends Command
*/
protected function lines()
{
return file(config_path($this->config . '.php'));
return file(rtrim(App::make('path.config'), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $this->config . '.php');
}
/**

View File

@ -35,17 +35,30 @@ class OctoberFresh extends Command
if (!$this->confirmToProceed('Are you sure?')) {
return;
}
$themeRemoved = false;
$pluginRemoved = false;
$demoThemePath = themes_path().'/demo';
if (File::exists($demoThemePath)) {
Artisan::call('plugin:remove', ['name' => 'October.Demo', '--force' => true]);
File::deleteDirectory($demoThemePath);
$this->info('Demo has been removed! Enjoy a fresh start.');
$themeRemoved = true;
}
else {
$this->error('Demo theme is already removed.');
$demoPluginPath = plugins_path().'/october/demo';
if (File::exists($demoPluginPath)) {
Artisan::call('plugin:remove', ['name' => 'October.Demo', '--force' => true]);
$pluginRemoved = true;
}
if ($themeRemoved && $pluginRemoved) {
$this->info('Demo theme and plugin have been removed! Enjoy a fresh start.');
} elseif ($themeRemoved) {
$this->info('Demo theme has been removed! Enjoy a fresh start.');
} elseif ($pluginRemoved) {
$this->info('Demo plugin has been removed! Enjoy a fresh start.');
} else {
$this->info('Demo theme and plugin have already been removed.');
}
}

View File

@ -105,7 +105,7 @@ class ThemeSync extends Command
foreach ($userPaths as $userPath) {
foreach ($themePaths as $themePath) {
$pregMatch = '/' . str_replace('/', '\/', $userPath) . '/i';
$pregMatch = '/^' . str_replace('/', '\/', $userPath) . '/i';
if ($userPath === $themePath || preg_match($pregMatch, $themePath)) {
$paths[] = $themePath;

View File

@ -450,12 +450,15 @@ class Updates extends Controller
/**
* Contacts the update server for a list of necessary updates.
*
* @param bool $force Whether or not to force the redownload of existing tools
* @return string The rendered "execute" partial
*/
public function onForceUpdate()
public function onForceUpdate($force = true)
{
try {
$manager = UpdateManager::instance();
$result = $manager->requestUpdateList(true);
$result = $manager->requestUpdateList($force);
$coreHash = array_get($result, 'core.hash', false);
$coreBuild = array_get($result, 'core.build', false);
@ -706,7 +709,7 @@ class Updates extends Controller
'system::project.owner' => $result['owner'],
]);
return $this->onForceUpdate();
return $this->onForceUpdate(false);
}
catch (Exception $ex) {
$this->handleError($ex);
@ -878,7 +881,7 @@ class Updates extends Controller
}
Flash::success(Lang::get("system::lang.plugins.{$bulkAction}_success"));
return $this->listRefresh('manage');
return redirect()->refresh();
}
//

View File

@ -6,16 +6,16 @@
data-handler="onLoadUpdates">
<?= e(trans('system::lang.updates.check_label')) ?>
</a>
<a
href="<?= Backend::url('system/updates/install/themes') ?>"
class="btn btn-success oc-icon-plus">
<?= e(trans('system::lang.themes.install')) ?>
</a>
<a
href="<?= Backend::url('system/updates/install') ?>"
class="btn btn-success oc-icon-plus">
<?= e(trans('system::lang.plugins.install')) ?>
</a>
<a
href="<?= Backend::url('system/updates/install/themes') ?>"
class="btn btn-success oc-icon-plus">
<?= e(trans('system::lang.themes.install')) ?>
</a>
<a
href="<?= Backend::url('system/updates/manage') ?>"
class="btn btn-default oc-icon-puzzle-piece">

View File

@ -441,7 +441,8 @@ return [
'manage_other_administrators' => 'Manage other administrators',
'impersonate_users' => 'Impersonate users',
'manage_preferences' => 'Manage backend preferences',
'manage_editor' => 'Manage code editor preferences',
'manage_editor' => 'Manage global code editor preferences',
'manage_own_editor' => 'Manage personal code editor preferences',
'view_the_dashboard' => 'View the dashboard',
'manage_default_dashboard' => 'Manage the default dashboard',
'manage_branding' => 'Customize the back-end',

View File

@ -22,7 +22,7 @@ return [
'alpha_num' => 'Le champ :attribute ne peut contenir que des lettres et des chiffres.',
'array' => 'Le champ :attribute doit être un groupe.',
'before' => 'Le champ :attribute doit être une date avant le :date.',
'before_or_equal' => 'LE champ :attribute doit être une date avant le ou égal à :date.',
'before_or_equal' => 'Le champ :attribute doit être une date avant le ou égal à :date.',
'between' => [
'numeric' => 'Le champ :attribute doit être compris entre :min - :max.',
'file' => 'Le champ :attribute doit être compris entre :min - :max kilooctets.',
@ -36,8 +36,8 @@ return [
'different' => 'Le champ :attribute et :other doivent être différents.',
'digits' => 'Le champ :attribute doit être de :digits chiffres.',
'digits_between' => 'Le champ :attribute doit être compris entre :min et :max chiffres.',
'dimensions' => 'Le cahmp :attribute a des dimensions dimage invalides.',
'distinct' => 'Le cahmp :attribute a une valeur en double..',
'dimensions' => 'Le champ :attribute a des dimensions dimage invalides.',
'distinct' => 'Le champ :attribute a une valeur en double..',
'email' => 'Le format du champ :attribute nest pas valide.',
'exists' => 'Le champ :attribute sélectionné nest pas valide.',
'file' => 'Le champ :attribute doit être un fichier.',

View File

@ -33,7 +33,6 @@ return [
'fullscreen' => 'Schermo intero',
'preview' => 'Anteprima',
],
'mediamanager' => [
'insert_link' => "Inserisci collegamento elemento multimediale",
'insert_image' => "Inserisci immagine",
@ -45,23 +44,28 @@ return [
'invalid_video_empty_insert' => "Si prega di selezionare un file video da inserire.",
'invalid_audio_empty_insert' => "Si prega di selezionare un file audio da inserire.",
],
'alert' => [
'confirm_button_text' => 'OK',
'cancel_button_text' => 'Annulla',
'widget_remove_confirm' => 'Rimuovere questo widget?',
],
'datepicker' => [
'previousMonth' => 'Mese precedente',
'nextMonth' => 'Mese successivo',
'months' => ['Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno', 'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre'],
'weekdays' => ['Domenica', 'Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato'],
'weekdaysShort' => ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab']
'weekdaysShort' => ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab'],
],
'colorpicker' => [
'choose' => 'OK',
],
'filter' => [
'group' => [
'all' => 'tutti'
'all' => 'tutti',
],
'scopes' => [
'apply_button_text' => 'Applica',
'clear_button_text' => 'Rimuovi',
],
'dates' => [
'all' => 'tutte',
@ -69,10 +73,16 @@ return [
'reset_button_text' => 'Reimposta',
'date_placeholder' => 'Data',
'after_placeholder' => 'Dopo',
'before_placeholder' => 'Prima'
]
'before_placeholder' => 'Prima',
],
'numbers' => [
'all' => 'tutti',
'filter_button_text' => 'Filtra',
'reset_button_text' => 'Reset',
'min_placeholder' => 'Min',
'max_placeholder' => 'Max',
],
],
'eventlog' => [
'show_stacktrace' => 'Visualizza la traccia dello stack',
'hide_stacktrace' => 'Nascondi la traccia dello stack',
@ -86,7 +96,7 @@ return [
'openWith' => 'Apri con',
'remember_choice' => 'Ricorda l\'opzione selezionata per questa sessione',
'open' => 'Apri',
'cancel' => 'Annulla'
'cancel' => 'Annulla',
]
]
];

View File

@ -37,7 +37,7 @@ return [
"exists" => "Il valore di :attribute non è valido.",
"image" => ":attribute deve essere un'immagine.",
"in" => "Il valore di :attribute non è valido.",
"integer" => ":attribute deve essere un numero interno.",
"integer" => ":attribute deve essere un numero intero.",
"ip" => ":attribute deve essere un indirizzo IP valido.",
"max" => [
"numeric" => ":attribute non può essere maggiore di :max.",

View File

@ -331,6 +331,8 @@ return [
'core_downloading' => 'Bestanden aan het downloaden',
'core_extracting' => 'Bestanden aan het uitpakken',
'core_set_build' => 'Bouwnummer bijwerken',
'update_warnings_title' => 'Enkele problemen zijn gedetecteerd en vereisen aandacht:',
'update_warnings_plugin_missing' => 'De :parent_code plugin vereist dat :code geinstalleerd is, alvorens deze zal werken',
'changelog' => 'Logboek van wijzigingen',
'changelog_view_details' => 'Bekijk details',
'plugins' => 'Plugins',
@ -438,8 +440,9 @@ return [
'manage_mail_settings' => 'Beheer e-mailinstellingen',
'manage_other_administrators' => 'Beheer mede-beheerders',
'impersonate_users' => 'Inloggen als',
'manage_preferences' => 'Beheer back-end instellingen',
'manage_editor' => 'Beheer code editor instellingen',
'manage_preferences' => 'Beheer back-end voorkeuren',
'manage_editor' => 'Beheer code editor voorkeuren',
'manage_own_editor' => 'Beheer persoonlijk code editor voorkeuren',
'view_the_dashboard' => 'Toon dashboard',
'manage_default_dashboard' => 'Beheer het standaard dashboard',
'manage_branding' => 'Back-end aanpassen',

View File

@ -32,6 +32,7 @@ return [
'boolean' => ':attribute moet ja of nee zijn.',
'confirmed' => ':attribute bevestiging komt niet overeen.',
'date' => ':attribute moet een datum bevatten.',
'date_equals' => ':attribute moet een datum zijn gelijk aan :date.',
'date_format' => ':attribute moet een geldig datum formaat bevatten.',
'different' => ':attribute en :other moeten verschillend zijn.',
'digits' => ':attribute moet bestaan uit :digits cijfers.',
@ -39,9 +40,22 @@ return [
'dimensions' => ':attribute heeft geen geldige afmetingen voor afbeeldingen.',
'distinct' => ':attribute heeft een dubbele waarde.',
'email' => ':attribute is geen geldig e-mailadres.',
'ends_with' => ':attribute moet eindigen op een van de volgende waarden: :values.',
'exists' => ':attribute bestaat niet.',
'file' => ':attribute moet een bestand zijn.',
'filled' => ':attribute is verplicht.',
'gt' => [
'numeric' => ':attribute moet groter zijn dan :value.',
'file' => ':attribute moet groter zijn dan :value kilobyte.',
'string' => ':attribute moet langer zijn dan :value karakters.',
'array' => ':attribute moet meer dan :value items bevatten.',
],
'gte' => [
'numeric' => ':attribute moet groter of gelijk zijn aan :value.',
'file' => ':attribute moet minstens :value kilobyte groot zijn.',
'string' => ':attribute moet minstens :value karakters lang zijn.',
'array' => ':attribute moet minstens :value items bevatten.',
],
'image' => ':attribute moet een afbeelding zijn.',
'in' => ':attribute is ongeldig.',
'in_array' => ':attribute bestaat niet in :other.',
@ -50,6 +64,18 @@ return [
'ipv4' => ':attribute moet een geldig IPv4-adres zijn.',
'ipv6' => ':attribute moet een geldig IPv6-adres zijn.',
'json' => ':attribute moet een geldige JSON-string zijn.',
'lt' => [
'numeric' => ':attribute moet kleiner zijn dan :value.',
'file' => ':attribute moet kleiner zijn dan :value kilobyte.',
'string' => ':attribute moet korter zijn dan :value karakters.',
'array' => ':attribute moet minder dan :value items bevatten.',
],
'lte' => [
'numeric' => ':attribute moet kleiner of gelijk zijn aan :value.',
'file' => ':attribute mag hoogstens :value kilobyte groot zijn.',
'string' => ':attribute mag hoogstens :value karakters lang zijn.',
'array' => ':attribute mag hoogstens :value items bevatten.',
],
'max' => [
'numeric' => ':attribute mag niet hoger dan :max zijn.',
'file' => ':attribute mag niet meer dan :max kilobytes zijn.',
@ -64,7 +90,8 @@ return [
'string' => ':attribute moet minimaal :min karakters zijn.',
'array' => ':attribute moet minimaal :min items bevatten.',
],
'not_in' => 'Het formaat van :attribute is ongeldig.',
'not_in' => 'De gekozen waarde van :attribute is ongeldig.',
'not_regex' => 'Het formaat van :attribute is ongeldig.',
'numeric' => ':attribute moet een nummer zijn.',
'present' => ':attribute moet bestaan.',
'regex' => ':attribute formaat is ongeldig.',
@ -82,11 +109,13 @@ return [
'string' => ':attribute moet :size karakters zijn.',
'array' => ':attribute moet :size items bevatten.',
],
'starts_with' => ':attribute moet beginnen met een van de volgende waarden: :values.',
'string' => ':attribute moet een tekenreeks zijn.',
'timezone' => ':attribute moet een geldige tijdzone zijn.',
'unique' => ':attribute is al in gebruik.',
'uploaded' => 'Het uploaden van :attribute is mislukt.',
'url' => ':attribute is geen geldige URL.',
'uuid' => ':attribute moet een geldig UUID zijn.',
/*
|--------------------------------------------------------------------------

View File

@ -166,18 +166,18 @@ return [
'mail_templates' => [
'menu_label' => 'Шаблоны почты',
'menu_description' => 'Изменение шаблонов писем, отправляемых пользователям и администраторам.',
'new_template' => 'Новый шаблон',
'new_layout' => 'Новый макет',
'new_template' => 'Новая тема',
'new_layout' => 'Новый шаблон',
'new_partial' => 'Новый фрагмент',
'template' => 'Шаблон',
'templates' => 'Шаблоны',
'partial' => 'Фрагмент',
'partials' => 'Фрагменты',
'menu_layouts_label' => 'Макеты почты',
'menu_layouts_label' => 'Шаблоны почты',
'menu_partials_label' => 'Фрагменты почты',
'layout' => 'Макет',
'layouts' => 'Макеты',
'no_layout' => '-- Нет макета --',
'layout' => 'Шаблон',
'layouts' => 'Шаблоны',
'no_layout' => '-- Нет шаблона --',
'name' => 'Название',
'name_comment' => 'Уникальное имя, используемое для обозначения этого шаблона',
'code' => 'Код',
@ -191,14 +191,14 @@ return [
'test_send' => 'Отправить тестовое сообщение',
'test_success' => 'Тестовое сообщение было успешно отправлено.',
'test_confirm' => 'Тестовое сообщение будет отправлено на :email. Продолжить?',
'creating' => 'Создание шаблона...',
'creating_layout' => 'Создание макета...',
'saving' => 'Сохранение шаблона...',
'saving_layout' => 'Сохранение макета...',
'delete_confirm' => 'Вы действительно хотите удалить этот шаблон?',
'delete_layout_confirm' => 'Вы действительно хотите удалить этот макет?',
'deleting' => 'Удаление шаблона...',
'deleting_layout' => 'Удаление макета...',
'creating' => 'Создание...',
'creating_layout' => 'Создание шаблона...',
'saving' => 'Сохранение...',
'saving_layout' => 'Сохранение шаблона...',
'delete_confirm' => 'Вы действительно хотите это удалить?',
'delete_layout_confirm' => 'Вы действительно хотите удалить этот шаблон?',
'deleting' => 'Удаление...',
'deleting_layout' => 'Удаление шаблона...',
'sending' => 'Отправка тестового сообщения...',
'return' => 'Вернуться к списку шаблонов',
'options' => 'Опции',

View File

@ -106,7 +106,7 @@ return [
],
'plugins' => [
'manage' => 'Upravljanje vtičnikov',
'install' => 'Namesti vtičnike in teme',
'install' => 'Namesti vtičnike',
'install_products' => 'Namesti produkte',
'search' => 'išči vtičnike za namestitev...',
'installed' => 'Nameščeni vtičniki',
@ -310,74 +310,77 @@ return [
'install_success' => 'Produkt je bil uspešno nameščen.',
],
'updates' => [
'title' => 'Upravljanje posodobitev',
'name' => 'Posodobitev programske opreme',
'menu_label' => 'Posodobitve in vtičniki',
'menu_description' => 'Posodobitev sistema, upravljanje in nameščanje vtičnikov in tem.',
'return_link' => 'Vrni se na sistemske posodobitve',
'check_label' => 'Preveri za nove posodobitve',
'retry_label' => 'Poskusi ponovno',
'plugin_name' => 'Ime',
'plugin_code' => 'Koda',
'plugin_description' => 'Opis',
'plugin_version' => 'Različica',
'plugin_author' => 'Avtor',
'plugin_not_found' => 'Vtičnika ni mogoče najti',
'core_current_build' => 'Trenutna različica',
'core_view_changelog' => 'Pokaži dnevnik sprememb',
'core_build' => 'Različica :build',
'core_build_help' => 'Na voljo je najnovejša različica.',
'core_downloading' => 'Prenašanje datotek aplikacije',
'core_extracting' => 'Ekstrahiranje datotek aplikacije',
'core_set_build' => 'Nastavljanje številke različice',
'changelog' => 'Dnevnik sprememb',
'changelog_view_details' => 'Pokaži podrobnosti',
'plugins' => 'Vtičniki',
'themes' => 'Teme',
'disabled' => 'Onemogočeno',
'plugin_downloading' => 'Prenašanje vtičnika :name',
'plugin_extracting' => 'Ekstrahiranje vtičnika :name',
'plugin_version_none' => 'Nov vtičnik',
'plugin_current_version' => 'Trenutna verzija',
'theme_new_install' => 'Namestitev nove teme.',
'theme_downloading' => 'Prenašanje teme :name',
'theme_extracting' => 'Ekstrahiranje teme :name',
'update_label' => 'Posodobitev programske opreme',
'update_completing' => 'Zaključevanje posodabljanja',
'update_loading' => 'Nalaganje razpoložljivih posodobitev...',
'update_success' => 'Posodabljanje končano.',
'update_failed_label' => 'Posodabljanje ni bilo uspešno.',
'force_label' => 'Vsili posodobitev',
'found' => [
'title' => 'Upravljanje posodobitev',
'name' => 'Posodobitev programske opreme',
'menu_label' => 'Posodobitve in vtičniki',
'menu_description' => 'Posodobitev sistema, upravljanje in nameščanje vtičnikov in tem.',
'return_link' => 'Vrni se na sistemske posodobitve',
'check_label' => 'Preveri za nove posodobitve',
'retry_label' => 'Poskusi ponovno',
'plugin_name' => 'Ime',
'plugin_code' => 'Koda',
'plugin_description' => 'Opis',
'plugin_version' => 'Različica',
'plugin_author' => 'Avtor',
'plugin_not_found' => 'Vtičnika ni mogoče najti',
'plugin_version_not_found' => 'Različice vtičnika ni mogoče najti',
'core_current_build' => 'Trenutna različica',
'core_view_changelog' => 'Pokaži dnevnik sprememb',
'core_build' => 'Različica :build',
'core_build_help' => 'Na voljo je najnovejša različica.',
'core_downloading' => 'Prenašanje datotek aplikacije',
'core_extracting' => 'Ekstrahiranje datotek aplikacije',
'core_set_build' => 'Nastavljanje številke različice',
'update_warnings_title' => 'Odkrite so bile nekatere težave, ki zahtevajo pozornost:',
'update_warnings_plugin_missing' => 'Vtičnik :parent_code zahteva za pravilno delovanje namestitev :code.',
'changelog' => 'Dnevnik sprememb',
'changelog_view_details' => 'Prikaži podrobnosti',
'plugins' => 'Vtičniki',
'themes' => 'Teme',
'disabled' => 'Onemogočeno',
'plugin_downloading' => 'Prenašanje vtičnika :name',
'plugin_extracting' => 'Ekstrahiranje vtičnika :name',
'plugin_version_none' => 'Nov vtičnik',
'plugin_current_version' => 'Trenutna verzija',
'theme_new_install' => 'Namestitev nove teme.',
'theme_downloading' => 'Prenašanje teme :name',
'theme_extracting' => 'Ekstrahiranje teme :name',
'update_label' => 'Posodobitev programske opreme',
'update_completing' => 'Zaključevanje posodabljanja',
'update_loading' => 'Nalaganje razpoložljivih posodobitev...',
'update_success' => 'Posodabljanje končano.',
'update_failed_label' => 'Posodabljanje ni bilo uspešno.',
'force_label' => 'Vsili posodobitev',
'found' => [
'label' => 'Nove posodobitve so na voljo!',
'help' => "Kliknite 'Posodobitev programske opreme' za začetek posodabljanja.",
'help' => 'Kliknite "Posodobitev programske opreme" za začetek posodabljanja.',
],
'none' => [
'none' => [
'label' => 'Ni posodobitev',
'help' => 'Nove posodobitve niso na voljo.',
],
'important_action' => [
'important_action' => [
'empty' => 'Izberite dejanje',
'confirm' => 'Potrdi posodobitev',
'skip' => 'Preskoči to posodobitev (le tokrat)',
'ignore' => 'Preskoči to posodobitev (vedno)',
],
'important_action_required' => 'Potrebno je ukrepanje',
'important_view_guide' => 'Oglejte si navodila za nadgradnjo',
'important_view_release_notes' => 'Oglejte si opombe ob izdaji',
'important_alert_text' => 'Nekatere posodobitve potrebujejo vašo pozornost.',
'details_title' => 'Podrobnosti vtičnika',
'details_view_homepage' => 'Odpri spletno stran',
'details_readme' => 'Dokumentacija',
'details_readme_missing' => 'Dokumentacija ni na voljo.',
'details_changelog' => 'Dnevnik sprememb',
'details_changelog_missing' => 'Dnevnik sprememb ni na voljo.',
'details_upgrades' => 'Navodila za nadgradnjo',
'details_upgrades_missing' => 'Navodila za nadgradnjo niso na voljo.',
'details_licence' => 'Licenca',
'details_licence_missing' => 'Licenca ni na voljo.',
'details_current_version' => 'Trenutna različica',
'details_author' => 'Avtor',
'important_action_required' => 'Potrebno je ukrepanje',
'important_view_guide' => 'Oglejte si navodila za nadgradnjo',
'important_view_release_notes' => 'Oglejte si opombe ob izdaji',
'important_alert_text' => 'Nekatere posodobitve potrebujejo vašo pozornost.',
'details_title' => 'Podrobnosti vtičnika',
'details_view_homepage' => 'Odpri spletno stran',
'details_readme' => 'Dokumentacija',
'details_readme_missing' => 'Dokumentacija ni na voljo.',
'details_changelog' => 'Dnevnik sprememb',
'details_changelog_missing' => 'Dnevnik sprememb ni na voljo.',
'details_upgrades' => 'Navodila za nadgradnjo',
'details_upgrades_missing' => 'Navodila za nadgradnjo niso na voljo.',
'details_licence' => 'Licenca',
'details_licence_missing' => 'Licenca ni na voljo.',
'details_current_version' => 'Trenutna različica',
'details_author' => 'Avtor',
],
'server' => [
'connect_error' => 'Napaka pri povezavi s strežnikom.',

View File

@ -32,6 +32,7 @@ return [
'boolean' => '":attribute" mora biti Da (true) ali Ne (false).',
'confirmed' => 'Potrditev polja ":attribute" se ne ujema.',
'date' => '":attribute" ni veljaven datum.',
'date_equals' => '":attribute" mora biti datum enak :date.',
'date_format' => '":attribute" se ne ujema s formatom :format.',
'different' => '":attribute" in :other morata biti različna.',
'digits' => '":attribute" mora vsebovati :digits številk.',
@ -39,9 +40,22 @@ return [
'dimensions' => '":attribute" vsebuje neveljavne dimenzije slike.',
'distinct' => '":attribute" polje ima podvojeno vrednost.',
'email' => '":attribute" mora biti veljaven e-poštni naslov.',
'ends_with' => '":attribute" se mora končati z eno od naslednjih vrednosti: :values.',
'exists' => 'Izbran element ":attribute" je neveljaven.',
'file' => '":attribute" mora biti datoteka.',
'filled' => '":attribute" polje mora vsebovati vrednost.',
'gt' => [
'numeric' => ':attribute mora biti večji od :value.',
'file' => ':attribute mora biti večji od :value kB.',
'string' => ':attribute mora vsebovati več kot :value znakov.',
'array' => ':attribute mora vsebovati več kot :value elementov.',
],
'gte' => [
'numeric' => ':attribute mora biti večji ali enak kot :value.',
'file' => ':attribute mora biti večji ali enak kot :value kB.',
'string' => ':attribute mora vsebovati vsaj ali več kot :value znakov.',
'array' => ':attribute mora vsebovati vsaj :value elementov ali več.',
],
'image' => '":attribute" mora biti slika.',
'in' => 'Izbran element ":attribute" je neveljaven.',
'in_array' => 'Element ":attribute" ne obstaja v ":other".',
@ -50,6 +64,18 @@ return [
'ipv4' => '":attribute" mora biti veljaven IPv4 naslov.',
'ipv6' => '":attribute" mora biti veljaven IPv6 naslov.',
'json' => '":attribute" mora biti veljaven JSON format.',
'lt' => [
'numeric' => ':attribute mora biti manjši od :value.',
'file' => ':attribute mora biti manjši od :value kB.',
'string' => ':attribute mora vsebovati manj kot :value znakov.',
'array' => ':attribute mora vsebovati manj kot :value elementov.',
],
'lte' => [
'numeric' => ':attribute mora biti manjši ali enak kot :value.',
'file' => ':attribute mora biti manjši ali enak kot :value kB.',
'string' => ':attribute mora vsebovati manj ali enako kot :value znakov.',
'array' => ':attribute ne sme vsebovati več kot :value elementov.',
],
'max' => [
'numeric' => ':attribute ne sme biti večji od :max.',
'file' => ':attribute ne sme biti večji od :max kB.',
@ -65,6 +91,7 @@ return [
'array' => ':attribute mora vsebovati vsaj :min elementov.',
],
'not_in' => 'Izbrani element ":attribute" je neveljaven.',
'not_regex' => 'Format za ":attribute" je neveljaven.',
'numeric' => '":attribute" mora biti število.',
'present' => 'Polje za ":attribute" mora obstajati.',
'regex' => 'Format za ":attribute" je neveljaven.',
@ -74,7 +101,7 @@ return [
'required_with' => 'Polje za ":attribute" je obvezno, kadar :values obstaja.',
'required_with_all' => 'Polje za ":attribute" je obvezno, kadar :values obstaja.',
'required_without' => 'Polje za ":attribute" je obvezno, kadar :values ne obstaja.',
'required_without_all' => 'Polje za ":attribute" je obvezno, kadar :values ne obstaja.',
'required_without_all' => 'Polje za ":attribute" je obvezno, kadar nobeden od :values ne obstaja.',
'same' => '":attribute" in ":other" se morata ujemati.',
'size' => [
'numeric' => ':attribute mora biti :size.',
@ -82,11 +109,13 @@ return [
'string' => ':attribute mora vsebovati :size znakov.',
'array' => ':attribute mora vsebovati :size elementov.',
],
'string' => ':attribute mora biti veljaven znakovni niz.',
'timezone' => ':attribute mora biti veljavno območje.',
'unique' => 'Element :attribute je že uporabljen.',
'uploaded' => 'Elementa :attribute ni bilo mogoče naložiti.',
'url' => 'Format elementa :attribute je neveljaven.',
'starts_with' => '":attribute" se mora začeti z eno od naslednjih vrednosti: :values.',
'string' => '":attribute" mora biti veljaven znakovni niz.',
'timezone' => '":attribute" mora biti veljavno območje.',
'unique' => 'Element ":attribute" je že zaseden.',
'uploaded' => 'Elementa ":attribute" ni bilo mogoče naložiti.',
'url' => 'Format za ":attribute" je neveljaven.',
'uuid' => '":attribute" mora biti veljaven UUID.',
/*
|--------------------------------------------------------------------------

View File

@ -0,0 +1,86 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Client-side Language Lines
|--------------------------------------------------------------------------
|
| These are messages made available to the client browser via JavaScript.
| To compile this file run: php artisan october:util compile lang
|
*/
'markdowneditor' => [
'formatting' => '格式',
'quote' => '引用',
'code' => '程式碼',
'header1' => '標題一',
'header2' => '標題二',
'header3' => '標題三',
'header4' => '標題四',
'header5' => '標題五',
'header6' => '標題六',
'bold' => '粗體',
'italic' => '斜體',
'unorderedlist' => '項目清單',
'orderedlist' => '數字清單',
'video' => '影片',
'image' => '圖片',
'link' => '連結',
'horizontalrule' => '插入水平線',
'fullscreen' => '全螢幕',
'preview' => '預覽',
],
'mediamanager' => [
'insert_link' => '插入媒體櫃連結',
'insert_image' => '插入媒體櫃圖片',
'insert_video' => '插入媒體櫃影片',
'insert_audio' => '插入媒體櫃音訊',
'invalid_file_empty_insert' => '請選擇檔案以插入連結。',
'invalid_file_single_insert' => '請選擇一個檔案。',
'invalid_image_empty_insert' => '請選擇插入的圖片。',
'invalid_video_empty_insert' => '請選擇插入的影片。',
'invalid_audio_empty_insert' => '請選擇插入的音訊。',
],
'alert' => [
'confirm_button_text' => '確認',
'cancel_button_text' => '取消',
'widget_remove_confirm' => '確定移除此元件?',
],
'datepicker' => [
'previousMonth' => '上個月',
'nextMonth' => '下個月',
'months' => ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
'weekdays' => ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'],
'weekdaysShort' => ['週日', '週一', '週二', '週三', '週四', '週五', '週六']
],
'colorpicker' => [
'choose' => '確定',
],
'filter' => [
'group' => [
'all' => '全部',
],
'scopes' => [
'apply_button_text' => '確定',
'clear_button_text' => '清除',
],
'dates' => [
'all' => '全部',
'filter_button_text' => '篩選',
'reset_button_text' => '重置',
'date_placeholder' => '日期',
'after_placeholder' => '在此之後',
'before_placeholder' => '在此之前',
],
'numbers' => [
'all' => '全部',
'filter_button_text' => '篩選',
'reset_button_text' => '重置',
'min_placeholder' => '最小值',
'max_placeholder' => '最大值',
],
]
];

View File

@ -26,6 +26,9 @@ class File extends FileBase
public function getThumb($width, $height, $options = [])
{
$url = '';
$width = !empty($width) ? $width : 0;
$height = !empty($height) ? $height : 0;
if (!$this->isPublic() && class_exists(Files::class)) {
$options = $this->getDefaultThumbOptions($options);
// Ensure that the thumb exists first

View File

@ -50,6 +50,12 @@ class MailLayout extends Model
public static $codeCache;
/**
* Fired before the model is deleted.
*
* @return void
* @throws ApplicationException if the template is locked
*/
public function beforeDelete()
{
if ($this->is_locked) {
@ -57,6 +63,11 @@ class MailLayout extends Model
}
}
/**
* List MailLayouts codes keyed by ID.
*
* @return array
*/
public static function listCodes()
{
if (self::$codeCache !== null) {
@ -66,11 +77,23 @@ class MailLayout extends Model
return self::$codeCache = self::lists('id', 'code');
}
/**
* Return the ID of a MailLayout instance from a defined code.
*
* @param string $code
* @return string
*/
public static function getIdFromCode($code)
{
return array_get(self::listCodes(), $code);
}
/**
* Find a MailLayout instance by its code or create a new instance from the view file.
*
* @param string $code
* @return MailLayout
*/
public static function findOrMakeLayout($code)
{
$layout = self::whereCode($code)->first();
@ -87,6 +110,7 @@ class MailLayout extends Model
/**
* Loops over each mail layout and ensures the system has a layout,
* if the layout does not exist, it will create one.
*
* @return void
*/
public static function createLayouts()
@ -107,6 +131,13 @@ class MailLayout extends Model
}
}
/**
* Fill model using a view file retrieved by code.
*
* @param string|null $code
* @return void
* @throws ApplicationException if a layout with the defined code is not registered.
*/
public function fillFromCode($code = null)
{
$definitions = MailManager::instance()->listRegisteredLayouts();
@ -122,6 +153,12 @@ class MailLayout extends Model
$this->fillFromView($definition);
}
/**
* Fill model using a view file retrieved by path.
*
* @param string $path
* @return void
*/
public function fillFromView($path)
{
$sections = self::getTemplateSections($path);
@ -150,8 +187,18 @@ class MailLayout extends Model
$this->content_text = array_get($sections, 'text');
}
/**
* Get section array from a view file retrieved by code.
*
* @param string $code
* @return array|null
*/
protected static function getTemplateSections($code)
{
return MailParser::parse(FileHelper::get(View::make($code)->getPath()));
if (!View::exists($code)) {
return null;
}
$view = View::make($code);
return MailParser::parse(FileHelper::get($view->getPath()));
}
}

View File

@ -42,6 +42,11 @@ class MailPartial extends Model
'content_html' => 'required',
];
/**
* Fired after the model has been fetched.
*
* @return void
*/
public function afterFetch()
{
if (!$this->is_custom) {
@ -49,6 +54,12 @@ class MailPartial extends Model
}
}
/**
* Find a MailPartial instance by code or create a new instance from a view file.
*
* @param string $code
* @return MailTemplate
*/
public static function findOrMakePartial($code)
{
try {
@ -68,6 +79,7 @@ class MailPartial extends Model
/**
* Loops over each mail layout and ensures the system has a layout,
* if the layout does not exist, it will create one.
*
* @return void
*/
public static function createPartials()
@ -98,6 +110,12 @@ class MailPartial extends Model
}
}
/**
* Fill model using a view file retrieved by code.
*
* @param string|null $code
* @return void
*/
public function fillFromCode($code = null)
{
$definitions = MailManager::instance()->listRegisteredPartials();
@ -113,6 +131,12 @@ class MailPartial extends Model
$this->fillFromView($definition);
}
/**
* Fill model using a view file retrieved by path.
*
* @param string $path
* @return void
*/
public function fillFromView($path)
{
$sections = self::getTemplateSections($path);
@ -122,8 +146,18 @@ class MailPartial extends Model
$this->content_text = array_get($sections, 'text');
}
/**
* Get section array from a view file retrieved by code.
*
* @param string $code
* @return array|null
*/
protected static function getTemplateSections($code)
{
return MailParser::parse(FileHelper::get(View::make($code)->getPath()));
if (!View::exists($code)) {
return null;
}
$view = View::make($code);
return MailParser::parse(FileHelper::get($view->getPath()));
}
}

View File

@ -47,6 +47,7 @@ class MailTemplate extends Model
/**
* Returns an array of template codes and descriptions.
*
* @return array
*/
public static function listAllTemplates()
@ -60,6 +61,7 @@ class MailTemplate extends Model
/**
* Returns a list of all mail templates.
*
* @return array Returns an array of the MailTemplate objects.
*/
public static function allTemplates()
@ -68,7 +70,9 @@ class MailTemplate extends Model
$codes = array_keys(self::listAllTemplates());
foreach ($codes as $code) {
$result[] = self::findOrMakeTemplate($code);
if (View::exists($code)) {
$result[] = self::findOrMakeTemplate($code);
}
}
return $result;
@ -76,6 +80,7 @@ class MailTemplate extends Model
/**
* Syncronise all file templates to the database.
*
* @return void
*/
public static function syncAll()
@ -117,6 +122,11 @@ class MailTemplate extends Model
}
}
/**
* Fired after the model has been fetched.
*
* @return void
*/
public function afterFetch()
{
if (!$this->is_custom) {
@ -124,31 +134,65 @@ class MailTemplate extends Model
}
}
/**
* Fill model using provided content.
*
* @param string $content
* @return void
*/
public function fillFromContent($content)
{
$this->fillFromSections(MailParser::parse($content));
}
/**
* Fill model using a view file path.
*
* @param string $path
* @return void
*/
public function fillFromView($path)
{
$this->fillFromSections(self::getTemplateSections($path));
}
/**
* Fill model using provided section array.
*
* @param array $sections
* @return void
*/
protected function fillFromSections($sections)
{
$this->content_html = $sections['html'];
$this->content_text = $sections['text'];
$this->content_html = array_get($sections, 'html');
$this->content_text = array_get($sections, 'text');
$this->subject = array_get($sections, 'settings.subject', 'No subject');
$layoutCode = array_get($sections, 'settings.layout', 'default');
$this->layout = MailLayout::findOrMakeLayout($layoutCode);
}
/**
* Get section array from a view file retrieved by code.
*
* @param string $code
* @return array|null
*/
protected static function getTemplateSections($code)
{
return MailParser::parse(FileHelper::get(View::make($code)->getPath()));
if (!View::exists($code)) {
return null;
}
$view = View::make($code);
return MailParser::parse(FileHelper::get($view->getPath()));
}
/**
* Find a MailTemplate record by code or create one from a view file.
*
* @param string $code
* @return MailTemplate model
*/
public static function findOrMakeTemplate($code)
{
$template = self::whereCode($code)->first();

View File

@ -208,7 +208,7 @@ trait AssetMaker
*
* Example usage:
*
* Event::listen('system.assets.beforeAddAsset', function (string $type, string $path, array $attributes) {
* Event::listen('system.assets.beforeAddAsset', function (string &$type, string &$path, array &$attributes) {
* if (in_array($path, $blockedAssets)) {
* return false;
* }
@ -216,7 +216,7 @@ trait AssetMaker
*
* Or
*
* $this->bindEvent('assets.beforeAddAsset', function (string $type, string $path, array $attributes) {
* $this->bindEvent('assets.beforeAddAsset', function (string &$type, string &$path, array &$attributes) {
* $attributes['special_cdn_flag'] = false;
* });
*

View File

@ -86,7 +86,7 @@ trait PropertyContainer
* Returns a defined property value or default if one is not set.
* @param string $name The property name to look for.
* @param string $default A default value to return if no name is found.
* @return string The property value or the default specified.
* @return mixed The property value or the default specified.
*/
public function property($name, $default = null)
{

View File

@ -1,5 +1,6 @@
<?php namespace System\Twig;
use System\Twig\Loader as TwigLoader;
use Twig\Environment as TwigEnvironment;
use Illuminate\Contracts\View\Engine as EngineInterface;
@ -26,7 +27,14 @@ class Engine implements EngineInterface
public function get($path, array $vars = [])
{
$previousAllow = TwigLoader::$allowInclude;
TwigLoader::$allowInclude = true;
$template = $this->environment->loadTemplate($path);
TwigLoader::$allowInclude = $previousAllow;
return $template->render($vars);
}
}

View File

@ -15,9 +15,9 @@ use Exception;
class Loader implements TwigLoaderInterface
{
/**
* @var string Expected file extension
* @var bool Allow any local file
*/
protected $extension = 'htm';
public static $allowInclude = false;
/**
* @var array Cache
@ -37,16 +37,11 @@ class Loader implements TwigLoaderInterface
return $this->cache[$name];
}
if (File::isFile($name)) {
if (static::$allowInclude === true && File::isFile($name)) {
return $this->cache[$name] = $name;
}
$view = $name;
if (File::extension($view) === $this->extension) {
$view = substr($view, 0, -strlen($this->extension));
}
$path = $finder->find($view);
$path = $finder->find($name);
return $this->cache[$name] = $path;
}

View File

@ -20,6 +20,8 @@ final class SecurityPolicy implements SecurityPolicyInterface
protected $blockedMethods = [
'addDynamicMethod',
'addDynamicProperty',
'bindEvent',
'bindEventOnce',
];
/**

View File

@ -11,7 +11,7 @@ $uri = urldecode(
// This file allows us to emulate Apache's "mod_rewrite" functionality from the
// built-in PHP web server. This provides a convenient way to test a Laravel
// application without having installed a "real" web server software here.
if ($uri !== '/' and file_exists(__DIR__.'/'.$uri)) {
if ($uri !== '/' && file_exists(__DIR__.'/'.$uri)) {
return false;
}
require_once __DIR__.'/index.php';

BIN
tests/fixtures/media/october space.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@ -1,3 +1,8 @@
1.2.0:
- "!!! Security update - see: https://octobercms.com"
1.1.0:
- !!! Drop support for blog settings
- drop_blog_settings_table.php
1.0.5:
- Create blog settings table
- Another update message

View File

@ -6,7 +6,33 @@ use October\Tests\Fixtures\Backend\Models\UserFixture;
class FormTestModel extends Model
{
public function modelCustomOptionsMethod()
{
return ['model', 'custom', 'options'];
}
public function getFieldNameOnModelOptionsMethodOptions()
{
return ['model', 'field name', 'options method'];
}
public function getDropdownOptions()
{
return ['dropdown', 'options'];
}
public function staticMethodOptions()
{
return ['static', 'method'];
}
}
class FormHelper
{
public static function staticMethodOptions()
{
return ['static', 'method'];
}
}
class FormTest extends PluginTestCase
@ -131,6 +157,61 @@ class FormTest extends PluginTestCase
$this->assertEquals('[name="array[trigger][]"]', array_get($attributes, 'data-trigger'));
}
public function testOptionsGeneration()
{
$form = new Form(null, [
'model' => new FormTestModel,
'arrayName' => 'array',
'fields' => [
'static_method_options' => [
'type' => 'dropdown',
'options' => 'FormHelper::staticMethodOptions',
'expect' => ['static', 'method'],
],
'callable_options' => [
'type' => 'dropdown',
'options' => [\FormHelper::class, 'staticMethodOptions'],
'expect' => ['static', 'method'],
],
'model_method_options' => [
'type' => 'dropdown',
'options' => 'modelCustomOptionsMethod',
'expect' => ['model', 'custom', 'options'],
],
'defined_options' => [
'type' => 'dropdown',
'options' => ['value1', 'value2'],
'expect' => ['value1', 'value2'],
],
'defined_options_key_value' => [
'type' => 'dropdown',
'options' => [
'key1' => 'value1',
'key2' => 'value2',
],
'expect' => [
'key1' => 'value1',
'key2' => 'value2',
],
],
'field_name_on_model_options_method' => [
'type' => 'dropdown',
'expect' => ['model', 'field name', 'options method'],
],
'get_dropdown_options_method' => [
'type' => 'dropdown',
'expect' => ['dropdown', 'options'],
],
]
]);
$form->render();
foreach ($form->getFields() as $name => $field) {
$this->assertEquals($field->options(), $field->config['expect']);
}
}
protected function restrictedFormFixture(bool $singlePermission = false)
{
return new Form(null, [

View File

@ -2,7 +2,7 @@
use Database\Tester\Models\Post;
class ModelTest extends PluginTestCase
class PluginModelTest extends PluginTestCase
{
public function setUp() : void
{

View File

@ -320,6 +320,21 @@ class ImageResizerTest extends PluginTestCase
);
}
public function testSpaceInFilename()
{
// Media URL with space
$this->setUpStorage();
$this->copyMedia();
$imageResizer = new ImageResizer(
URL::to(MediaLibrary::url('october space.png')),
100,
100
);
$this->assertStringContainsString('october%20space', $imageResizer->getResizedUrl(), 'Resized URLs are not properly URL encoded');
}
protected function setUpStorage()
{
$this->app->useStoragePath(base_path('storage/temp'));

View File

@ -76,17 +76,17 @@ class MediaLibraryTest extends TestCase // @codingStandardsIgnoreLine
$contents = MediaLibrary::instance()->listFolderContents();
$this->assertNotEmpty($contents, 'Media library item is not discovered');
$this->assertCount(2, $contents);
$this->assertEquals('file', $contents[0]->type, 'Media library item does not have the right type');
$this->assertEquals('/october.png', $contents[0]->path, 'Media library item does not have the right path');
$this->assertNotEmpty($contents[0]->lastModified, 'Media library item last modified is empty');
$this->assertNotEmpty($contents[0]->size, 'Media library item size is empty');
$this->assertCount(3, $contents);
$this->assertEquals('file', $contents[1]->type, 'Media library item does not have the right type');
$this->assertEquals('/text.txt', $contents[1]->path, 'Media library item does not have the right path');
$this->assertEquals('/october.png', $contents[1]->path, 'Media library item does not have the right path');
$this->assertNotEmpty($contents[1]->lastModified, 'Media library item last modified is empty');
$this->assertNotEmpty($contents[1]->size, 'Media library item size is empty');
$this->assertEquals('file', $contents[2]->type, 'Media library item does not have the right type');
$this->assertEquals('/text.txt', $contents[2]->path, 'Media library item does not have the right path');
$this->assertNotEmpty($contents[2]->lastModified, 'Media library item last modified is empty');
$this->assertNotEmpty($contents[2]->size, 'Media library item size is empty');
}
protected function setUpStorage()

View File

@ -14,6 +14,12 @@ class UpdatesControllerTest extends TestCase
$controller = $this->getMockBuilder(Updates::class)->disableOriginalConstructor()->getMock();
$expectedVersions = [
'1.2.0' => [
'!!! Security update - see: https://octobercms.com',
],
'1.1.0' => [
'!!! Drop support for blog settings',
],
'1.0.5' => [
'Create blog settings table',
'Another update message',

View File

@ -24,7 +24,7 @@ class VersionManagerTest extends TestCase
$result = self::callProtectedMethod($manager, 'getLatestFileVersion', ['\October\\Tester']);
$this->assertNotNull($result);
$this->assertEquals('1.0.5', $result);
$this->assertEquals('1.2.0', $result);
}
public function testGetFileVersions()
@ -32,16 +32,24 @@ class VersionManagerTest extends TestCase
$manager = VersionManager::instance();
$result = self::callProtectedMethod($manager, 'getFileVersions', ['\October\\Tester']);
$this->assertCount(5, $result);
$this->assertCount(7, $result);
$this->assertArrayHasKey('1.0.1', $result);
$this->assertArrayHasKey('1.0.2', $result);
$this->assertArrayHasKey('1.0.3', $result);
$this->assertArrayHasKey('1.0.4', $result);
$this->assertArrayHasKey('1.0.5', $result);
$this->assertArrayHasKey('1.1.0', $result);
$this->assertArrayHasKey('1.2.0', $result);
$sample = $result['1.0.1'];
$comment = array_shift($sample);
$this->assertEquals("Added some upgrade file and some seeding", $comment);
$this->assertEquals('Added some upgrade file and some seeding', $sample[0]);
$sample = $result['1.1.0'];
$this->assertEquals('!!! Drop support for blog settings', $sample[0]);
$this->assertEquals('drop_blog_settings_table.php', $sample[1]);
$sample = $result['1.2.0'];
$this->assertEquals('!!! Security update - see: https://octobercms.com', $sample[0]);
/*
* Test junk file
@ -70,9 +78,11 @@ class VersionManagerTest extends TestCase
$manager = VersionManager::instance();
$result = self::callProtectedMethod($manager, 'getNewFileVersions', ['\October\\Tester', '1.0.3']);
$this->assertCount(2, $result);
$this->assertCount(4, $result);
$this->assertArrayHasKey('1.0.4', $result);
$this->assertArrayHasKey('1.0.5', $result);
$this->assertArrayHasKey('1.1.0', $result);
$this->assertArrayHasKey('1.2.0', $result);
/*
* When at version 0, should return everything
@ -80,12 +90,14 @@ class VersionManagerTest extends TestCase
$manager = VersionManager::instance();
$result = self::callProtectedMethod($manager, 'getNewFileVersions', ['\October\\Tester']);
$this->assertCount(5, $result);
$this->assertCount(7, $result);
$this->assertArrayHasKey('1.0.1', $result);
$this->assertArrayHasKey('1.0.2', $result);
$this->assertArrayHasKey('1.0.3', $result);
$this->assertArrayHasKey('1.0.4', $result);
$this->assertArrayHasKey('1.0.5', $result);
$this->assertArrayHasKey('1.1.0', $result);
$this->assertArrayHasKey('1.2.0', $result);
}
/**