api
This commit is contained in:
parent
6fb81c4df3
commit
63f1adb70d
5
.env
5
.env
|
|
@ -30,4 +30,7 @@ ROUTES_CACHE=false
|
|||
ASSET_CACHE=false
|
||||
DATABASE_TEMPLATES=false
|
||||
LINK_POLICY=detect
|
||||
ENABLE_CSRF=true
|
||||
ENABLE_CSRF=true
|
||||
JWT_SECRET=KErrClY2eG5aYIDvq17uN6aohm5z7wfbskDLViPNjHRDnT8cT0OR98sT0w317qzx
|
||||
|
||||
JWT_ALGO=HS256
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
config/database.php
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
# QuanKim/JwtAuth custom plugin for CakePHP
|
||||
[](https://travis-ci.org/QuanKim/cakephp-jwt-auth)
|
||||
[](https://codecov.io/github/QuanKim/cakephp-jwt-auth)
|
||||
[](https://packagist.org/packages/QuanKim/cakephp-jwt-auth)
|
||||
[](LICENSE.txt)
|
||||
## Installation
|
||||
|
||||
You can install this plugin into your CakePHP application using [composer](http://getcomposer.org).
|
||||
|
||||
The recommended way to install composer packages is:
|
||||
|
||||
```
|
||||
composer require quankim/cakephp-jwt-auth
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
In your app's `config/bootstrap.php` add:
|
||||
|
||||
```php
|
||||
// In config/bootstrap.php
|
||||
Plugin::load('QuanKim/JwtAuth');
|
||||
```
|
||||
|
||||
or using cake's console:
|
||||
|
||||
```sh
|
||||
./bin/cake plugin load QuanKim/JwtAuth
|
||||
```
|
||||
Migrate AuthToken table:
|
||||
```sh
|
||||
./bin/cake migrations migrate -p QuanKim/JwtAuth
|
||||
```
|
||||
## Configuration:
|
||||
|
||||
Setup `AuthComponent`:
|
||||
|
||||
```php
|
||||
// In your controller, for e.g. src/Api/AppController.php
|
||||
public function initialize()
|
||||
{
|
||||
parent::initialize();
|
||||
|
||||
$this->loadComponent('Auth', [
|
||||
'storage' => 'Memory',
|
||||
'authenticate', [
|
||||
'QuanKim/JwtAuth.Jwt' => [
|
||||
'userModel' => 'Users',
|
||||
'fields' => [
|
||||
'username' => 'id'
|
||||
],
|
||||
|
||||
'parameter' => 'token',
|
||||
|
||||
// Boolean indicating whether the "sub" claim of JWT payload
|
||||
// should be used to query the Users model and get user info.
|
||||
// If set to `false` JWT's payload is directly returned.
|
||||
'queryDatasource' => true,
|
||||
]
|
||||
],
|
||||
|
||||
'unauthorizedRedirect' => false,
|
||||
'checkAuthIn' => 'Controller.initialize',
|
||||
|
||||
// If you don't have a login action in your application set
|
||||
// 'loginAction' to false to prevent getting a MissingRouteException.
|
||||
'loginAction' => false
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
Setup `Config/app.php`
|
||||
Add in bottom of file:
|
||||
```php
|
||||
'AuthToken'=>[
|
||||
'expire'=>3600
|
||||
]
|
||||
```
|
||||
## Working
|
||||
|
||||
The authentication class checks for the token in two locations:
|
||||
|
||||
- `HTTP_AUTHORIZATION` environment variable:
|
||||
|
||||
It first checks if token is passed using `Authorization` request header.
|
||||
The value should be of form `Bearer <token>`. The `Authorization` header name
|
||||
and token prefix `Bearer` can be customzied using options `header` and `prefix`
|
||||
respectively.
|
||||
|
||||
**Note:** Some servers don't populate `$_SERVER['HTTP_AUTHORIZATION']` when
|
||||
`Authorization` header is set. So it's upto you to ensure that either
|
||||
`$_SERVER['HTTP_AUTHORIZATION']` or `$_ENV['HTTP_AUTHORIZATION']` is set.
|
||||
|
||||
For e.g. for apache you could use the following:
|
||||
|
||||
```
|
||||
RewriteEngine On
|
||||
RewriteCond %{HTTP:Authorization} ^(.*)
|
||||
RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]
|
||||
```
|
||||
|
||||
- The query string variable specified using `parameter` config:
|
||||
|
||||
Next it checks if the token is present in query string. The default variable
|
||||
name is `token` and can be customzied by using the `parameter` config shown
|
||||
above.
|
||||
|
||||
## Token Generation
|
||||
|
||||
You can use `\Firebase\JWT\JWT::encode()` of the [firebase/php-jwt](https://github.com/firebase/php-jwt)
|
||||
lib, which this plugin depends on, to generate tokens.
|
||||
|
||||
**The payload should have the "sub" (subject) claim whos value is used to query the
|
||||
Users model and find record matching the "id" field.**
|
||||
|
||||
Example:
|
||||
```php
|
||||
$access_token = JWT::encode([
|
||||
'sub' => $user['id'],
|
||||
'exp' => time() + $expire
|
||||
],Security::salt());
|
||||
$refresh_token = JWT::encode([
|
||||
'sub' => $user['id'],
|
||||
'ref'=>time()
|
||||
],Security::salt());
|
||||
$authToken = $this->Users->AuthToken->newEntity();
|
||||
$authToken->user_id = $user['id'];
|
||||
$authToken->access_token = $access_token;
|
||||
$authToken->refresh_token = $refresh_token;
|
||||
$this->Users->AuthToken->save($authToken);
|
||||
$this->set([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'access_token' => $access_token,
|
||||
'refresh_token'=> $refresh_token,
|
||||
'id'=>$user['id'],
|
||||
'username'=> $user['username'],
|
||||
'email'=> $user['email']
|
||||
],
|
||||
'_serialize' => ['success', 'data']
|
||||
]);
|
||||
```
|
||||
You can set the `queryDatasource` option to `false` to directly return the token's
|
||||
payload as user info without querying datasource for matching user record.
|
||||
|
||||
## Further reading
|
||||
|
||||
For an end to end usage example check out [this](http://www.bravo-kernel.com/2015/04/how-to-add-jwt-authentication-to-a-cakephp-3-rest-api/) blog post by Bravo Kernel.
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "quankim/cakephp-jwt-auth",
|
||||
"description": "QuanKim/JwtAuth plugin for CakePHP 3",
|
||||
"type": "cakephp-plugin",
|
||||
"require": {
|
||||
"php": ">=5.4.16",
|
||||
"quankim/php-jwt":"*"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name":"QuanKim",
|
||||
"role":"Author",
|
||||
"homepage":"https://github.com/quankim"
|
||||
}
|
||||
],
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "*"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"QuanKim\\JwtAuth\\": "src"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"QuanKim\\JwtAuth\\Test\\": "tests",
|
||||
"Cake\\Test\\": "./vendor/cakephp/cakephp/tests"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
use Migrations\AbstractMigration;
|
||||
|
||||
class AuthToken extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* Change Method.
|
||||
*
|
||||
* More information on this method is available here:
|
||||
* http://docs.phinx.org/en/latest/migrations.html#the-change-method
|
||||
* @return void
|
||||
*/
|
||||
public function change()
|
||||
{
|
||||
$table = $this->table('auth_token');
|
||||
$table->addColumn('user_id', 'integer', [
|
||||
'limit' => 11
|
||||
]);
|
||||
$table->addColumn('access_token', 'string', [
|
||||
'limit'=>512,
|
||||
'default' => null,
|
||||
'null' => false,
|
||||
]);
|
||||
$table->addColumn('refresh_token', 'string', [
|
||||
'limit'=>512,
|
||||
'default' => null,
|
||||
'null' => false,
|
||||
]);
|
||||
$table->addColumn('created', 'datetime', [
|
||||
'default' => 'CURRENT_TIMESTAMP',
|
||||
'null' => false,
|
||||
]);
|
||||
$table->addColumn('modified', 'datetime', [
|
||||
'default' => 'CURRENT_TIMESTAMP',
|
||||
'null' => false,
|
||||
'update'=>'CURRENT_TIMESTAMP'
|
||||
]);
|
||||
$table->addForeignKey('user_id','users','id');
|
||||
$table->create();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
use Cake\Routing\Router;
|
||||
|
||||
Router::plugin(
|
||||
'QuanKim/JwtAuth',
|
||||
['path' => '/auth'],
|
||||
function ($routes) {
|
||||
$routes->connect('/token',['controller'=>'Users','action'=>'token']);
|
||||
$routes->fallbacks('DashedRoute');
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit
|
||||
colors="true"
|
||||
processIsolation="false"
|
||||
stopOnFailure="false"
|
||||
syntaxCheck="false"
|
||||
bootstrap="./tests/bootstrap.php"
|
||||
>
|
||||
<php>
|
||||
<ini name="memory_limit" value="-1"/>
|
||||
<ini name="apc.enable_cli" value="1"/>
|
||||
</php>
|
||||
|
||||
<!-- Add any additional test suites you want to run here -->
|
||||
<testsuites>
|
||||
<testsuite name="QuanKim/JwtAuth Test Suite">
|
||||
<directory>./tests/TestCase</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<!-- Setup a listener for fixtures -->
|
||||
<listeners>
|
||||
<listener
|
||||
class="\Cake\TestSuite\Fixture\FixtureInjector"
|
||||
file="./vendor/cakephp/cakephp/src/TestSuite/Fixture/FixtureInjector.php">
|
||||
<arguments>
|
||||
<object class="\Cake\TestSuite\Fixture\FixtureManager" />
|
||||
</arguments>
|
||||
</listener>
|
||||
</listeners>
|
||||
|
||||
<!-- Prevent coverage reports from looking in tests and vendors -->
|
||||
<filter>
|
||||
<blacklist>
|
||||
<directory suffix=".php">./vendor/</directory>
|
||||
<directory suffix=".ctp">./vendor/</directory>
|
||||
|
||||
<directory suffix=".php">./tests/</directory>
|
||||
<directory suffix=".ctp">./tests/</directory>
|
||||
</blacklist>
|
||||
</filter>
|
||||
|
||||
</phpunit>
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
<?php
|
||||
namespace QuanKim\JwtAuth\Auth;
|
||||
|
||||
use Cake\Auth\BaseAuthenticate;
|
||||
use Cake\Controller\ComponentRegistry;
|
||||
use Cake\Core\Configure;
|
||||
use Cake\Log\Log;
|
||||
use Cake\Network\Request;
|
||||
use Cake\Network\Response;
|
||||
use Cake\ORM\TableRegistry;
|
||||
use Cake\Utility\Security;
|
||||
|
||||
use Exception;
|
||||
use QuanKim\PhpJwt\JWT;
|
||||
|
||||
/**
|
||||
* An authentication adapter for authenticating using JSON Web Tokens.
|
||||
*
|
||||
* ```
|
||||
* $this->Auth->config('authenticate', [
|
||||
* 'ADmad/JwtAuth.Jwt' => [
|
||||
* 'parameter' => 'token',
|
||||
* 'userModel' => 'Users',
|
||||
* 'fields' => [
|
||||
* 'username' => 'id'
|
||||
* ],
|
||||
* ]
|
||||
* ]);
|
||||
* ```
|
||||
*
|
||||
* @copyright 2015 ADmad
|
||||
* @license MIT
|
||||
*
|
||||
* @see http://jwt.io
|
||||
* @see http://tools.ietf.org/html/draft-ietf-oauth-json-web-token
|
||||
*/
|
||||
class JwtAuthenticate extends BaseAuthenticate
|
||||
{
|
||||
/**
|
||||
* Parsed token.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $_token;
|
||||
|
||||
/**
|
||||
* Payload data.
|
||||
*
|
||||
* @var object|null
|
||||
*/
|
||||
protected $_payload;
|
||||
|
||||
/**
|
||||
* Exception.
|
||||
*
|
||||
* @var \Exception
|
||||
*/
|
||||
protected $_error;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* Settings for this object.
|
||||
*
|
||||
* - `header` - Header name to check. Defaults to `'authorization'`.
|
||||
* - `prefix` - Token prefix. Defaults to `'bearer'`.
|
||||
* - `parameter` - The url parameter name of the token. Defaults to `token`.
|
||||
* First $_SERVER['HTTP_AUTHORIZATION'] is checked for token value.
|
||||
* Its value should be of form "Bearer <token>". If empty this query string
|
||||
* paramater is checked.
|
||||
* - `allowedAlgs` - List of supported verification algorithms.
|
||||
* Defaults to ['HS256']. See API of JWT::decode() for more info.
|
||||
* - `queryDatasource` - Boolean indicating whether the `sub` claim of JWT
|
||||
* token should be used to query the user model and get user record. If
|
||||
* set to `false` JWT's payload is directly retured. Defaults to `true`.
|
||||
* - `userModel` - The model name of users, defaults to `Users`.
|
||||
* - `fields` - Key `username` denotes the identifier field for fetching user
|
||||
* record. The `sub` claim of JWT must contain identifier value.
|
||||
* Defaults to ['username' => 'id'].
|
||||
* - `finder` - Finder method.
|
||||
* - `unauthenticatedException` - Fully namespaced exception name. Exception to
|
||||
* throw if authentication fails. Set to false to do nothing.
|
||||
* Defaults to '\Cake\Network\Exception\UnauthorizedException'.
|
||||
*
|
||||
* @param \Cake\Controller\ComponentRegistry $registry The Component registry
|
||||
* used on this request.
|
||||
* @param array $config Array of config to use.
|
||||
*/
|
||||
public function __construct(ComponentRegistry $registry, $config)
|
||||
{
|
||||
$this->config([
|
||||
'header' => 'authorization',
|
||||
'prefix' => 'bearer',
|
||||
'parameter' => 'token',
|
||||
'allowedAlgs' => ['HS256'],
|
||||
'queryDatasource' => true,
|
||||
'fields' => ['username' => 'id'],
|
||||
'unauthenticatedException' => '\Cake\Network\Exception\UnauthorizedException',
|
||||
]);
|
||||
|
||||
parent::__construct($registry, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user record based on info available in JWT.
|
||||
*
|
||||
* @param \Cake\Network\Request $request The request object.
|
||||
* @param \Cake\Network\Response $response Response object.
|
||||
*
|
||||
* @return bool|array User record array or false on failure.
|
||||
*/
|
||||
public function authenticate(Request $request, Response $response)
|
||||
{
|
||||
return $this->getUser($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user record based on info available in JWT.
|
||||
*
|
||||
* @param \Cake\Network\Request $request Request object.
|
||||
*
|
||||
* @return bool|array User record array or false on failure.
|
||||
*/
|
||||
public function getUser(Request $request)
|
||||
{
|
||||
$payload = $this->getPayload($request);
|
||||
if (!$this->_config['queryDatasource']) {
|
||||
return json_decode(json_encode($payload), true);
|
||||
}
|
||||
|
||||
if (!isset($payload->sub)) {
|
||||
return false;
|
||||
}
|
||||
$user = $this->_findUser($payload->sub);
|
||||
if (!$user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
unset($user[$this->_config['fields']['password']]);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get payload data.
|
||||
*
|
||||
* @param \Cake\Network\Request|null $request Request instance or null
|
||||
*
|
||||
* @return object|null Payload object on success, null on failurec
|
||||
*/
|
||||
public function getPayload($request = null)
|
||||
{
|
||||
if (!$request) {
|
||||
return $this->_payload;
|
||||
}
|
||||
|
||||
$payload = null;
|
||||
|
||||
$token = $this->getToken($request);
|
||||
if ($token) {
|
||||
$payload = $this->_decode($token);
|
||||
}
|
||||
// Check xem token co trong database hay khong
|
||||
$table = TableRegistry::get('AuthToken');
|
||||
$user = (isset($payload->sub)) ? $table->find('all')->where(['user_id'=>$payload->sub,'access_token'=>$token])->toArray() : [];
|
||||
if (count($user) == 0)
|
||||
return null;
|
||||
return $this->_payload = $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get token from header or query string.
|
||||
*
|
||||
* @param \Cake\Network\Request|null $request Request object.
|
||||
*
|
||||
* @return string|null Token string if found else null.
|
||||
*/
|
||||
public function getToken($request = null)
|
||||
{
|
||||
$config = $this->_config;
|
||||
|
||||
if (!$request) {
|
||||
return $this->_token;
|
||||
}
|
||||
|
||||
$header = $request->header($config['header']);
|
||||
if ($header) {
|
||||
return $this->_token = str_ireplace($config['prefix'] . ' ', '', $header);
|
||||
}
|
||||
|
||||
if (!empty($this->_config['parameter'])) {
|
||||
$token = $request->query($this->_config['parameter']);
|
||||
}
|
||||
|
||||
return $this->_token = $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode JWT token.
|
||||
*
|
||||
* @param string $token JWT token to decode.
|
||||
*
|
||||
* @return object|null The JWT's payload as a PHP object, null on failure.
|
||||
*/
|
||||
protected function _decode($token)
|
||||
{
|
||||
try {
|
||||
$payload = JWT::decode($token, Security::salt(), $this->_config['allowedAlgs']);
|
||||
|
||||
return $payload;
|
||||
} catch (Exception $e) {
|
||||
if (Configure::read('debug')) {
|
||||
throw $e;
|
||||
}
|
||||
$this->_error = $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles an unauthenticated access attempt. Depending on value of config
|
||||
* `unauthenticatedException` either throws the specified exception or returns
|
||||
* null.
|
||||
*
|
||||
* @param \Cake\Network\Request $request A request object.
|
||||
* @param \Cake\Network\Response $response A response object.
|
||||
*
|
||||
* @throws \Cake\Network\Exception\UnauthorizedException Or any other
|
||||
* configured exception.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function unauthenticated(Request $request, Response $response)
|
||||
{
|
||||
if (!$this->_config['unauthenticatedException']) {
|
||||
return;
|
||||
}
|
||||
|
||||
$message = $this->_error ? $this->_error->getMessage() : $this->_registry->Auth->_config['authError'];
|
||||
|
||||
$exception = new $this->_config['unauthenticatedException']($message);
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Created by PhpStorm.
|
||||
* User: QuânKim
|
||||
* Date: 7/4/2016
|
||||
* Time: 12:53 AM
|
||||
*/
|
||||
namespace QuanKim\JwtAuth\Controller;
|
||||
|
||||
use App\Controller\AppController as BaseController;
|
||||
|
||||
class AppController extends BaseController
|
||||
{
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
/**
|
||||
* Created by PhpStorm.
|
||||
* User: QuânKim
|
||||
* Date: 7/4/2016
|
||||
* Time: 12:55 AM
|
||||
*/
|
||||
|
||||
namespace QuanKim\JwtAuth\Controller;
|
||||
|
||||
|
||||
use Cake\Core\Configure;
|
||||
use Cake\ORM\TableRegistry;
|
||||
use Cake\Utility\Security;
|
||||
use QuanKim\PhpJwt\JWT;
|
||||
|
||||
class UsersController extends AppController
|
||||
{
|
||||
public function token(){
|
||||
if ($this->request->is('post')){
|
||||
$table = TableRegistry::get('AuthToken');
|
||||
$refresh_token = $this->request->data('refresh_token');
|
||||
$authToken = $table->find('all')->where(['refresh_token'=>$refresh_token])->first();
|
||||
if ($authToken) {
|
||||
$expire = (!is_null(Configure::read('AuthToken.expire'))) ? Configure::read('AuthToken.expire') : 3600;
|
||||
$access_token = JWT::encode([
|
||||
'sub' => $authToken['user_id'],
|
||||
'exp' => time() + $expire
|
||||
],Security::salt());
|
||||
$refresh_token = JWT::encode([
|
||||
'sub' => $authToken['user_id'],
|
||||
'ref'=>time()
|
||||
],Security::salt());
|
||||
$authToken->access_token = $access_token;
|
||||
$authToken->refresh_token = $refresh_token;
|
||||
$table->save($authToken);
|
||||
$this->set([
|
||||
'success'=>true,
|
||||
'data'=>[
|
||||
'access_token'=>$access_token,
|
||||
'refresh_token'=>$refresh_token
|
||||
],
|
||||
'_serialize' => ['success', 'data']
|
||||
]);
|
||||
} else {
|
||||
$this->set([
|
||||
'success'=>false,
|
||||
'refresh_token_expired'=>true,
|
||||
'_serialize' => ['success','refresh_token_expired']
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Võ Hồng Quân
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# QuanKim/JwtAuth plugin for CakePHP
|
||||
|
||||
## Installation
|
||||
|
||||
You can install this plugin into your CakePHP application using [composer](http://getcomposer.org).
|
||||
|
||||
The recommended way to install composer packages is:
|
||||
|
||||
```
|
||||
composer require quankim/php-jwt
|
||||
```
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "quankim/php-jwt",
|
||||
"description": "QuanKim/PhpJwt plugin for CakePHP",
|
||||
"type": "cakephp-plugin",
|
||||
"require": {
|
||||
"php": ">=5.4.16"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name":"QuanKim",
|
||||
"role":"Author",
|
||||
"homepage":"https://github.com/quankim"
|
||||
}
|
||||
],
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "*"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"QuanKim\\PhpJwt\\": "src"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"QuanKim\\PhpJwt\\Test\\": "tests",
|
||||
"Cake\\Test\\": "./vendor/cakephp/cakephp/tests"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
use Cake\Routing\Router;
|
||||
|
||||
Router::plugin(
|
||||
'QuanKim/PhpJwt',
|
||||
['path' => '/quan-kim/php-jwt'],
|
||||
function ($routes) {
|
||||
$routes->fallbacks('DashedRoute');
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit
|
||||
colors="true"
|
||||
processIsolation="false"
|
||||
stopOnFailure="false"
|
||||
syntaxCheck="false"
|
||||
bootstrap="./tests/bootstrap.php"
|
||||
>
|
||||
<php>
|
||||
<ini name="memory_limit" value="-1"/>
|
||||
<ini name="apc.enable_cli" value="1"/>
|
||||
</php>
|
||||
|
||||
<!-- Add any additional test suites you want to run here -->
|
||||
<testsuites>
|
||||
<testsuite name="QuanKim/PhpJwt Test Suite">
|
||||
<directory>./tests/TestCase</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<!-- Setup a listener for fixtures -->
|
||||
<listeners>
|
||||
<listener
|
||||
class="\Cake\TestSuite\Fixture\FixtureInjector"
|
||||
file="./vendor/cakephp/cakephp/src/TestSuite/Fixture/FixtureInjector.php">
|
||||
<arguments>
|
||||
<object class="\Cake\TestSuite\Fixture\FixtureManager" />
|
||||
</arguments>
|
||||
</listener>
|
||||
</listeners>
|
||||
|
||||
<!-- Prevent coverage reports from looking in tests and vendors -->
|
||||
<filter>
|
||||
<blacklist>
|
||||
<directory suffix=".php">./vendor/</directory>
|
||||
<directory suffix=".ctp">./vendor/</directory>
|
||||
|
||||
<directory suffix=".php">./tests/</directory>
|
||||
<directory suffix=".ctp">./tests/</directory>
|
||||
</blacklist>
|
||||
</filter>
|
||||
|
||||
</phpunit>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
namespace QuanKim\PhpJwt;
|
||||
|
||||
class BeforeValidException extends \UnexpectedValueException
|
||||
{
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
namespace QuanKim\PhpJwt;
|
||||
|
||||
class ExpiredException extends \UnexpectedValueException
|
||||
{
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,397 @@
|
|||
<?php
|
||||
|
||||
namespace QuanKim\PhpJwt;
|
||||
use Cake\Core\Configure;
|
||||
use Cake\ORM\TableRegistry;
|
||||
use \DomainException;
|
||||
use \InvalidArgumentException;
|
||||
use \UnexpectedValueException;
|
||||
use \DateTime;
|
||||
use Cake\Utility\Security;
|
||||
/**
|
||||
* JSON Web Token implementation, based on this spec:
|
||||
* http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* @category Authentication
|
||||
* @package Authentication_JWT
|
||||
* @author Neuman Vong <neuman@twilio.com>
|
||||
* @author Anant Narayanan <anant@php.net>
|
||||
* @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD
|
||||
* @link https://github.com/quankim
|
||||
*/
|
||||
class JWT
|
||||
{
|
||||
|
||||
/**
|
||||
* When checking nbf, iat or expiration times,
|
||||
* we want to provide some extra leeway time to
|
||||
* account for clock skew.
|
||||
*/
|
||||
public static $leeway = 0;
|
||||
|
||||
public static $supported_algs = array(
|
||||
'HS256' => array('hash_hmac', 'SHA256'),
|
||||
'HS512' => array('hash_hmac', 'SHA512'),
|
||||
'HS384' => array('hash_hmac', 'SHA384'),
|
||||
'RS256' => array('openssl', 'SHA256'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Decodes a JWT string into a PHP object.
|
||||
*
|
||||
* @param string $jwt The JWT
|
||||
* @param string|array|null $key The key, or map of keys.
|
||||
* If the algorithm used is asymmetric, this is the public key
|
||||
* @param array $allowed_algs List of supported verification algorithms
|
||||
* Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'
|
||||
*
|
||||
* @return object The JWT's payload as a PHP object
|
||||
*
|
||||
* @throws DomainException Algorithm was not provided
|
||||
* @throws UnexpectedValueException Provided JWT was invalid
|
||||
* @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed
|
||||
* @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf'
|
||||
* @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat'
|
||||
* @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim
|
||||
*
|
||||
* @uses jsonDecode
|
||||
* @uses urlsafeB64Decode
|
||||
*/
|
||||
public static function decode($jwt, $key, $allowed_algs = array())
|
||||
{
|
||||
if (empty($key)) {
|
||||
throw new InvalidArgumentException('Key may not be empty');
|
||||
}
|
||||
$tks = explode('.', $jwt);
|
||||
if (count($tks) != 3) {
|
||||
throw new UnexpectedValueException('Wrong number of segments');
|
||||
}
|
||||
list($headb64, $bodyb64, $cryptob64) = $tks;
|
||||
if (null === ($header = JWT::jsonDecode(JWT::urlsafeB64Decode($headb64)))) {
|
||||
throw new UnexpectedValueException('Invalid header encoding');
|
||||
}
|
||||
if (null === $payload = JWT::jsonDecode(JWT::urlsafeB64Decode($bodyb64))) {
|
||||
throw new UnexpectedValueException('Invalid claims encoding');
|
||||
}
|
||||
$sig = JWT::urlsafeB64Decode($cryptob64);
|
||||
|
||||
if (empty($header->alg)) {
|
||||
throw new DomainException('Empty algorithm');
|
||||
}
|
||||
if (empty(self::$supported_algs[$header->alg])) {
|
||||
throw new DomainException('Algorithm not supported');
|
||||
}
|
||||
if (!is_array($allowed_algs) || !in_array($header->alg, $allowed_algs)) {
|
||||
throw new DomainException('Algorithm not allowed');
|
||||
}
|
||||
if (is_array($key) || $key instanceof \ArrayAccess) {
|
||||
if (isset($header->kid)) {
|
||||
$key = $key[$header->kid];
|
||||
} else {
|
||||
throw new DomainException('"kid" empty, unable to lookup correct key');
|
||||
}
|
||||
}
|
||||
|
||||
// Check the signature
|
||||
if (!JWT::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) {
|
||||
throw new SignatureInvalidException('Signature verification failed');
|
||||
}
|
||||
|
||||
// Check if the nbf if it is defined. This is the time that the
|
||||
// token can actually be used. If it's not yet that time, abort.
|
||||
if (isset($payload->nbf) && $payload->nbf > (time() + self::$leeway)) {
|
||||
throw new BeforeValidException(
|
||||
'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->nbf)
|
||||
);
|
||||
}
|
||||
|
||||
// Check that this token has been created before 'now'. This prevents
|
||||
// using tokens that have been created for later use (and haven't
|
||||
// correctly used the nbf claim).
|
||||
if (isset($payload->iat) && $payload->iat > (time() + self::$leeway)) {
|
||||
throw new BeforeValidException(
|
||||
'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->iat)
|
||||
);
|
||||
}
|
||||
// Check if refresh token return access t
|
||||
if (isset($payload->ref)) {
|
||||
header('Content-Type:application/json');
|
||||
$table = TableRegistry::get('AuthToken');
|
||||
$authToken = $table->find('all')->where(['user_id'=>$payload->sub,'refresh_token'=>$jwt])->first();
|
||||
if ($authToken) {
|
||||
$expire = (!is_null(Configure::read('AuthToken.expire'))) ? Configure::read('AuthToken.expire') : 3600;
|
||||
$access_token = JWT::encode([
|
||||
'sub' => $authToken['user_id'],
|
||||
'exp' => time() + $expire
|
||||
],Security::salt());
|
||||
$refresh_token = JWT::encode([
|
||||
'sub' => $authToken['user_id'],
|
||||
'ref'=>time()
|
||||
],Security::salt());
|
||||
$authToken->access_token = $access_token;
|
||||
$authToken->refresh_token = $refresh_token;
|
||||
$table->save($authToken);
|
||||
echo json_encode([
|
||||
'success'=>true,
|
||||
'data'=>[
|
||||
'access_token'=>$access_token,
|
||||
'refresh_token'=>$refresh_token
|
||||
]
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success'=>false,
|
||||
'refresh_token_expired'=>true
|
||||
]);
|
||||
}
|
||||
exit();
|
||||
}
|
||||
// Check if this token has expired.
|
||||
if (isset($payload->exp) && (time() - self::$leeway) >= $payload->exp) {
|
||||
//throw new ExpiredException('Expired token');
|
||||
header('Content-Type:application/json');
|
||||
echo json_encode([
|
||||
'success'=>false,
|
||||
'access_token_expired'=>true
|
||||
]);
|
||||
exit();
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts and signs a PHP object or array into a JWT string.
|
||||
*
|
||||
* @param object|array $payload PHP object or array
|
||||
* @param string $key The secret key.
|
||||
* If the algorithm used is asymmetric, this is the private key
|
||||
* @param string $alg The signing algorithm.
|
||||
* Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'
|
||||
* @param array $head An array with header elements to attach
|
||||
*
|
||||
* @return string A signed JWT
|
||||
*
|
||||
* @uses jsonEncode
|
||||
* @uses urlsafeB64Encode
|
||||
*/
|
||||
public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $head = null)
|
||||
{
|
||||
$header = array('typ' => 'JWT', 'alg' => $alg);
|
||||
if ($keyId !== null) {
|
||||
$header['kid'] = $keyId;
|
||||
}
|
||||
if ( isset($head) && is_array($head) ) {
|
||||
$header = array_merge($head, $header);
|
||||
}
|
||||
$segments = array();
|
||||
$segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($header));
|
||||
$segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($payload));
|
||||
$signing_input = implode('.', $segments);
|
||||
|
||||
$signature = JWT::sign($signing_input, $key, $alg);
|
||||
$segments[] = JWT::urlsafeB64Encode($signature);
|
||||
|
||||
return implode('.', $segments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign a string with a given key and algorithm.
|
||||
*
|
||||
* @param string $msg The message to sign
|
||||
* @param string|resource $key The secret key
|
||||
* @param string $alg The signing algorithm.
|
||||
* Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'
|
||||
*
|
||||
* @return string An encrypted message
|
||||
*
|
||||
* @throws DomainException Unsupported algorithm was specified
|
||||
*/
|
||||
public static function sign($msg, $key, $alg = 'HS256')
|
||||
{
|
||||
if (empty(self::$supported_algs[$alg])) {
|
||||
throw new DomainException('Algorithm not supported');
|
||||
}
|
||||
list($function, $algorithm) = self::$supported_algs[$alg];
|
||||
switch($function) {
|
||||
case 'hash_hmac':
|
||||
return hash_hmac($algorithm, $msg, $key, true);
|
||||
case 'openssl':
|
||||
$signature = '';
|
||||
$success = openssl_sign($msg, $signature, $key, $algorithm);
|
||||
if (!$success) {
|
||||
throw new DomainException("OpenSSL unable to sign data");
|
||||
} else {
|
||||
return $signature;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a signature with the message, key and method. Not all methods
|
||||
* are symmetric, so we must have a separate verify and sign method.
|
||||
*
|
||||
* @param string $msg The original message (header and body)
|
||||
* @param string $signature The original signature
|
||||
* @param string|resource $key For HS*, a string key works. for RS*, must be a resource of an openssl public key
|
||||
* @param string $alg The algorithm
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @throws DomainException Invalid Algorithm or OpenSSL failure
|
||||
*/
|
||||
private static function verify($msg, $signature, $key, $alg)
|
||||
{
|
||||
if (empty(self::$supported_algs[$alg])) {
|
||||
throw new DomainException('Algorithm not supported');
|
||||
}
|
||||
|
||||
list($function, $algorithm) = self::$supported_algs[$alg];
|
||||
switch($function) {
|
||||
case 'openssl':
|
||||
$success = openssl_verify($msg, $signature, $key, $algorithm);
|
||||
if (!$success) {
|
||||
throw new DomainException("OpenSSL unable to verify data: " . openssl_error_string());
|
||||
} else {
|
||||
return $signature;
|
||||
}
|
||||
case 'hash_hmac':
|
||||
default:
|
||||
$hash = hash_hmac($algorithm, $msg, $key, true);
|
||||
if (function_exists('hash_equals')) {
|
||||
return hash_equals($signature, $hash);
|
||||
}
|
||||
$len = min(self::safeStrlen($signature), self::safeStrlen($hash));
|
||||
|
||||
$status = 0;
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$status |= (ord($signature[$i]) ^ ord($hash[$i]));
|
||||
}
|
||||
$status |= (self::safeStrlen($signature) ^ self::safeStrlen($hash));
|
||||
|
||||
return ($status === 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a JSON string into a PHP object.
|
||||
*
|
||||
* @param string $input JSON string
|
||||
*
|
||||
* @return object Object representation of JSON string
|
||||
*
|
||||
* @throws DomainException Provided string was invalid JSON
|
||||
*/
|
||||
public static function jsonDecode($input)
|
||||
{
|
||||
if (version_compare(PHP_VERSION, '5.4.0', '>=') && !(defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) {
|
||||
/** In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you
|
||||
* to specify that large ints (like Steam Transaction IDs) should be treated as
|
||||
* strings, rather than the PHP default behaviour of converting them to floats.
|
||||
*/
|
||||
$obj = json_decode($input, false, 512, JSON_BIGINT_AS_STRING);
|
||||
} else {
|
||||
/** Not all servers will support that, however, so for older versions we must
|
||||
* manually detect large ints in the JSON string and quote them (thus converting
|
||||
*them to strings) before decoding, hence the preg_replace() call.
|
||||
*/
|
||||
$max_int_length = strlen((string) PHP_INT_MAX) - 1;
|
||||
$json_without_bigints = preg_replace('/:\s*(-?\d{'.$max_int_length.',})/', ': "$1"', $input);
|
||||
$obj = json_decode($json_without_bigints);
|
||||
}
|
||||
|
||||
if (function_exists('json_last_error') && $errno = json_last_error()) {
|
||||
JWT::handleJsonError($errno);
|
||||
} elseif ($obj === null && $input !== 'null') {
|
||||
throw new DomainException('Null result with non-null input');
|
||||
}
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a PHP object into a JSON string.
|
||||
*
|
||||
* @param object|array $input A PHP object or array
|
||||
*
|
||||
* @return string JSON representation of the PHP object or array
|
||||
*
|
||||
* @throws DomainException Provided object could not be encoded to valid JSON
|
||||
*/
|
||||
public static function jsonEncode($input)
|
||||
{
|
||||
$json = json_encode($input);
|
||||
if (function_exists('json_last_error') && $errno = json_last_error()) {
|
||||
JWT::handleJsonError($errno);
|
||||
} elseif ($json === 'null' && $input !== null) {
|
||||
throw new DomainException('Null result with non-null input');
|
||||
}
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a string with URL-safe Base64.
|
||||
*
|
||||
* @param string $input A Base64 encoded string
|
||||
*
|
||||
* @return string A decoded string
|
||||
*/
|
||||
public static function urlsafeB64Decode($input)
|
||||
{
|
||||
$remainder = strlen($input) % 4;
|
||||
if ($remainder) {
|
||||
$padlen = 4 - $remainder;
|
||||
$input .= str_repeat('=', $padlen);
|
||||
}
|
||||
return base64_decode(strtr($input, '-_', '+/'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a string with URL-safe Base64.
|
||||
*
|
||||
* @param string $input The string you want encoded
|
||||
*
|
||||
* @return string The base64 encode of what you passed in
|
||||
*/
|
||||
public static function urlsafeB64Encode($input)
|
||||
{
|
||||
return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to create a JSON error.
|
||||
*
|
||||
* @param int $errno An error number from json_last_error()
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function handleJsonError($errno)
|
||||
{
|
||||
$messages = array(
|
||||
JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
|
||||
JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
|
||||
JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON'
|
||||
);
|
||||
throw new DomainException(
|
||||
isset($messages[$errno])
|
||||
? $messages[$errno]
|
||||
: 'Unknown JSON error: ' . $errno
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of bytes in cryptographic strings.
|
||||
*
|
||||
* @param string
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private static function safeStrlen($str)
|
||||
{
|
||||
if (function_exists('mb_strlen')) {
|
||||
return mb_strlen($str, '8bit');
|
||||
}
|
||||
return strlen($str);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
namespace QuanKim\PhpJwt;
|
||||
|
||||
class SignatureInvalidException extends \UnexpectedValueException
|
||||
{
|
||||
|
||||
}
|
||||
|
|
@ -25,41 +25,49 @@
|
|||
"source": "https://github.com/octobercms/october"
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.0.8",
|
||||
"october/rain": "1.0.*",
|
||||
"october/system": "1.0.*",
|
||||
"october/backend": "1.0.*",
|
||||
"october/cms": "1.0.*",
|
||||
"laravel/framework": "~5.5.40",
|
||||
"php": ">=7.2.9",
|
||||
"october/rain": "1.1.*",
|
||||
"october/system": "1.1.*",
|
||||
"october/backend": "1.1.*",
|
||||
"october/cms": "1.1.*",
|
||||
"laravel/framework": "~6.0",
|
||||
"lovata/oc-toolbox-plugin": "^1.34",
|
||||
"lovata/oc-shopaholic-plugin": "^1.30"
|
||||
"lovata/oc-shopaholic-plugin": "^1.30",
|
||||
"renatio/dynamicpdf-plugin": "^6.0",
|
||||
"quankim/cakephp-jwt-auth": "^1.1",
|
||||
"php-open-source-saver/jwt-auth": "^2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"fzaninotto/faker": "~1.7",
|
||||
"phpunit/phpunit": "~6.5",
|
||||
"phpunit/phpunit-selenium": "~1.2",
|
||||
"meyfa/phpunit-assert-gd": "1.1.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",
|
||||
"kharanenka/laravel-scope-active": "1.0.*"
|
||||
"dms/phpunit-arraysubset-asserts": "^0.1.0|^0.2.1"
|
||||
},
|
||||
"autoload-dev": {
|
||||
"classmap": [
|
||||
"tests/concerns/InteractsWithAuthentication.php",
|
||||
"tests/fixtures/backend/models/UserFixture.php",
|
||||
"tests/TestCase.php",
|
||||
"tests/UiTestCase.php",
|
||||
"tests/PluginTestCase.php"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"post-create-project-cmd": [
|
||||
"php artisan key:generate",
|
||||
"php artisan package:discover"
|
||||
"php artisan key:generate"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"php artisan october:util set build",
|
||||
"php artisan package:discover"
|
||||
"php artisan october:version"
|
||||
],
|
||||
"test": [
|
||||
"phpunit --stop-on-failure"
|
||||
],
|
||||
"lint": [
|
||||
"parallel-lint --exclude vendor --exclude storage --exclude tests/fixtures/plugins/testvendor/goto/Plugin.php ."
|
||||
],
|
||||
"sniff": [
|
||||
"phpcs --colors -nq --report=\"full\" --extensions=\"php\""
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -57,9 +57,9 @@ return [
|
|||
'engine' => 'InnoDB',
|
||||
'host' => 'localhost',
|
||||
'port' => 3306,
|
||||
'database' => 'sapaly_october',
|
||||
'username' => 'sapaly',
|
||||
'password' => '22MOcP6^I#8tI5ovcXluXDT#%vWC^CtsFvKHOygi8TMnzaMuLYx@123',
|
||||
'database' => 'sapaly_git',
|
||||
'username' => 'root',
|
||||
'password' => '',
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ class ServiceProvider extends ModuleServiceProvider
|
|||
$combiner->registerBundle('~/modules/backend/formwidgets/colorpicker/assets/less/colorpicker.less');
|
||||
$combiner->registerBundle('~/modules/backend/formwidgets/permissioneditor/assets/less/permissioneditor.less');
|
||||
$combiner->registerBundle('~/modules/backend/formwidgets/markdowneditor/assets/less/markdowneditor.less');
|
||||
$combiner->registerBundle('~/modules/backend/formwidgets/sensitive/assets/less/sensitive.less');
|
||||
|
||||
/*
|
||||
* Rich Editor is protected by DRM
|
||||
|
|
@ -164,10 +165,16 @@ class ServiceProvider extends ModuleServiceProvider
|
|||
'backend.manage_editor' => [
|
||||
'label' => 'system::lang.permissions.manage_editor',
|
||||
'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',
|
||||
'roles' => UserRole::CODE_DEVELOPER,
|
||||
],
|
||||
'media.manage_media' => [
|
||||
'label' => 'backend::lang.permissions.manage_media',
|
||||
|
|
@ -202,6 +209,7 @@ class ServiceProvider extends ModuleServiceProvider
|
|||
$manager->registerFormWidget('Backend\FormWidgets\TagList', 'taglist');
|
||||
$manager->registerFormWidget('Backend\FormWidgets\MediaFinder', 'mediafinder');
|
||||
$manager->registerFormWidget('Backend\FormWidgets\NestedForm', 'nestedform');
|
||||
$manager->registerFormWidget('Backend\FormWidgets\Sensitive', 'sensitive');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -646,7 +646,7 @@ class FormController extends ControllerBehavior
|
|||
* View helper to render the form fields belonging to the
|
||||
* secondary tabs section.
|
||||
*
|
||||
* <?= $this->formRenderPrimaryTabs() ?>
|
||||
* <?= $this->formRenderSecondaryTabs() ?>
|
||||
*
|
||||
* @return string HTML markup
|
||||
* @throws \October\Rain\Exception\ApplicationException if the Form Widget isn't set
|
||||
|
|
|
|||
|
|
@ -11,10 +11,11 @@ use Backend\Behaviors\ImportExportController\TranscodeFilter;
|
|||
use Illuminate\Database\Eloquent\MassAssignmentException;
|
||||
use League\Csv\Reader as CsvReader;
|
||||
use League\Csv\Writer as CsvWriter;
|
||||
use October\Rain\Parse\League\EscapeFormula as CsvEscapeFormula;
|
||||
use League\Csv\EscapeFormula as CsvEscapeFormula;
|
||||
use ApplicationException;
|
||||
use SplTempFileObject;
|
||||
use Exception;
|
||||
use League\Csv\Statement;
|
||||
|
||||
/**
|
||||
* Adds features for importing and exporting data.
|
||||
|
|
@ -250,10 +251,13 @@ class ImportExportController extends ControllerBehavior
|
|||
$reader = $this->createCsvReader($path);
|
||||
|
||||
if (post('first_row_titles')) {
|
||||
$reader->setOffset(1);
|
||||
$reader->setHeaderOffset(1);
|
||||
}
|
||||
|
||||
$result = $reader->setLimit(50)->fetchColumn((int) $columnId);
|
||||
$result = (new Statement())
|
||||
->limit(50)
|
||||
->process($reader)
|
||||
->fetchColumn((int) $columnId);
|
||||
$data = iterator_to_array($result, false);
|
||||
|
||||
/*
|
||||
|
|
@ -624,9 +628,7 @@ class ImportExportController extends ControllerBehavior
|
|||
$csv->setDelimiter($options['delimiter']);
|
||||
$csv->setEnclosure($options['enclosure']);
|
||||
$csv->setEscape($options['escape']);
|
||||
|
||||
// Temporary until upgrading to league/csv >= 9.1.0 (will be $csv->addFormatter($formatter))
|
||||
$formatter = new CsvEscapeFormula();
|
||||
$csv->addFormatter(new CsvEscapeFormula());
|
||||
|
||||
/*
|
||||
* Add headers
|
||||
|
|
@ -662,9 +664,6 @@ class ImportExportController extends ControllerBehavior
|
|||
$record[] = $value;
|
||||
}
|
||||
|
||||
// Temporary until upgrading to league/csv >= 9.1.0
|
||||
$record = $formatter($record);
|
||||
|
||||
$csv->insertOne($record);
|
||||
}
|
||||
|
||||
|
|
@ -808,9 +807,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']),
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ class ListController extends ControllerBehavior
|
|||
'recordUrl',
|
||||
'recordOnClick',
|
||||
'recordsPerPage',
|
||||
'perPageOptions',
|
||||
'showPageNumbers',
|
||||
'noRecordsMessage',
|
||||
'defaultSort',
|
||||
|
|
@ -297,16 +298,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 +309,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 +349,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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -1113,7 +1115,7 @@ class RelationController extends ControllerBehavior
|
|||
$this->relationObject->add($newModel, $sessionKey);
|
||||
}
|
||||
elseif ($this->viewMode == 'single') {
|
||||
$newModel = $this->manageWidget->model;
|
||||
$newModel = $this->viewModel = $this->viewWidget->model = $this->manageWidget->model;
|
||||
$this->viewWidget->setFormValues($saveData);
|
||||
|
||||
/*
|
||||
|
|
@ -1123,6 +1125,15 @@ class RelationController extends ControllerBehavior
|
|||
$newModel->save(null, $this->manageWidget->getSessionKey());
|
||||
}
|
||||
|
||||
if ($this->relationType === 'hasOne') {
|
||||
// Unassign previous relation if one is already assigned
|
||||
$relation = $this->relationObject->getParent()->{$this->relationName} ?? null;
|
||||
|
||||
if ($relation) {
|
||||
$this->relationObject->remove($relation, $sessionKey);
|
||||
}
|
||||
}
|
||||
|
||||
$this->relationObject->add($newModel, $sessionKey);
|
||||
|
||||
/*
|
||||
|
|
@ -1157,10 +1168,9 @@ class RelationController extends ControllerBehavior
|
|||
}
|
||||
}
|
||||
elseif ($this->viewMode == 'single') {
|
||||
$this->viewModel = $this->manageWidget->model;
|
||||
|
||||
$this->manageWidget->setFormValues($saveData);
|
||||
$this->manageWidget->model->save(null, $this->manageWidget->getSessionKey());
|
||||
$this->viewWidget->setFormValues($saveData);
|
||||
$this->viewModel->save(null, $this->manageWidget->getSessionKey());
|
||||
}
|
||||
|
||||
return $this->relationRefresh();
|
||||
|
|
@ -1241,6 +1251,15 @@ class RelationController extends ControllerBehavior
|
|||
*/
|
||||
elseif ($this->viewMode == 'single') {
|
||||
if ($recordId && ($model = $this->relationModel->find($recordId))) {
|
||||
if ($this->relationType === 'hasOne') {
|
||||
// Unassign previous relation if one is already assigned
|
||||
$relation = $this->relationObject->getParent()->{$this->relationName} ?? null;
|
||||
|
||||
if ($relation) {
|
||||
$this->relationObject->remove($relation, $sessionKey);
|
||||
}
|
||||
}
|
||||
|
||||
$this->relationObject->add($model, $sessionKey);
|
||||
$this->viewWidget->setFormValues($model->attributes);
|
||||
|
||||
|
|
@ -1309,7 +1328,11 @@ class RelationController extends ControllerBehavior
|
|||
}
|
||||
}
|
||||
|
||||
// Reinitialise the form with a blank model
|
||||
$this->initRelation($this->model);
|
||||
|
||||
$this->viewWidget->setFormValues([]);
|
||||
$this->viewModel = $this->relationModel;
|
||||
}
|
||||
|
||||
return $this->relationRefresh();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -111,6 +111,14 @@ class BackendController extends ControllerBase
|
|||
self::extendableExtendCallback($callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function callAction($method, $parameters)
|
||||
{
|
||||
return parent::callAction($method, array_values($parameters));
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass unhandled URLs to the CMS Controller, if it exists
|
||||
*
|
||||
|
|
@ -210,7 +218,7 @@ class BackendController extends ControllerBase
|
|||
* Look for a Plugin controller
|
||||
*/
|
||||
if (count($params) >= 2) {
|
||||
list($author, $plugin) = $params;
|
||||
[$author, $plugin] = $params;
|
||||
|
||||
$pluginCode = ucfirst($author) . '.' . ucfirst($plugin);
|
||||
if (PluginManager::instance()->isDisabled($pluginCode)) {
|
||||
|
|
|
|||
|
|
@ -617,7 +617,7 @@ class Controller extends ControllerBase
|
|||
$pageHandler = $this->action . '_' . $handler;
|
||||
|
||||
if ($this->methodExists($pageHandler)) {
|
||||
$result = call_user_func_array([$this, $pageHandler], $this->params);
|
||||
$result = call_user_func_array([$this, $pageHandler], array_values($this->params));
|
||||
return $result ?: true;
|
||||
}
|
||||
|
||||
|
|
@ -625,7 +625,7 @@ class Controller extends ControllerBase
|
|||
* Process page global handler (onSomething)
|
||||
*/
|
||||
if ($this->methodExists($handler)) {
|
||||
$result = call_user_func_array([$this, $handler], $this->params);
|
||||
$result = call_user_func_array([$this, $handler], array_values($this->params));
|
||||
return $result ?: true;
|
||||
}
|
||||
|
||||
|
|
@ -662,7 +662,7 @@ class Controller extends ControllerBase
|
|||
{
|
||||
$this->addViewPath($widget->getViewPaths());
|
||||
|
||||
$result = call_user_func_array([$widget, $handler], $this->params);
|
||||
$result = call_user_func_array([$widget, $handler], array_values($this->params));
|
||||
|
||||
$this->vars = $widget->vars + $this->vars;
|
||||
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ class FormField
|
|||
/**
|
||||
* @var string Specifies a comment to accompany the field
|
||||
*/
|
||||
public $comment;
|
||||
public $comment = '';
|
||||
|
||||
/**
|
||||
* @var string Specifies the comment position.
|
||||
|
|
@ -139,7 +139,7 @@ class FormField
|
|||
/**
|
||||
* @var string Specifies a message to display when there is no value supplied (placeholder).
|
||||
*/
|
||||
public $placeholder;
|
||||
public $placeholder = '';
|
||||
|
||||
/**
|
||||
* @var array Contains a list of attributes specified in the field configuration.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,23 +8,18 @@
|
|||
"authors": [
|
||||
{
|
||||
"name": "Alexey Bobkov",
|
||||
"email": "aleksey.bobkov@gmail.com"
|
||||
"email": "aleksey.bobkov@gmail.com",
|
||||
"role": "Co-founder"
|
||||
},
|
||||
{
|
||||
"name": "Samuel Georges",
|
||||
"email": "daftspunky@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Luke Towers",
|
||||
"email": "octobercms@luketowers.ca",
|
||||
"homepage": "https://luketowers.ca",
|
||||
"role": "Maintainer"
|
||||
"email": "daftspunky@gmail.com",
|
||||
"role": "Co-founder"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.0",
|
||||
"composer/installers": "~1.0",
|
||||
"october/rain": "~1.0.469"
|
||||
"php": ">=7.2",
|
||||
"composer/installers": "~1.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ use ApplicationException;
|
|||
use ValidationException;
|
||||
use Exception;
|
||||
use Config;
|
||||
use October\Rain\Foundation\Http\Middleware\CheckForTrustedHost;
|
||||
|
||||
/**
|
||||
* Authentication controller
|
||||
|
|
@ -147,6 +148,20 @@ class Auth extends Controller
|
|||
*/
|
||||
public function restore_onSubmit()
|
||||
{
|
||||
// Force Trusted Host verification on password reset link generation
|
||||
// regardless of config to protect against host header poisoning
|
||||
$trustedHosts = Config::get('app.trustedHosts', false);
|
||||
if ($trustedHosts === false) {
|
||||
$hosts = CheckForTrustedHost::processTrustedHosts(true);
|
||||
|
||||
if (count($hosts)) {
|
||||
Request::setTrustedHosts($hosts);
|
||||
|
||||
// Trigger the host validation logic
|
||||
Request::getHost();
|
||||
}
|
||||
}
|
||||
|
||||
$rules = [
|
||||
'login' => 'required|between:2,255'
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,32 @@
|
|||
<?php namespace Backend\Database\Seeds;
|
||||
|
||||
use Str;
|
||||
use Seeder;
|
||||
use Eloquent;
|
||||
use Backend\Database\Seeds\SeedSetupAdmin;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*
|
||||
* @return void
|
||||
* @return string
|
||||
*/
|
||||
public function run()
|
||||
{
|
||||
Eloquent::unguard();
|
||||
$shouldRandomizePassword = SeedSetupAdmin::$password === 'admin';
|
||||
$adminPassword = $shouldRandomizePassword ? Str::random(22) : SeedSetupAdmin::$password;
|
||||
|
||||
$this->call('Backend\Database\Seeds\SeedSetupAdmin');
|
||||
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 $shouldRandomizePassword ? 'The following password has been automatically generated for the "admin" account: '
|
||||
. "<fg=yellow;options=bold>${adminPassword}</>" : '';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -64,6 +64,11 @@ class FileUpload extends FormWidgetBase
|
|||
*/
|
||||
public $maxFilesize;
|
||||
|
||||
/**
|
||||
* @var integer|null Max files number.
|
||||
*/
|
||||
public $maxFiles;
|
||||
|
||||
/**
|
||||
* @var array Options used for generating thumbnails.
|
||||
*/
|
||||
|
|
@ -109,6 +114,7 @@ class FileUpload extends FormWidgetBase
|
|||
'imageHeight',
|
||||
'fileTypes',
|
||||
'maxFilesize',
|
||||
'maxFiles',
|
||||
'mimeTypes',
|
||||
'thumbOptions',
|
||||
'useCaption',
|
||||
|
|
@ -152,13 +158,14 @@ class FileUpload extends FormWidgetBase
|
|||
$this->vars['singleFile'] = $fileList->first();
|
||||
$this->vars['displayMode'] = $this->getDisplayMode();
|
||||
$this->vars['emptyIcon'] = $this->getConfig('emptyIcon', 'icon-upload');
|
||||
$this->vars['imageHeight'] = $this->imageHeight;
|
||||
$this->vars['imageWidth'] = $this->imageWidth;
|
||||
$this->vars['imageHeight'] = (is_int($this->imageHeight)) ? $this->imageHeight : null;
|
||||
$this->vars['imageWidth'] = (is_int($this->imageWidth)) ? $this->imageWidth : null;
|
||||
$this->vars['acceptedFileTypes'] = $this->getAcceptedFileTypes(true);
|
||||
$this->vars['maxFilesize'] = $this->maxFilesize;
|
||||
$this->vars['maxFilesize'] = (is_int($this->maxFilesize)) ? $this->maxFilesize : null;
|
||||
$this->vars['cssDimensions'] = $this->getCssDimensions();
|
||||
$this->vars['cssBlockDimensions'] = $this->getCssDimensions('block');
|
||||
$this->vars['useCaption'] = $this->useCaption;
|
||||
$this->vars['maxFiles'] = (is_int($this->maxFiles)) ? $this->maxFiles : null;
|
||||
$this->vars['prompt'] = $this->getPromptText();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,117 @@
|
|||
<?php namespace Backend\FormWidgets;
|
||||
|
||||
use Backend\Classes\FormWidgetBase;
|
||||
|
||||
/**
|
||||
* Sensitive widget.
|
||||
*
|
||||
* Renders a password field that can be optionally made visible
|
||||
*
|
||||
* @package october\backend
|
||||
*/
|
||||
class Sensitive extends FormWidgetBase
|
||||
{
|
||||
/**
|
||||
* @var bool If true, the sensitive field cannot be edited, but can be toggled.
|
||||
*/
|
||||
public $readOnly = false;
|
||||
|
||||
/**
|
||||
* @var bool If true, the sensitive field is disabled.
|
||||
*/
|
||||
public $disabled = false;
|
||||
|
||||
/**
|
||||
* @var bool If true, a button will be available to copy the value.
|
||||
*/
|
||||
public $allowCopy = false;
|
||||
|
||||
/**
|
||||
* @var string The string that will be used as a placeholder for an unrevealed sensitive value.
|
||||
*/
|
||||
public $hiddenPlaceholder = '__hidden__';
|
||||
|
||||
/**
|
||||
* @var bool If true, the sensitive input will be hidden if the user changes to another tab in their browser.
|
||||
*/
|
||||
public $hideOnTabChange = true;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected $defaultAlias = 'sensitive';
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
$this->fillFromConfig([
|
||||
'readOnly',
|
||||
'disabled',
|
||||
'allowCopy',
|
||||
'hiddenPlaceholder',
|
||||
'hideOnTabChange',
|
||||
]);
|
||||
|
||||
if ($this->formField->disabled || $this->formField->readOnly) {
|
||||
$this->previewMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$this->prepareVars();
|
||||
|
||||
return $this->makePartial('sensitive');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the view data for the widget partial.
|
||||
*/
|
||||
public function prepareVars()
|
||||
{
|
||||
$this->vars['readOnly'] = $this->readOnly;
|
||||
$this->vars['disabled'] = $this->disabled;
|
||||
$this->vars['hasValue'] = !empty($this->getLoadValue());
|
||||
$this->vars['allowCopy'] = $this->allowCopy;
|
||||
$this->vars['hiddenPlaceholder'] = $this->hiddenPlaceholder;
|
||||
$this->vars['hideOnTabChange'] = $this->hideOnTabChange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reveals the value of a hidden, unmodified sensitive field.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function onShowValue()
|
||||
{
|
||||
return [
|
||||
'value' => $this->getLoadValue()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getSaveValue($value)
|
||||
{
|
||||
if ($value === $this->hiddenPlaceholder) {
|
||||
$value = $this->getLoadValue();
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function loadAssets()
|
||||
{
|
||||
$this->addCss('css/sensitive.css', 'core');
|
||||
$this->addJs('js/sensitive.js', 'core');
|
||||
}
|
||||
}
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
FileUpload.prototype = Object.create(BaseProto)
|
||||
FileUpload.prototype.constructor = FileUpload
|
||||
|
||||
FileUpload.prototype.init = function() {
|
||||
FileUpload.prototype.init = function () {
|
||||
if (this.options.isMulti === null) {
|
||||
this.options.isMulti = this.$el.hasClass('is-multi')
|
||||
}
|
||||
|
|
@ -69,7 +69,7 @@
|
|||
|
||||
}
|
||||
|
||||
FileUpload.prototype.dispose = function() {
|
||||
FileUpload.prototype.dispose = function () {
|
||||
|
||||
this.$el.off('click', '.upload-object.is-success', this.proxy(this.onClickSuccessObject))
|
||||
this.$el.off('click', '.upload-object.is-error', this.proxy(this.onClickErrorObject))
|
||||
|
|
@ -94,18 +94,25 @@
|
|||
// Uploading
|
||||
//
|
||||
|
||||
FileUpload.prototype.bindUploader = function() {
|
||||
FileUpload.prototype.bindUploader = function () {
|
||||
this.uploaderOptions = {
|
||||
url: this.options.url,
|
||||
paramName: this.options.paramName,
|
||||
clickable: this.$uploadButton.get(0),
|
||||
previewsContainer: this.$filesContainer.get(0),
|
||||
maxFiles: !this.options.isMulti ? 1 : null,
|
||||
maxFilesize: this.options.maxFilesize,
|
||||
timeout: 0,
|
||||
headers: {}
|
||||
}
|
||||
|
||||
if (!this.options.isMulti) {
|
||||
this.uploaderOptions.maxFiles = 1
|
||||
} else if (this.options.maxFiles) {
|
||||
this.uploaderOptions.maxFiles = this.options.maxFiles
|
||||
} else {
|
||||
this.uploaderOptions.maxFiles = null
|
||||
}
|
||||
|
||||
if (this.options.fileTypes) {
|
||||
this.uploaderOptions.acceptedFiles = this.options.fileTypes
|
||||
}
|
||||
|
|
@ -131,18 +138,48 @@
|
|||
}
|
||||
|
||||
this.dropzone = new Dropzone(this.$el.get(0), this.uploaderOptions)
|
||||
|
||||
this.dropzone.on('addedfile', this.proxy(this.onUploadAddedFile))
|
||||
this.dropzone.on('sending', this.proxy(this.onUploadSending))
|
||||
this.dropzone.on('success', this.proxy(this.onUploadSuccess))
|
||||
this.dropzone.on('error', this.proxy(this.onUploadError))
|
||||
this.dropzone.on('maxfilesreached', this.proxy(this.removeEventListeners))
|
||||
this.dropzone.on('removedfile', this.proxy(this.setupEventListeners))
|
||||
this.loadAlreadyUploadedFiles()
|
||||
}
|
||||
|
||||
FileUpload.prototype.onResizeFileInfo = function(file) {
|
||||
FileUpload.prototype.removeEventListeners = function () {
|
||||
this.dropzone.removeEventListeners()
|
||||
}
|
||||
|
||||
FileUpload.prototype.setupEventListeners = function () {
|
||||
if (this.dropzone.files.length < this.dropzone.options.maxFiles) {
|
||||
this.dropzone.setupEventListeners()
|
||||
}
|
||||
}
|
||||
|
||||
FileUpload.prototype.loadAlreadyUploadedFiles = function () {
|
||||
var self = this
|
||||
|
||||
this.$el.find('.server-file').each(function () {
|
||||
var file = $(this).data()
|
||||
|
||||
self.dropzone.files.push(file)
|
||||
self.dropzone.emit('addedfile', file)
|
||||
self.dropzone.emit('success', file, file)
|
||||
|
||||
$(this).remove()
|
||||
})
|
||||
|
||||
self.dropzone._updateMaxFilesReachedClass()
|
||||
}
|
||||
|
||||
FileUpload.prototype.onResizeFileInfo = function (file) {
|
||||
var info,
|
||||
targetWidth,
|
||||
targetHeight
|
||||
|
||||
if (!this.options.thumbnailWidth && !this.options.thumbnailWidth) {
|
||||
if (!this.options.thumbnailWidth && !this.options.thumbnailHeight) {
|
||||
targetWidth = targetHeight = 100
|
||||
}
|
||||
else if (this.options.thumbnailWidth) {
|
||||
|
|
@ -186,12 +223,12 @@
|
|||
this.evalIsPopulated()
|
||||
}
|
||||
|
||||
FileUpload.prototype.onUploadSending = function(file, xhr, formData) {
|
||||
FileUpload.prototype.onUploadSending = function (file, xhr, formData) {
|
||||
this.addExtraFormData(formData)
|
||||
xhr.setRequestHeader('X-OCTOBER-REQUEST-HANDLER', this.options.uploadHandler)
|
||||
}
|
||||
|
||||
FileUpload.prototype.onUploadSuccess = function(file, response) {
|
||||
FileUpload.prototype.onUploadSuccess = function (file, response) {
|
||||
var $preview = $(file.previewElement),
|
||||
$img = $('.image img', $preview)
|
||||
|
||||
|
|
@ -207,7 +244,7 @@
|
|||
this.triggerChange();
|
||||
}
|
||||
|
||||
FileUpload.prototype.onUploadError = function(file, error) {
|
||||
FileUpload.prototype.onUploadError = function (file, error) {
|
||||
var $preview = $(file.previewElement)
|
||||
$preview.addClass('is-error')
|
||||
}
|
||||
|
|
@ -215,11 +252,11 @@
|
|||
/*
|
||||
* Trigger change event (Compatibility with october.form.js)
|
||||
*/
|
||||
FileUpload.prototype.triggerChange = function() {
|
||||
FileUpload.prototype.triggerChange = function () {
|
||||
this.$el.closest('[data-field-name]').trigger('change.oc.formwidget')
|
||||
}
|
||||
|
||||
FileUpload.prototype.addExtraFormData = function(formData) {
|
||||
FileUpload.prototype.addExtraFormData = function (formData) {
|
||||
if (this.options.extraData) {
|
||||
$.each(this.options.extraData, function (name, value) {
|
||||
formData.append(name, value)
|
||||
|
|
@ -234,10 +271,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
FileUpload.prototype.removeFileFromElement = function($element) {
|
||||
FileUpload.prototype.removeFileFromElement = function ($element) {
|
||||
var self = this
|
||||
|
||||
$element.each(function() {
|
||||
$element.each(function () {
|
||||
var $el = $(this),
|
||||
obj = $el.data('dzFileObject')
|
||||
|
||||
|
|
@ -254,7 +291,7 @@
|
|||
// Sorting
|
||||
//
|
||||
|
||||
FileUpload.prototype.bindSortable = function() {
|
||||
FileUpload.prototype.bindSortable = function () {
|
||||
var
|
||||
self = this,
|
||||
placeholderEl = $('<div class="upload-object upload-placeholder"/>').css({
|
||||
|
|
@ -276,16 +313,15 @@
|
|||
})
|
||||
}
|
||||
|
||||
FileUpload.prototype.onSortAttachments = function() {
|
||||
FileUpload.prototype.onSortAttachments = function () {
|
||||
if (this.options.sortHandler) {
|
||||
|
||||
/*
|
||||
* Build an object of ID:ORDER
|
||||
*/
|
||||
var orderData = {}
|
||||
|
||||
this.$el.find('.upload-object.is-success')
|
||||
.each(function(index){
|
||||
.each(function (index) {
|
||||
var id = $(this).data('id')
|
||||
orderData[id] = index + 1
|
||||
})
|
||||
|
|
@ -300,16 +336,16 @@
|
|||
// User interaction
|
||||
//
|
||||
|
||||
FileUpload.prototype.onRemoveObject = function(ev) {
|
||||
FileUpload.prototype.onRemoveObject = function (ev) {
|
||||
var self = this,
|
||||
$object = $(ev.target).closest('.upload-object')
|
||||
|
||||
$(ev.target)
|
||||
.closest('.upload-remove-button')
|
||||
.one('ajaxPromise', function(){
|
||||
.one('ajaxPromise', function () {
|
||||
$object.addClass('is-loading')
|
||||
})
|
||||
.one('ajaxDone', function(){
|
||||
.one('ajaxDone', function () {
|
||||
self.removeFileFromElement($object)
|
||||
self.evalIsPopulated()
|
||||
self.triggerChange()
|
||||
|
|
@ -322,7 +358,7 @@
|
|||
FileUpload.prototype.onClickSuccessObject = function(ev) {
|
||||
if ($(ev.target).closest('.meta').length) return
|
||||
|
||||
var $target = $(ev.target).closest('.upload-object')
|
||||
var $target = $(ev.target).closest('.upload-object')
|
||||
|
||||
if (!this.options.configHandler) {
|
||||
window.open($target.data('path'))
|
||||
|
|
@ -334,9 +370,9 @@
|
|||
extraData: { file_id: $target.data('id') }
|
||||
})
|
||||
|
||||
$target.one('popupComplete', function(event, element, modal){
|
||||
$target.one('popupComplete', function (event, element, modal) {
|
||||
|
||||
modal.one('ajaxDone', 'button[type=submit]', function(e, context, data) {
|
||||
modal.one('ajaxDone', 'button[type=submit]', function (e, context, data) {
|
||||
if (data.displayName) {
|
||||
$('[data-dz-name]', $target).text(data.displayName)
|
||||
}
|
||||
|
|
@ -344,7 +380,7 @@
|
|||
})
|
||||
}
|
||||
|
||||
FileUpload.prototype.onClickErrorObject = function(ev) {
|
||||
FileUpload.prototype.onClickErrorObject = function (ev) {
|
||||
var
|
||||
self = this,
|
||||
$target = $(ev.target).closest('.upload-object'),
|
||||
|
|
@ -366,7 +402,7 @@
|
|||
})
|
||||
|
||||
var $container = $target.data('oc.popover').$container
|
||||
$container.one('click', '[data-remove-file]', function() {
|
||||
$container.one('click', '[data-remove-file]', function () {
|
||||
$target.data('oc.popover').hide()
|
||||
self.removeFileFromElement($target)
|
||||
self.evalIsPopulated()
|
||||
|
|
@ -377,7 +413,7 @@
|
|||
// Helpers
|
||||
//
|
||||
|
||||
FileUpload.prototype.evalIsPopulated = function() {
|
||||
FileUpload.prototype.evalIsPopulated = function () {
|
||||
var isPopulated = !!$('.upload-object', this.$filesContainer).length
|
||||
this.$el.toggleClass('is-populated', isPopulated)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
data-max-filesize="<?= $maxFilesize ?>"
|
||||
<?php if ($useCaption): ?>data-config-handler="<?= $this->getEventHandler('onLoadAttachmentConfig') ?>"<?php endif ?>
|
||||
<?php if ($acceptedFileTypes): ?>data-file-types="<?= $acceptedFileTypes ?>"<?php endif ?>
|
||||
<?php if ($maxFiles): ?>data-max-files="<?= $maxFiles ?>"<?php endif ?>
|
||||
<?= $this->formField->getAttributes() ?>
|
||||
>
|
||||
|
||||
|
|
@ -20,27 +21,14 @@
|
|||
<!-- Existing files -->
|
||||
<div class="upload-files-container">
|
||||
<?php foreach ($fileList as $file): ?>
|
||||
<div class="upload-object is-success" data-id="<?= $file->id ?>" data-path="<?= $file->pathUrl ?>">
|
||||
<div class="icon-container">
|
||||
<i class="icon-file"></i>
|
||||
</div>
|
||||
<div class="info">
|
||||
<h4 class="filename">
|
||||
<span data-dz-name><?= e($file->title ?: $file->file_name) ?></span>
|
||||
<a
|
||||
href="javascript:;"
|
||||
class="upload-remove-button"
|
||||
data-request="<?= $this->getEventHandler('onRemoveAttachment') ?>"
|
||||
data-request-confirm="<?= e(trans('backend::lang.fileupload.remove_confirm')) ?>"
|
||||
data-request-data="file_id: <?= $file->id ?>"
|
||||
><i class="icon-times"></i></a>
|
||||
</h4>
|
||||
<p class="size"><?= e($file->sizeToString()) ?></p>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<a href="javascript:;" class="drag-handle"><i class="icon-bars"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="server-file"
|
||||
data-id="<?= $file->id ?>"
|
||||
data-path="<?= $file->pathUrl ?>"
|
||||
data-thumb="<?= $file->thumbUrl ?>"
|
||||
data-name="<?= e($file->title ?: $file->file_name) ?>"
|
||||
data-size="<?= e($file->file_size) ?>"
|
||||
data-accepted="true"
|
||||
></div>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
data-max-filesize="<?= $maxFilesize ?>"
|
||||
<?php if ($useCaption): ?>data-config-handler="<?= $this->getEventHandler('onLoadAttachmentConfig') ?>"<?php endif ?>
|
||||
<?php if ($acceptedFileTypes): ?>data-file-types="<?= $acceptedFileTypes ?>"<?php endif ?>
|
||||
<?php if ($maxFiles): ?>data-max-files="<?= $maxFiles ?>"<?php endif ?>
|
||||
<?= $this->formField->getAttributes() ?>
|
||||
>
|
||||
|
||||
|
|
@ -20,27 +21,14 @@
|
|||
<!-- Existing files -->
|
||||
<div class="upload-files-container">
|
||||
<?php foreach ($fileList as $file): ?>
|
||||
<div class="upload-object is-success" data-id="<?= $file->id ?>" data-path="<?= $file->pathUrl ?>">
|
||||
<div class="icon-container image">
|
||||
<img src="<?= $file->thumbUrl ?>" alt="" />
|
||||
</div>
|
||||
<div class="info">
|
||||
<h4 class="filename">
|
||||
<span data-dz-name><?= e($file->title ?: $file->file_name) ?></span>
|
||||
<a
|
||||
href="javascript:;"
|
||||
class="upload-remove-button"
|
||||
data-request="<?= $this->getEventHandler('onRemoveAttachment') ?>"
|
||||
data-request-confirm="<?= e(trans('backend::lang.fileupload.remove_confirm')) ?>"
|
||||
data-request-data="file_id: <?= $file->id ?>"
|
||||
><i class="icon-times"></i></a>
|
||||
</h4>
|
||||
<p class="size"><?= e($file->sizeToString()) ?></p>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<a href="javascript:;" class="drag-handle"><i class="icon-bars"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="server-file"
|
||||
data-id="<?= $file->id ?>"
|
||||
data-path="<?= $file->pathUrl ?>"
|
||||
data-thumb="<?= $file->thumbUrl ?>"
|
||||
data-name="<?= e($file->title ?: $file->file_name) ?>"
|
||||
data-size="<?= e($file->file_size) ?>"
|
||||
data-accepted="true"
|
||||
></div>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -64,9 +64,10 @@ setTimeout(function(){editor.popups.show('link.insert')
|
|||
setLinkValue(link)},300)}
|
||||
function setLinkValue(link){var $popup=editor.popups.get('link.insert');var text_inputs=$popup.find('input.fr-link-attr[type="text"]');var check_inputs=$popup.find('input.fr-link-attr[type="checkbox"]');var $input;var i;for(i=0;i<text_inputs.length;i++){$input=$(text_inputs[i]);if(link[$input.attr('name')]){$input.val(link[$input.attr('name')]);}
|
||||
else if($input.attr('name')!='text'){$input.val('');}}
|
||||
for(i=0;i<check_inputs.length;i++){$input=$(check_inputs[i]);$input.prop('checked',$input.data('checked')==link[$input.attr('name')]);}}
|
||||
for(i=0;i<check_inputs.length;i++){$input=$(check_inputs[i]);$input.prop('checked',$input.data('checked')==link[$input.attr('name')]);}
|
||||
editor.selection.restore();}
|
||||
function insertLink(){richeditorPageLinksPlugin=this
|
||||
editor.$el.popup({handler:editor.opts.pageLinksHandler})}
|
||||
editor.$el.popup({handler:editor.opts.pageLinksHandler}).one('shown.oc.popup.pageLinks',function(){editor.selection.save()})}
|
||||
function _init(){}
|
||||
return{_init:_init,setLinkValueFromPopup:setLinkValueFromPopup,setLinkValue:setLinkValue,insertLink:insertLink}}
|
||||
$.FE.DEFAULTS.linkInsertButtons=['linkBack','|','linkPageLinks']
|
||||
|
|
|
|||
|
|
@ -54,6 +54,9 @@ $.FroalaEditor.DEFAULTS.key = 'JA6B2B5A1qB1F1F4D3I1A15A11D3E6B5dVh1VCQWa1EOQFe1N
|
|||
$input = $(check_inputs[i]);
|
||||
$input.prop('checked', $input.data('checked') == link[$input.attr('name')]);
|
||||
}
|
||||
|
||||
// Restore selection, so that the link gets inserted properly.
|
||||
editor.selection.restore();
|
||||
}
|
||||
|
||||
function insertLink() {
|
||||
|
|
@ -61,6 +64,9 @@ $.FroalaEditor.DEFAULTS.key = 'JA6B2B5A1qB1F1F4D3I1A15A11D3E6B5dVh1VCQWa1EOQFe1N
|
|||
|
||||
editor.$el.popup({
|
||||
handler: editor.opts.pageLinksHandler
|
||||
}).one('shown.oc.popup.pageLinks', function () {
|
||||
// Save the current selection so it can be restored after popup is closed.
|
||||
editor.selection.save()
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ $.FE.LANGUAGE['sk'] = {
|
|||
"Remove": "Odstr\u00e1ni\u0165",
|
||||
"More": "Viac",
|
||||
"Update": "Aktualizova\u0165",
|
||||
"Style": "\u0165t\u00fdl",
|
||||
"Style": "\u0160t\u00fdl",
|
||||
|
||||
// Font
|
||||
"Font Family": "Typ p\u00edsma",
|
||||
|
|
@ -77,7 +77,7 @@ $.FE.LANGUAGE['sk'] = {
|
|||
"Heading 4": "Nadpis 4",
|
||||
|
||||
// Style
|
||||
"Paragraph Style": "\u0165t\u00fdl odstavca",
|
||||
"Paragraph Style": "\u0160t\u00fdl odstavca",
|
||||
"Inline Style": "Inline \u0161t\u00fdl",
|
||||
|
||||
// Alignment
|
||||
|
|
@ -157,7 +157,7 @@ $.FE.LANGUAGE['sk'] = {
|
|||
"Insert Table": "Vlo\u017ei\u0165 tabu\u013eku",
|
||||
"Table Header": "Hlavi\u010dka tabu\u013eky",
|
||||
"Remove Table": "Odstrani\u0165 tabu\u013eku",
|
||||
"Table Style": "\u0165t\u00fdl tabu\u013eky",
|
||||
"Table Style": "\u0160t\u00fdl tabu\u013eky",
|
||||
"Horizontal Align": "Horizont\u00e1lne zarovnanie",
|
||||
"Row": "Riadok",
|
||||
"Insert row above": "Vlo\u017ei\u0165 riadok nad",
|
||||
|
|
@ -179,9 +179,11 @@ $.FE.LANGUAGE['sk'] = {
|
|||
"Align Top": "Zarovnat na vrch",
|
||||
"Align Middle": "Zarovnat na stred",
|
||||
"Align Bottom": "Zarovnat na spodok",
|
||||
"Cell Style": "\u0165t\u00fdl bunky",
|
||||
"Cell Style": "\u0160t\u00fdl bunky",
|
||||
|
||||
// Files
|
||||
"Insert Audio": "Vlo\u017Ei\u0165 zvuk",
|
||||
"Insert File": "Vlo\u017Ei\u0165 s\u00FAbor",
|
||||
"Upload File": "Nahra\u0165 s\u00fabor",
|
||||
"Drop file": "Vlo\u017ete s\u00fabor sem",
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
div[data-control="sensitive"] a[data-toggle],
|
||||
div[data-control="sensitive"] a[data-copy] {box-shadow:none;border:1px solid #d1d6d9;border-left:0}
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
/*
|
||||
* Sensitive field widget plugin.
|
||||
*
|
||||
* Data attributes:
|
||||
* - data-control="sensitive" - enables the plugin on an element
|
||||
*
|
||||
* JavaScript API:
|
||||
* $('div#someElement').sensitive({...})
|
||||
*/
|
||||
+function ($) { "use strict";
|
||||
var Base = $.oc.foundation.base,
|
||||
BaseProto = Base.prototype
|
||||
|
||||
var Sensitive = function(element, options) {
|
||||
this.$el = $(element)
|
||||
this.options = options
|
||||
this.clean = Boolean(this.$el.data('clean'))
|
||||
this.hidden = true
|
||||
|
||||
this.$input = this.$el.find('[data-input]').first()
|
||||
this.$toggle = this.$el.find('[data-toggle]').first()
|
||||
this.$icon = this.$el.find('[data-icon]').first()
|
||||
this.$loader = this.$el.find('[data-loader]').first()
|
||||
this.$copy = this.$el.find('[data-copy]').first()
|
||||
|
||||
$.oc.foundation.controlUtils.markDisposable(element)
|
||||
Base.call(this)
|
||||
this.init()
|
||||
}
|
||||
|
||||
Sensitive.DEFAULTS = {
|
||||
readOnly: false,
|
||||
disabled: false,
|
||||
eventHandler: null,
|
||||
hideOnTabChange: false,
|
||||
}
|
||||
|
||||
Sensitive.prototype = Object.create(BaseProto)
|
||||
Sensitive.prototype.constructor = Sensitive
|
||||
|
||||
Sensitive.prototype.init = function() {
|
||||
this.$input.on('keydown', this.proxy(this.onInput))
|
||||
this.$toggle.on('click', this.proxy(this.onToggle))
|
||||
|
||||
if (this.options.hideOnTabChange) {
|
||||
// Watch for tab change or minimise
|
||||
document.addEventListener('visibilitychange', this.proxy(this.onTabChange))
|
||||
}
|
||||
|
||||
if (this.$copy.length) {
|
||||
this.$copy.on('click', this.proxy(this.onCopy))
|
||||
}
|
||||
}
|
||||
|
||||
Sensitive.prototype.dispose = function () {
|
||||
this.$input.off('keydown', this.proxy(this.onInput))
|
||||
this.$toggle.off('click', this.proxy(this.onToggle))
|
||||
|
||||
if (this.options.hideOnTabChange) {
|
||||
document.removeEventListener('visibilitychange', this.proxy(this.onTabChange))
|
||||
}
|
||||
|
||||
if (this.$copy.length) {
|
||||
this.$copy.off('click', this.proxy(this.onCopy))
|
||||
}
|
||||
|
||||
this.$input = this.$toggle = this.$icon = this.$loader = null
|
||||
this.$el = null
|
||||
|
||||
BaseProto.dispose.call(this)
|
||||
}
|
||||
|
||||
Sensitive.prototype.onInput = function() {
|
||||
if (this.clean) {
|
||||
this.clean = false
|
||||
this.$input.val('')
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
Sensitive.prototype.onToggle = function() {
|
||||
if (this.$input.val() !== '' && this.clean) {
|
||||
this.reveal()
|
||||
} else {
|
||||
this.toggleVisibility()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
Sensitive.prototype.onTabChange = function() {
|
||||
if (document.hidden && !this.hidden) {
|
||||
this.toggleVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
Sensitive.prototype.onCopy = function() {
|
||||
var that = this,
|
||||
deferred = $.Deferred(),
|
||||
isHidden = this.hidden
|
||||
|
||||
deferred.then(function () {
|
||||
if (that.hidden) {
|
||||
that.toggleVisibility()
|
||||
}
|
||||
|
||||
that.$input.focus()
|
||||
that.$input.select()
|
||||
|
||||
try {
|
||||
document.execCommand('copy')
|
||||
} catch (err) {
|
||||
}
|
||||
|
||||
that.$input.blur()
|
||||
if (isHidden) {
|
||||
that.toggleVisibility()
|
||||
}
|
||||
})
|
||||
|
||||
if (this.$input.val() !== '' && this.clean) {
|
||||
this.reveal(deferred)
|
||||
} else {
|
||||
deferred.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
Sensitive.prototype.toggleVisibility = function() {
|
||||
if (this.hidden) {
|
||||
this.$input.attr('type', 'text')
|
||||
} else {
|
||||
this.$input.attr('type', 'password')
|
||||
}
|
||||
|
||||
this.$icon.toggleClass('icon-eye icon-eye-slash')
|
||||
|
||||
this.hidden = !this.hidden
|
||||
}
|
||||
|
||||
Sensitive.prototype.reveal = function(deferred) {
|
||||
var that = this
|
||||
this.$icon.css({
|
||||
visibility: 'hidden'
|
||||
})
|
||||
this.$loader.removeClass('hide')
|
||||
|
||||
this.$input.request(this.options.eventHandler, {
|
||||
success: function (data) {
|
||||
that.$input.val(data.value)
|
||||
that.clean = false
|
||||
|
||||
that.$icon.css({
|
||||
visibility: 'visible'
|
||||
})
|
||||
that.$loader.addClass('hide')
|
||||
|
||||
that.toggleVisibility()
|
||||
|
||||
if (deferred) {
|
||||
deferred.resolve()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var old = $.fn.sensitive
|
||||
|
||||
$.fn.sensitive = function (option) {
|
||||
var args = Array.prototype.slice.call(arguments, 1), result
|
||||
this.each(function () {
|
||||
var $this = $(this)
|
||||
var data = $this.data('oc.sensitive')
|
||||
var options = $.extend({}, Sensitive.DEFAULTS, $this.data(), typeof option == 'object' && option)
|
||||
if (!data) $this.data('oc.sensitive', (data = new Sensitive(this, options)))
|
||||
if (typeof option == 'string') result = data[option].apply(data, args)
|
||||
if (typeof result != 'undefined') return false
|
||||
})
|
||||
|
||||
return result ? result : this
|
||||
}
|
||||
|
||||
$.fn.sensitive.noConflict = function () {
|
||||
$.fn.sensitive = old
|
||||
return this
|
||||
}
|
||||
|
||||
$(document).render(function () {
|
||||
$('[data-control="sensitive"]').sensitive()
|
||||
});
|
||||
|
||||
}(window.jQuery);
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
@import "../../../../assets/less/core/boot.less";
|
||||
|
||||
div[data-control="sensitive"] {
|
||||
a[data-toggle],
|
||||
a[data-copy] {
|
||||
box-shadow: none;
|
||||
border: 1px solid @input-group-addon-border-color;
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<div
|
||||
data-control="sensitive"
|
||||
data-clean="true"
|
||||
data-event-handler="<?= $this->getEventHandler('onShowValue') ?>"
|
||||
<?php if ($hideOnTabChange): ?>data-hide-on-tab-change="true"<?php endif ?>
|
||||
>
|
||||
<div class="loading-indicator-container size-form-field">
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="password"
|
||||
name="<?= $this->getFieldName() ?>"
|
||||
id="<?= $this->getId() ?>"
|
||||
value="<?= ($hasValue) ? $hiddenPlaceholder : '' ?>"
|
||||
placeholder="<?= e(trans($this->formField->placeholder)) ?>"
|
||||
class="form-control"
|
||||
<?php if ($this->previewMode): ?>disabled="disabled"<?php endif ?>
|
||||
autocomplete="off"
|
||||
data-input
|
||||
/>
|
||||
<?php if ($allowCopy): ?>
|
||||
<a
|
||||
href="javascript:;"
|
||||
class="input-group-addon btn btn-secondary"
|
||||
data-copy
|
||||
>
|
||||
<i class="icon-copy"></i>
|
||||
</a>
|
||||
<?php endif ?>
|
||||
<a
|
||||
href="javascript:;"
|
||||
class="input-group-addon btn btn-secondary"
|
||||
data-toggle
|
||||
>
|
||||
<i class="icon-eye" data-icon></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="loading-indicator hide" data-loader>
|
||||
<span class="p-a"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,18 +1,32 @@
|
|||
<?php
|
||||
$selectedValues = is_array($selectedValues) ? $selectedValues : [];
|
||||
$availableOptions = $useKey ? $fieldOptions : array_unique(array_merge($selectedValues, $fieldOptions));
|
||||
$displayOnlyOptions = [];
|
||||
|
||||
foreach ($availableOptions as $key => $option) {
|
||||
if (!strlen($option)) {
|
||||
continue;
|
||||
}
|
||||
if (($useKey && in_array($key, $selectedValues)) || (!$useKey && in_array($option, $selectedValues))) {
|
||||
$displayOnlyOptions[] = $option;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!-- Tag List -->
|
||||
<?php if ($this->previewMode || $field->readOnly || $field->disabled): ?>
|
||||
<ul class="form-control taglist--preview" <?= $field->readOnly || $field->disabled ? 'disabled="disabled"' : ''; ?>>
|
||||
<?php foreach ($availableOptions as $key => $option): ?>
|
||||
<?php if (!strlen($option)) continue ?>
|
||||
<?php if (($useKey && in_array($key, $selectedValues)) || (!$useKey && in_array($option, $selectedValues))): ?>
|
||||
<li class="taglist__item"><?= e(trans($option)) ?></li>
|
||||
<?php endif ?>
|
||||
<?php foreach ($displayOnlyOptions as $option): ?>
|
||||
<li class="taglist__item"><?= e(trans($option)) ?></li>
|
||||
<?php endforeach ?>
|
||||
</ul>
|
||||
<?php if ($field->readOnly): ?>
|
||||
<?php if (is_array($field->value)): ?>
|
||||
<?php foreach ($displayOnlyOptions as $option): ?>
|
||||
<input
|
||||
type="hidden"
|
||||
name="<?= $field->getName() ?>[]"
|
||||
value="<?= $option ?>">
|
||||
<?php endforeach ?>
|
||||
<?php else: ?>
|
||||
<input
|
||||
type="hidden"
|
||||
name="<?= $field->getName() ?>"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use October\Rain\Router\Helper as RouterHelper;
|
|||
use System\Helpers\DateTime as DateTimeHelper;
|
||||
use Backend\Classes\Skin;
|
||||
use Backend\Helpers\Exception\DecompileException;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Backend Helper
|
||||
|
|
@ -85,6 +86,26 @@ class Backend
|
|||
return Redirect::intended($this->uri() . '/' . $path, $status, $headers, $secure);
|
||||
}
|
||||
|
||||
/**
|
||||
* makeCarbon converts mixed inputs to a Carbon object and sets the backend timezone
|
||||
* @return \Carbon\Carbon
|
||||
*/
|
||||
public static function makeCarbon($value, $throwException = true)
|
||||
{
|
||||
$carbon = DateTimeHelper::makeCarbon($value, $throwException);
|
||||
|
||||
try {
|
||||
// Find user preference
|
||||
$carbon->setTimezone(\Backend\Models\Preference::get('timezone'));
|
||||
}
|
||||
catch (Exception $ex) {
|
||||
// Use system default
|
||||
$carbon->setTimezone(Config::get('backend.timezone', Config::get('app.timezone')));
|
||||
}
|
||||
|
||||
return $carbon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy method for dateTime() using "date" format alias.
|
||||
* @return string
|
||||
|
|
@ -214,14 +235,14 @@ class Backend
|
|||
$contents = file_get_contents($assetFile);
|
||||
|
||||
// Find all assets that are compiled in this file
|
||||
preg_match_all('/^=require\s+([A-z0-9-_+\.\/]+)$/m', $contents, $matches, PREG_SET_ORDER);
|
||||
preg_match_all('/^=require\s+([A-z0-9-_+\.\/]+)[\n|\r\n|$]/m', $contents, $matches, PREG_SET_ORDER);
|
||||
|
||||
// Determine correct asset path
|
||||
$directory = str_replace(basename($file), '', $file);
|
||||
|
||||
if (count($matches)) {
|
||||
$results = array_map(function ($match) use ($directory) {
|
||||
return $directory . $match[1];
|
||||
return str_replace('/', DIRECTORY_SEPARATOR, $directory . $match[1]);
|
||||
}, $matches);
|
||||
|
||||
foreach ($results as $i => $result) {
|
||||
|
|
|
|||
|
|
@ -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:'
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -307,11 +307,11 @@ return [
|
|||
'auto_closing' => 'Cerrado de etiquetas automático',
|
||||
'show_invisibles' => 'Mostrar caracteres invisibles',
|
||||
'show_gutter' => 'Mostrar numeros de línea',
|
||||
'basic_autocompletion'=> 'Autocompletado Basico (Ctrl + Espacio)',
|
||||
'live_autocompletion'=> 'Autocompletado en Vivo',
|
||||
'enable_snippets'=> 'Activar uso de Snippets',
|
||||
'display_indent_guides'=> 'Mostrar Guias de Identado',
|
||||
'show_print_margin'=> 'Mostrar Margen de impresión',
|
||||
'basic_autocompletion' => 'Autocompletado Basico (Ctrl + Espacio)',
|
||||
'live_autocompletion' => 'Autocompletado en Vivo',
|
||||
'enable_snippets' => 'Activar uso de Snippets',
|
||||
'display_indent_guides' => 'Mostrar Guias de Identado',
|
||||
'show_print_margin' => 'Mostrar Margen de impresión',
|
||||
'mode_off' => 'Off',
|
||||
'mode_fluid' => 'Fluido',
|
||||
'40_characters' => '40 Caracteres',
|
||||
|
|
@ -396,7 +396,8 @@ return [
|
|||
'filter' => [
|
||||
'all' => 'todo',
|
||||
'options_method_not_exists' => "La clase de modelo :model debe definir un método :method() para regresar opciones para el filtro ':filter'.",
|
||||
'date_all' => 'todo el período'
|
||||
'date_all' => 'todo el período',
|
||||
'number_all' => 'todos los números'
|
||||
],
|
||||
'import_export' => [
|
||||
'upload_csv_file' => '1. Subir un archivo CSV',
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ return [
|
|||
'role_field' => 'Rôle',
|
||||
'role_comment' => 'Les rôles définissent les permissions de l\'utilisateur, elles peuvent être écrasés au niveau de l\'utilisateur dans l\'onglet "Permissions".',
|
||||
'groups' => 'Groupes',
|
||||
'groups_comment' => 'Préciser les groupes auxquels ce compte doit appartenir. Les groupes définissent les permissions des utilisateurs, qui peuvent être surchargées au niveau de l’utilisateur, dans l’onglet Permissions.',
|
||||
'groups_comment' => 'Préciser les groupes auxquels ce compte doit appartenir.',
|
||||
'avatar' => 'Avatar',
|
||||
'password' => 'Mot de passe',
|
||||
'password_confirmation' => 'Confirmer le mot de passe',
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ return [
|
|||
'field' => [
|
||||
'invalid_type' => 'A(z) :type mezőtípus érvénytelen.',
|
||||
'options_method_invalid_model' => "A(z) ':field' tulajdonság nem passzol a modellhez. Próbálja meghatározni a beállítást, ami megfelelő a(z) :model osztály számára.",
|
||||
'options_method_not_exists' => "A(z) :model modell osztálynak egy :method() nevű metódust kell definiálnia a(z) ':field' űrlapmező számára, ami visszaadja a beállításokat.",
|
||||
'options_method_not_exists' => "A(z) :model osztálynak egy :method() nevű metódust kell definiálnia a(z) ':field' űrlapmező számára, ami visszaadja a beállításokat.",
|
||||
'options_static_method_invalid_value' => "A(z) :class osztályban lévő ':method()' nevű metódus nem ad vissza érvényes tömböt.",
|
||||
'colors_method_not_exists' => "A(z) :model modell osztálynak egy :method() nevű metódust kell definiálnia a(z) ':field' űrlapmező számára, ami visszaadja a html HEX kódot."
|
||||
],
|
||||
'widget' => [
|
||||
|
|
@ -43,11 +44,15 @@ return [
|
|||
],
|
||||
'account' => [
|
||||
'impersonate' => 'Átjelentkezés a fiókba',
|
||||
'impersonate_confirm' => 'Biztos benne, hogy átjelentkezik a felhasználó saját fiókjába? Ezáltal a jelenlegi munkamenetből ki lesz jelentkeztetve.',
|
||||
'impersonate_confirm' => 'Biztos, hogy átjelentkezik a felhasználó saját fiókjába? Ezáltal a jelenlegi munkamenetből ki lesz jelentkeztetve.',
|
||||
'impersonate_success' => 'Sikeresen átjelentkezett a másik fiókba',
|
||||
'impersonate_working' => 'Átjelentkezés...',
|
||||
'impersonating' => 'Átjelentkezve mint :full_name',
|
||||
'stop_impersonating' => 'Visszajelentkezés',
|
||||
'unsuspend' => 'Felfüggesztés',
|
||||
'unsuspend_confirm' => 'Biztos, hogy felfüggeszti a felhasználót?',
|
||||
'unsuspend_success' => 'A felfüggesztés sikeresen megtörtént.',
|
||||
'unsuspend_working' => 'Felfüggesztés folyamatban...',
|
||||
'signed_in_as' => 'Belépve mint :full_name',
|
||||
'sign_out' => 'Kijelentkezés',
|
||||
'login' => 'Belépés',
|
||||
|
|
@ -368,6 +373,7 @@ return [
|
|||
'editor' => [
|
||||
'menu_label' => 'Szövegszerkesztő',
|
||||
'menu_description' => 'A megjelenésének és működésének testreszabása.',
|
||||
'preview' => 'Előnézet',
|
||||
'font_size' => 'Betűméret',
|
||||
'tab_size' => 'Tabulátor mérete',
|
||||
'use_hard_tabs' => 'Behúzás tabulátorokkal',
|
||||
|
|
@ -402,6 +408,7 @@ return [
|
|||
'label' => 'Megnevezés',
|
||||
'class_name' => 'CSS osztály',
|
||||
'markup_tags' => 'Szabályok',
|
||||
'markup_tag' => 'Szabály',
|
||||
'allowed_empty_tags' => 'Engedélyezett üres elemek',
|
||||
'allowed_empty_tags_comment' => 'Azon HTML elemek, amik üres érték esetén sem lesznek eltávolítva.',
|
||||
'allowed_tags' => 'Engedélyezett elemek',
|
||||
|
|
@ -412,14 +419,17 @@ return [
|
|||
'remove_tags_comment' => 'Azon HTML elemek, amik a tartalmukkal együtt törölhetőek.',
|
||||
'line_breaker_tags' => 'Sortörő elemek',
|
||||
'line_breaker_tags_comment' => 'Azon HTML elemek, amik végén kötelezően egy új sor jelenik meg.',
|
||||
'toolbar_buttons' => 'Eszköztár',
|
||||
'toolbar_options' => 'Eszköztár',
|
||||
'toolbar_buttons' => 'Saját konfiguráció',
|
||||
'toolbar_buttons_comment' => 'Az alapértelmezetten megjelenő eszközök listája.',
|
||||
'toolbar_buttons_preset' => 'Előre beállított eszköztár konfigurációk:',
|
||||
'toolbar_buttons_preset' => 'Előre beállított konfigurációk:',
|
||||
'toolbar_buttons_presets' => [
|
||||
'default' => 'Alapértelmezett',
|
||||
'minimal' => 'Minimális',
|
||||
'full' => 'Teljes',
|
||||
],
|
||||
'paragraph_formats' => 'Bekezdés formátumok',
|
||||
'paragraph_formats_comment' => 'Az ehhez tartozó lenyíló listában fognak megjelenni.',
|
||||
],
|
||||
'tooltips' => [
|
||||
'preview_website' => 'Weboldal megtekintése'
|
||||
|
|
@ -562,7 +572,8 @@ return [
|
|||
]
|
||||
],
|
||||
'permissions' => [
|
||||
'manage_media' => 'Média kezelése'
|
||||
'manage_media' => 'Média kezelése',
|
||||
'allow_unsafe_markdown' => 'Nem biztonságos szerkesztő használata',
|
||||
],
|
||||
'mediafinder' => [
|
||||
'label' => 'Média',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ return [
|
|||
'invalid_type' => 'Tipo de campo inválido :type.',
|
||||
'options_method_invalid_model' => 'O atributo ":field" não resolve a classe. Tente especificar as opções do método para o modelo :model.',
|
||||
'options_method_not_exists' => 'A classe :model deve definir um método :method() retornando opções para o campo ":field".',
|
||||
'options_static_method_invalid_value' => "O método estático ':method()' na :class não retornou um array de opções válidas.",
|
||||
'colors_method_not_exists' => 'A classe de modelo :model deve definir um método :method() retornando códigos HEX de cor html para o campo de formulário ":field".'
|
||||
],
|
||||
'widget' => [
|
||||
|
|
@ -48,14 +49,18 @@ return [
|
|||
'impersonate_working' => 'Representando...',
|
||||
'impersonating' => 'Representando :full_name',
|
||||
'stop_impersonating' => 'Pare de representar',
|
||||
'unsuspend' => 'Reativar',
|
||||
'unsuspend_confirm' => 'Tem certeza de que deseja reativar este usuário?',
|
||||
'unsuspend_success' => 'O usuário foi reativado.',
|
||||
'unsuspend_working' => 'Reativando...',
|
||||
'signed_in_as' => 'Assinado como :full_name',
|
||||
'remember_me' => 'Permaneça logado',
|
||||
'sign_out' => 'Sair',
|
||||
'login' => 'Entrar',
|
||||
'reset' => 'Redefinir',
|
||||
'restore' => 'Restaurar',
|
||||
'login_placeholder' => 'Usuário',
|
||||
'password_placeholder' => 'Senha',
|
||||
'remember_me' => 'Permaneça logado',
|
||||
'forgot_password' => 'Esqueceu sua senha?',
|
||||
'enter_email' => 'Entre com seu email',
|
||||
'enter_login' => 'Entre com seu nome de usuário',
|
||||
|
|
@ -214,8 +219,8 @@ return [
|
|||
'setup_title' => 'Configuração da Lista',
|
||||
'setup_help' => 'Selecione as colunas que deseja ver na lista. Você pode alterar as posições das colunas arrastando-as para cima ou para baixo.',
|
||||
'records_per_page' => 'Registros por página',
|
||||
'check' => 'Marcar',
|
||||
'records_per_page_help' => 'Selecione o número de registros a serem exibidos por página. Note que um número grande pode prejudicar a performance.',
|
||||
'check' => 'Marcar',
|
||||
'delete_selected' => 'Excluir selecionado',
|
||||
'delete_selected_empty' => 'Não há registros selecionados para excluir.',
|
||||
'delete_selected_confirm' => 'Excluir os registros selecionados?',
|
||||
|
|
@ -236,6 +241,7 @@ return [
|
|||
'remove_file' => 'Remover arquivo'
|
||||
],
|
||||
'repeater' => [
|
||||
'add_new_item' => 'Adicionar novo item',
|
||||
'min_items_failed' => ':name requer um mínimo de :min itens, apenas :items foram fornecidos',
|
||||
'max_items_failed' => ':name requer um máximo de :max itens, apenas :items foram fornecidos',
|
||||
],
|
||||
|
|
@ -246,6 +252,7 @@ return [
|
|||
'create_success' => ':name foi criado com sucesso',
|
||||
'update_success' => ':name foi atualizado com sucesso',
|
||||
'delete_success' => ':name foi apagado com sucesso',
|
||||
'restore_success' => ':name restaurado',
|
||||
'reset_success' => 'Reinicialização completada',
|
||||
'missing_id' => 'O ID do registro não foi fornecido',
|
||||
'missing_model' => 'Formulário utilizado na classe :class não tem um model definido.',
|
||||
|
|
@ -304,6 +311,10 @@ return [
|
|||
'invalid_model_class' => 'A classe de modelo fornecida ":modelClass" para o recordfinder é inválida',
|
||||
'cancel' => 'Cancelar',
|
||||
],
|
||||
'pagelist' => [
|
||||
'page_link' => 'Link da página',
|
||||
'select_page' => 'Selecione uma página ...',
|
||||
],
|
||||
'relation' => [
|
||||
'missing_config' => 'Comportamento relation não tem uma configuração para ":config".',
|
||||
'missing_definition' => 'Comportamento relation não contém uma definição para ":field".',
|
||||
|
|
@ -356,21 +367,25 @@ return [
|
|||
'permissions' => 'Diretório :name ou seus subdiretórios não são graváveis pelo PHP. Por favor, defina permissões de escrita para o servidor neste diretório.',
|
||||
'extension' => 'A extensão PHP :name não está instalada. Por favor, instale esta biblioteca para ativar a extensão.',
|
||||
'plugin_missing' => 'O plugin :name é uma dependência, mas não está instalado. Por favor, instale este plugin.',
|
||||
'debug' => 'O modo de depuração está ativado. Isso não é recomendado para instalações de produção.',
|
||||
'decompileBackendAssets' => 'Os assets no Backend estão atualmente descompilados. Isso não é recomendado para instalações de produção.',
|
||||
],
|
||||
'editor' => [
|
||||
'menu_label' => 'Definições do Editor',
|
||||
'menu_description' => 'Gerenciar configurações do editor.',
|
||||
'preview' => 'Prévia',
|
||||
'font_size' => 'Tamanho da fonte',
|
||||
'tab_size' => 'Tamanho do espaçamento',
|
||||
'use_hard_tabs' => 'Recuo usando guias',
|
||||
'code_folding' => 'Código flexível',
|
||||
'code_folding_begin' => 'Marca de início',
|
||||
'code_folding_begin_end' => 'Marca de início e fim',
|
||||
'autocompletion' => 'Autocompletar',
|
||||
'code_folding' => 'Código flexível',
|
||||
'word_wrap' => 'Quebra de linha',
|
||||
'highlight_active_line' => 'Destaque na linha ativa',
|
||||
'auto_closing' => 'Auto completar tags e caracteres especiais',
|
||||
'show_invisibles' => 'Mostrar caracteres invisíveis',
|
||||
'show_gutter' => 'Mostrar numeração de linhas',
|
||||
'basic_autocompletion'=> 'Autocompletar básico (Ctrl + Espaço)',
|
||||
'live_autocompletion'=> 'Autocompletar em tempo real',
|
||||
'enable_snippets'=> 'Habilitar trechos de códigos (Tab)',
|
||||
|
|
@ -380,7 +395,7 @@ return [
|
|||
'mode_fluid' => 'Fluido',
|
||||
'40_characters' => '40 caracteres',
|
||||
'80_characters' => '80 caracteres',
|
||||
'show_gutter' => 'Mostrar numeração de linhas',
|
||||
'theme' => 'Esquema de cores',
|
||||
'markup_styles' => 'Estilos de marcação',
|
||||
'custom_styles' => 'Folha de estilo personalizada',
|
||||
'custom styles_comment' => 'Estilos personalizados para incluir no editor HTML.',
|
||||
|
|
@ -393,6 +408,7 @@ return [
|
|||
'label' => 'Rótulo',
|
||||
'class_name' => 'Nome da classe',
|
||||
'markup_tags' => 'Etiquetas de marcação',
|
||||
'markup_tag' => 'Tag de marcação',
|
||||
'allowed_empty_tags' => 'Permitir etiquetas vazias',
|
||||
'allowed_empty_tags_comment' => 'A lista de etiquetas não é removida quando não há conteúdo.',
|
||||
'allowed_tags' => 'Etiquetas permitidas',
|
||||
|
|
@ -401,11 +417,19 @@ return [
|
|||
'no_wrap_comment' => 'Lista de etiquetas que não devem ser agrupadas.',
|
||||
'remove_tags' => 'Excluir etiqueta',
|
||||
'remove_tags_comment' => 'Lista de etiquetas que serão exclídas juntas com seu conteúdo.',
|
||||
'theme' => 'Esquema de cores',
|
||||
'line_breaker_tags' => 'Tags de quebra de linha',
|
||||
'line_breaker_tags_comment' => 'A lista de tags usadas para colocar um elemento em quebra de linha.',
|
||||
'toolbar_options' => 'Opções da barra de ferramentas',
|
||||
'toolbar_buttons' => 'Botões da barra de ferramentas',
|
||||
'toolbar_buttons_comment' => 'Os botões da barra de ferramentas a serem exibidos no Rich Editor por padrão.',
|
||||
'toolbar_buttons_preset' => 'Insira uma configuração predefinida de botãos na barra de ferramentas:',
|
||||
'toolbar_buttons_presets' => [
|
||||
'default' => 'Padrão',
|
||||
'minimal' => 'Mínimo',
|
||||
'full' => 'Completo',
|
||||
],
|
||||
'paragraph_formats' => 'Formatos de parágrafo',
|
||||
'paragraph_formats_comment' => 'As opções que aparecerão na lista de Formatos do parágrafo.',
|
||||
],
|
||||
'tooltips' => [
|
||||
'preview_website' => 'Visualizar a página'
|
||||
|
|
@ -542,12 +566,14 @@ return [
|
|||
'iso_8859_13' => 'ISO-8859-13 (Latin-7, Baltic Rim)',
|
||||
'iso_8859_14' => 'ISO-8859-14 (Latin-8, Celtic)',
|
||||
'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)'
|
||||
]
|
||||
],
|
||||
'permissions' => [
|
||||
'manage_media' => 'Gerenciar mídias'
|
||||
'manage_media' => 'Gerenciar mídias',
|
||||
'allow_unsafe_markdown' => 'Usar Markdown inseguro (pode incluir Javascript)',
|
||||
],
|
||||
'mediafinder' => [
|
||||
'label' => 'Localizador de Mídia',
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@ return [
|
|||
],
|
||||
'field' => [
|
||||
'invalid_type' => 'Использован неверный тип поля: :type.',
|
||||
'options_method_invalid_model' => "Атрибут ':field' не соответствует допустимой модели. Попробуйте явно указать метод параметров для класса :model .",
|
||||
'options_method_not_exists' => "Класс модели :model должен содержать метод :method(), возвращающий опции для поля формы ':field'.",
|
||||
'colors_method_not_exists' => "Класс модели :model должен содержать метод :method(), возвращающий HTML цвет в HEX для поля формы ':field'."
|
||||
'options_static_method_invalid_value' => "Статический метод ':method()' в :class не вернул допустимый массив параметров.",
|
||||
'colors_method_not_exists' => "Класс модели :model должен содержать метод :method(), возвращающий HTML цвет в HEX для поля формы ':field'.",
|
||||
],
|
||||
'widget' => [
|
||||
'not_registered' => "Класс виджета ':name' не зарегистрирован.",
|
||||
|
|
@ -24,12 +26,12 @@ return [
|
|||
'access_denied' => [
|
||||
'label' => 'Доступ запрещен',
|
||||
'help' => 'У вас нет необходимых прав для просмотра этой страницы.',
|
||||
'cms_link' => 'Перейти к CMS'
|
||||
'cms_link' => 'Перейти к CMS',
|
||||
],
|
||||
'no_database' => [
|
||||
'label' => 'Отсутствует база данных',
|
||||
'help' => "Для доступа к серверу требуется база данных. Проверьте, что база данных настроена и перенесена, прежде чем повторять попытку.",
|
||||
'cms_link' => 'Вернуться на главную страницу'
|
||||
'cms_link' => 'Вернуться на главную страницу',
|
||||
],
|
||||
],
|
||||
'partial' => [
|
||||
|
|
@ -41,6 +43,16 @@ return [
|
|||
'not_found' => "AJAX обработчик ':name' не найден.",
|
||||
],
|
||||
'account' => [
|
||||
'impersonate' => 'Имперсонация пользователя',
|
||||
'impersonate_confirm' => 'Вы уверены, что хотите имперсонировать себя в качестве этого пользователя? Вы сможете вернуться в исходное состояние выйдя из системы.',
|
||||
'impersonate_success' => 'Теперь вы имперсонированы как этот пользователь',
|
||||
'impersonate_working' => 'Имперсонация...',
|
||||
'impersonating' => 'Имперсонация :full_name',
|
||||
'stop_impersonating' => 'Остановить имперсонацию',
|
||||
'unsuspend' => 'Приостановлен',
|
||||
'unsuspend_confirm' => 'Вы уверены что хотите приостановить данного пользователя?',
|
||||
'unsuspend_success' => 'Пользователь был приостановлен.',
|
||||
'unsuspend_working' => 'Приостановка...',
|
||||
'signed_in_as' => 'Выполнен вход как :full_name',
|
||||
'sign_out' => 'Выйти',
|
||||
'login' => 'Вход',
|
||||
|
|
@ -50,9 +62,9 @@ return [
|
|||
'password_placeholder' => 'пароль',
|
||||
'remember_me' => 'Оставаться в системе',
|
||||
'forgot_password' => 'Забыли пароль?',
|
||||
'enter_email' => 'Введите вашу почту',
|
||||
'enter_email' => 'Введите ваш Email',
|
||||
'enter_login' => 'Введите ваш Логин',
|
||||
'email_placeholder' => 'почта',
|
||||
'email_placeholder' => 'email',
|
||||
'enter_new_password' => 'Введите новый пароль',
|
||||
'password_reset' => 'Сбросить пароль',
|
||||
'restore_success' => 'На вашу электронную почту отправлено сообщение с инструкциями для восстановления пароля.',
|
||||
|
|
@ -63,7 +75,7 @@ return [
|
|||
'apply' => 'Применить',
|
||||
'cancel' => 'Отменить',
|
||||
'delete' => 'Удалить',
|
||||
'ok' => 'OK'
|
||||
'ok' => 'OK',
|
||||
],
|
||||
'dashboard' => [
|
||||
'menu_label' => 'Панель управления',
|
||||
|
|
@ -92,7 +104,7 @@ return [
|
|||
'expand_all' => 'Развернуть всё',
|
||||
'status' => [
|
||||
'widget_title_default' => 'Статус системы',
|
||||
'update_available' => '{0} нет новый обновлений!|{1} доступно новое обновление!|[2,Inf] доступны новые обновления!',
|
||||
'update_available' => '{0} нет новых обновлений!|{1} доступно новое обновление!|[2,Inf] доступны новые обновления!',
|
||||
'updates_pending' => 'Доступны обновления',
|
||||
'updates_nil' => 'Используется последняя версия',
|
||||
'updates_link' => 'Обновить',
|
||||
|
|
@ -102,7 +114,7 @@ return [
|
|||
'core_build' => 'Сборка',
|
||||
'event_log' => 'Лог событий',
|
||||
'request_log' => 'Лог запросов',
|
||||
'app_birthday' => 'Онлайн с'
|
||||
'app_birthday' => 'Онлайн с',
|
||||
],
|
||||
'welcome' => [
|
||||
'widget_title_default' => 'Добро пожаловать',
|
||||
|
|
@ -111,7 +123,7 @@ return [
|
|||
'first_sign_in' => 'Это первый раз, когда вы вошли в систему.',
|
||||
'last_sign_in' => 'Последний раз вы заходили',
|
||||
'view_access_logs' => 'Посмотреть лог доступа',
|
||||
'nice_message' => 'Хорошего дня!'
|
||||
'nice_message' => 'Хорошего дня!',
|
||||
],
|
||||
],
|
||||
'user' => [
|
||||
|
|
@ -163,7 +175,7 @@ return [
|
|||
'new' => 'Добавить группу',
|
||||
'delete_confirm' => 'Вы действительно хотите удалить эту группу администраторов?',
|
||||
'return' => 'Вернуться к списку групп',
|
||||
'users_count' => 'Пользователи'
|
||||
'users_count' => 'Пользователи',
|
||||
],
|
||||
'role' => [
|
||||
'name' => 'Роль',
|
||||
|
|
@ -177,10 +189,10 @@ return [
|
|||
'new' => 'Новая роль',
|
||||
'delete_confirm' => 'Удалить эту роль администратора?',
|
||||
'return' => 'Вернуться к списку ролей',
|
||||
'users_count' => 'Пользователи'
|
||||
'users_count' => 'Пользователи',
|
||||
],
|
||||
'preferences' => [
|
||||
'not_authenticated' => 'Невозможно загрузить или сохранить настройки для неавторизованного пользователя.'
|
||||
'not_authenticated' => 'Невозможно загрузить или сохранить настройки для неавторизованного пользователя.',
|
||||
],
|
||||
'trashed_hint_title' => 'Этот аккаунт был удален',
|
||||
'trashed_hint_desc' => 'Этот аккаунт был удален и не может быть авторизован. Чтобы восстановить его, нажмите иконку восстановления пользователя в правом нижнем углу.',
|
||||
|
|
@ -214,7 +226,7 @@ return [
|
|||
'delete_selected_confirm' => 'Удалить выбранные записи?',
|
||||
'delete_selected_success' => 'Выбранные записи успешно удалены.',
|
||||
'column_switch_true' => 'Да',
|
||||
'column_switch_false' => 'Нет'
|
||||
'column_switch_false' => 'Нет',
|
||||
],
|
||||
'fileupload' => [
|
||||
'attachment' => 'Приложение',
|
||||
|
|
@ -226,7 +238,7 @@ return [
|
|||
'upload_file' => 'Загрузить файл',
|
||||
'upload_error' => 'Ошибка загрузки',
|
||||
'remove_confirm' => 'Вы уверены?',
|
||||
'remove_file' => 'Удалить файл'
|
||||
'remove_file' => 'Удалить файл',
|
||||
],
|
||||
'repeater' => [
|
||||
'add_new_item' => 'Добавить новый объект',
|
||||
|
|
@ -292,15 +304,16 @@ return [
|
|||
'delete_row' => 'Удалить строку',
|
||||
'concurrency_file_changed_title' => 'Файл был изменен',
|
||||
'concurrency_file_changed_description' => 'Файл,редактируемый вами, был изменен другим пользователем. Вы можете перезагрузить файл и потерять ваши изменения или перезаписать его',
|
||||
'return_to_list' => 'Вернуться к списку'
|
||||
'return_to_list' => 'Вернуться к списку',
|
||||
],
|
||||
'recordfinder' => [
|
||||
'find_record' => 'Найти запись',
|
||||
'cancel' => 'Отмена'
|
||||
'invalid_model_class' => 'Предоставленный класс модели ":modelClass" для поиска записи является недействительным',
|
||||
'cancel' => 'Отмена',
|
||||
],
|
||||
'pagelist' => [
|
||||
'page_link' => 'Ссылка на страницу',
|
||||
'select_page' => 'Выберите страницу...'
|
||||
'select_page' => 'Выберите страницу...',
|
||||
],
|
||||
'relation' => [
|
||||
'missing_config' => "Поведение отношения не имеет конфигурации для ':config'.",
|
||||
|
|
@ -333,11 +346,11 @@ return [
|
|||
'link_name' => 'Соединение :name',
|
||||
'unlink' => 'Отвязать',
|
||||
'unlink_name' => 'Разъединение :name',
|
||||
'unlink_confirm' => 'Вы уверены?'
|
||||
'unlink_confirm' => 'Вы уверены?',
|
||||
],
|
||||
'reorder' => [
|
||||
'default_title' => 'Сортировать записи',
|
||||
'no_records' => 'Нет доступных записей для сортировки.'
|
||||
'no_records' => 'Нет доступных записей для сортировки.',
|
||||
],
|
||||
'model' => [
|
||||
'name' => 'Модель',
|
||||
|
|
@ -355,10 +368,12 @@ return [
|
|||
'extension' => 'Расширение PHP :name не установлено. Установите эту библиотеку и активируйте расширение.',
|
||||
'plugin_missing' => 'Плагин :name имеет зависимость. Установите этот плагин.',
|
||||
'debug' => 'Режим отладки включен. Это не рекомендуется для рабочих инсталяций.',
|
||||
'decompileBackendAssets' => 'Ассеты в бэкенде в настоящее время декомпилированы. Это не рекомендуется для рабочих инсталяций.',
|
||||
],
|
||||
'editor' => [
|
||||
'menu_label' => 'Настройки редактора',
|
||||
'menu_description' => 'Управление настройками редактора кода.',
|
||||
'preview' => 'Предпросмотр',
|
||||
'font_size' => 'Размер шрифта',
|
||||
'tab_size' => 'Размер табуляции',
|
||||
'use_hard_tabs' => 'Использовать табуляцию для индентации',
|
||||
|
|
@ -393,6 +408,7 @@ return [
|
|||
'label' => 'Название',
|
||||
'class_name' => 'Класс',
|
||||
'markup_tags' => 'Теги разметки',
|
||||
'markup_tag' => 'Тег разметки',
|
||||
'allowed_empty_tags' => 'Разрешенные пустые теги',
|
||||
'allowed_empty_tags_comment' => 'Список тегов, которые не будут удаляться, если внутри них нет содержания.',
|
||||
'allowed_tags' => 'Разрешенные теги',
|
||||
|
|
@ -403,20 +419,29 @@ return [
|
|||
'remove_tags_comment' => 'Список тегов, которые будут удалены вместе с их содержанием.',
|
||||
'line_breaker_tags' => 'Теги с переводом строки',
|
||||
'line_breaker_tags_comment' => 'Список тегов, в которых будет использоваться тег перевода строки',
|
||||
'toolbar_options' => 'Опции панели инструментов',
|
||||
'toolbar_buttons' => 'Кнопки панели инструментов',
|
||||
'toolbar_buttons_comment' => 'Кнопки панели инструментов, которые будут отображаться в Rich Editor по умолчанию.'
|
||||
'toolbar_buttons_comment' => 'Кнопки панели инструментов, которые будут отображаться в Rich Editor по умолчанию.',
|
||||
'toolbar_buttons_preset' => 'Вставить предустановленный набор кнопок панели инструментов:',
|
||||
'toolbar_buttons_presets' => [
|
||||
'default' => 'По умолчанию',
|
||||
'minimal' => 'Минимальный',
|
||||
'full' => 'Полный',
|
||||
],
|
||||
'paragraph_formats' => 'Форматы абзацев',
|
||||
'paragraph_formats_comment' => 'Опции появляющиеся в выпадающем списке Форматы абзацев.',
|
||||
],
|
||||
'tooltips' => [
|
||||
'preview_website' => 'Просмотр сайта'
|
||||
'preview_website' => 'Просмотр сайта',
|
||||
],
|
||||
'mysettings' => [
|
||||
'menu_label' => 'Мои настройки',
|
||||
'menu_description' => 'Управление настройками учетной записи администратора.'
|
||||
'menu_description' => 'Управление настройками учетной записи администратора.',
|
||||
],
|
||||
'myaccount' => [
|
||||
'menu_label' => 'Мой аккаунт',
|
||||
'menu_description' => 'Управление личной информацией (имя, почта, пароль)',
|
||||
'menu_keywords' => 'безопасность логин'
|
||||
'menu_keywords' => 'безопасность логин',
|
||||
],
|
||||
'branding' => [
|
||||
'menu_label' => 'Персонализация панели управления',
|
||||
|
|
@ -441,7 +466,7 @@ return [
|
|||
'menu_mode_inline' => 'Строчный',
|
||||
'menu_mode_inline_no_icons' => 'Строчный (без иконок)',
|
||||
'menu_mode_tile' => 'Плитка',
|
||||
'menu_mode_collapsed' => 'Схлопнутый'
|
||||
'menu_mode_collapsed' => 'Схлопнутый',
|
||||
],
|
||||
'backend_preferences' => [
|
||||
'menu_label' => 'Настройки панели управления',
|
||||
|
|
@ -451,7 +476,7 @@ return [
|
|||
'timezone' => 'Часовой пояс',
|
||||
'timezone_comment' => 'Выводить даты в выбранном часовом поясе.',
|
||||
'locale' => 'Язык',
|
||||
'locale_comment' => 'Выберите желаемый язык панели управления.'
|
||||
'locale_comment' => 'Выберите желаемый язык панели управления.',
|
||||
],
|
||||
'access_log' => [
|
||||
'hint' => 'В этом журнале отображается список успешных попыток авторизаций администраторов. Записи хранятся :days дней.',
|
||||
|
|
@ -464,13 +489,13 @@ return [
|
|||
'ip_address' => 'IP адрес',
|
||||
'first_name' => 'Имя',
|
||||
'last_name' => 'Фамилия',
|
||||
'email' => 'Почта'
|
||||
'email' => 'Email',
|
||||
],
|
||||
'filter' => [
|
||||
'all' => 'все',
|
||||
'options_method_not_exists' => "Модель класса :model должна определить метод :method() возвращающего варианты для фильтра ':filter'.",
|
||||
'date_all' => 'весь период',
|
||||
'number_all' => 'все номера'
|
||||
'number_all' => 'все номера',
|
||||
],
|
||||
'import_export' => [
|
||||
'upload_csv_file' => '1. Загрузка CSV-файл',
|
||||
|
|
@ -541,12 +566,14 @@ return [
|
|||
'iso_8859_13' => 'ISO-8859-13 (Latin-7, Baltic Rim)',
|
||||
'iso_8859_14' => 'ISO-8859-14 (Latin-8, Celtic)',
|
||||
'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)'
|
||||
]
|
||||
],
|
||||
'permissions' => [
|
||||
'manage_media' => 'Загрузка и управление медиаконтентом - изображениями, видео, звуками, документами',
|
||||
'allow_unsafe_markdown' => 'Использовать небезопасный Markdown (может включать Javascript)',
|
||||
],
|
||||
'mediafinder' => [
|
||||
'label' => 'Поиск медиа',
|
||||
|
|
@ -614,6 +641,6 @@ return [
|
|||
'selection_mode' => 'Режим выделения',
|
||||
'resize_image' => 'Изменение размера изображения',
|
||||
'image_size' => 'Размер изображения:',
|
||||
'selected_size' => 'Выбрано:'
|
||||
'selected_size' => 'Выбрано:',
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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' => '降冪',
|
||||
]
|
||||
];
|
||||
|
|
|
|||
|
|
@ -9,6 +9,6 @@
|
|||
<?php endif ?>
|
||||
<?php if (EditorSetting::isConfigured()): ?>
|
||||
<style>
|
||||
<?= EditorSetting::renderCss() ?>
|
||||
<?= strip_tags(EditorSetting::renderCss()) ?>
|
||||
</style>
|
||||
<?php endif ?>
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use Lang;
|
|||
use Model;
|
||||
use Response;
|
||||
use League\Csv\Writer as CsvWriter;
|
||||
use October\Rain\Parse\League\EscapeFormula as CsvEscapeFormula;
|
||||
use League\Csv\EscapeFormula as CsvEscapeFormula;
|
||||
use ApplicationException;
|
||||
use SplTempFileObject;
|
||||
|
||||
|
|
@ -112,8 +112,7 @@ abstract class ExportModel extends Model
|
|||
$csv->setEscape($options['escape']);
|
||||
}
|
||||
|
||||
// Temporary until upgrading to league/csv >= 9.1.0 (will be $csv->addFormatter($formatter))
|
||||
$formatter = new CsvEscapeFormula();
|
||||
$csv->addFormatter(new CsvEscapeFormula());
|
||||
|
||||
/*
|
||||
* Add headers
|
||||
|
|
@ -128,10 +127,6 @@ abstract class ExportModel extends Model
|
|||
*/
|
||||
foreach ($results as $result) {
|
||||
$data = $this->matchDataToColumns($result, $columns);
|
||||
|
||||
// Temporary until upgrading to league/csv >= 9.1.0
|
||||
$data = $formatter($data);
|
||||
|
||||
$csv->insertOne($data);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ use Str;
|
|||
use Lang;
|
||||
use Model;
|
||||
use League\Csv\Reader as CsvReader;
|
||||
use League\Csv\Statement as CsvStatement;
|
||||
|
||||
/**
|
||||
* Model used for importing data
|
||||
|
|
@ -108,11 +109,6 @@ abstract class ImportModel extends Model
|
|||
*/
|
||||
$reader = CsvReader::createFromPath($filePath, 'r');
|
||||
|
||||
// Filter out empty rows
|
||||
$reader->addFilter(function (array $row) {
|
||||
return count($row) > 1 || reset($row) !== null;
|
||||
});
|
||||
|
||||
if ($options['delimiter'] !== null) {
|
||||
$reader->setDelimiter($options['delimiter']);
|
||||
}
|
||||
|
|
@ -125,15 +121,11 @@ abstract class ImportModel extends Model
|
|||
$reader->setEscape($options['escape']);
|
||||
}
|
||||
|
||||
if ($options['firstRowTitles']) {
|
||||
$reader->setOffset(1);
|
||||
}
|
||||
|
||||
if (
|
||||
$options['encoding'] !== null &&
|
||||
$reader->isActiveStreamFilter()
|
||||
$reader->supportsStreamFilter()
|
||||
) {
|
||||
$reader->appendStreamFilter(sprintf(
|
||||
$reader->addStreamFilter(sprintf(
|
||||
'%s%s:%s',
|
||||
TranscodeFilter::FILTER_NAME,
|
||||
strtolower($options['encoding']),
|
||||
|
|
@ -141,8 +133,19 @@ abstract class ImportModel extends Model
|
|||
));
|
||||
}
|
||||
|
||||
// Create reader statement
|
||||
$stmt = (new CsvStatement)
|
||||
->where(function (array $row) {
|
||||
// Filter out empty rows
|
||||
return count($row) > 1 || reset($row) !== null;
|
||||
});
|
||||
|
||||
if ($options['firstRowTitles']) {
|
||||
$stmt = $stmt->offset(1);
|
||||
}
|
||||
|
||||
$result = [];
|
||||
$contents = $reader->fetch();
|
||||
$contents = $stmt->process($reader);
|
||||
foreach ($contents as $row) {
|
||||
$result[] = $this->processImportRow($row, $matches);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ class User extends UserBase
|
|||
public $rules = [
|
||||
'email' => 'required|between:6,255|email|unique:backend_users',
|
||||
'login' => 'required|between:2,255|unique:backend_users',
|
||||
'password' => 'required:create|between:4,255|confirmed',
|
||||
'password_confirmation' => 'required_with:password|between:4,255'
|
||||
'password' => 'required:create|min:4|confirmed',
|
||||
'password_confirmation' => 'required_with:password|min:4'
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ App::before(function ($request) {
|
|||
'middleware' => ['web'],
|
||||
'prefix' => Config::get('cms.backendUri', 'backend')
|
||||
], function () {
|
||||
Route::any('{slug}', 'Backend\Classes\BackendController@run')->where('slug', '(.*)?');
|
||||
Route::any('{slug?}', 'Backend\Classes\BackendController@run')->where('slug', '(.*)?');
|
||||
})
|
||||
;
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ use BackendAuth;
|
|||
*/
|
||||
class Filter extends WidgetBase
|
||||
{
|
||||
const DEFAULT_AFTER_DATE = '0001-01-01 00:00:00';
|
||||
|
||||
const DEFAULT_BEFORE_DATE = '2999-12-31 23:59:59';
|
||||
|
||||
//
|
||||
// Configurable properties
|
||||
//
|
||||
|
|
@ -117,7 +121,7 @@ class Filter extends WidgetBase
|
|||
$after = $scope->value[0]->format('Y-m-d H:i:s');
|
||||
$before = $scope->value[1]->format('Y-m-d H:i:s');
|
||||
|
||||
if (strcasecmp($after, '0000-00-00 00:00:00') > 0) {
|
||||
if (strcasecmp($after, self::DEFAULT_AFTER_DATE) > 0) {
|
||||
$params['afterStr'] = Backend::dateTime($scope->value[0], ['formatAlias' => 'dateMin']);
|
||||
$params['after'] = $after;
|
||||
}
|
||||
|
|
@ -126,7 +130,7 @@ class Filter extends WidgetBase
|
|||
$params['after'] = null;
|
||||
}
|
||||
|
||||
if (strcasecmp($before, '2999-12-31 23:59:59') < 0) {
|
||||
if (strcasecmp($before, self::DEFAULT_BEFORE_DATE) < 0) {
|
||||
$params['beforeStr'] = Backend::dateTime($scope->value[1], ['formatAlias' => 'dateMin']);
|
||||
$params['before'] = $before;
|
||||
}
|
||||
|
|
@ -1027,9 +1031,9 @@ class Filter extends WidgetBase
|
|||
$dates[] = Carbon::createFromFormat('Y-m-d H:i:s', $date);
|
||||
} elseif (empty($date)) {
|
||||
if ($i == 0) {
|
||||
$dates[] = Carbon::createFromFormat('Y-m-d H:i:s', '0000-00-00 00:00:00');
|
||||
$dates[] = Carbon::createFromFormat('Y-m-d H:i:s', self::DEFAULT_AFTER_DATE);
|
||||
} else {
|
||||
$dates[] = Carbon::createFromFormat('Y-m-d H:i:s', '2999-12-31 23:59:59');
|
||||
$dates[] = Carbon::createFromFormat('Y-m-d H:i:s', self::DEFAULT_BEFORE_DATE);
|
||||
}
|
||||
} else {
|
||||
$dates = [];
|
||||
|
|
|
|||
|
|
@ -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]}($this, $field);
|
||||
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),
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ use October\Rain\Html\Helper as HtmlHelper;
|
|||
use October\Rain\Router\Helper as RouterHelper;
|
||||
use System\Helpers\DateTime as DateTimeHelper;
|
||||
use System\Classes\PluginManager;
|
||||
use System\Classes\MediaLibrary;
|
||||
use System\Classes\ImageResizer;
|
||||
use Backend\Classes\ListColumn;
|
||||
use Backend\Classes\WidgetBase;
|
||||
use October\Rain\Database\Model;
|
||||
|
|
@ -61,6 +63,12 @@ class Lists extends WidgetBase
|
|||
*/
|
||||
public $recordsPerPage;
|
||||
|
||||
/**
|
||||
* @var array Options for number of items per page.
|
||||
*/
|
||||
public $perPageOptions;
|
||||
|
||||
|
||||
/**
|
||||
* @var bool Shows the sorting options for each column.
|
||||
*/
|
||||
|
|
@ -197,6 +205,7 @@ class Lists extends WidgetBase
|
|||
'noRecordsMessage',
|
||||
'showPageNumbers',
|
||||
'recordsPerPage',
|
||||
'perPageOptions',
|
||||
'showSorting',
|
||||
'defaultSort',
|
||||
'showCheckboxes',
|
||||
|
|
@ -522,7 +531,7 @@ class Lists extends WidgetBase
|
|||
/*
|
||||
* Apply sorting
|
||||
*/
|
||||
if (($sortColumn = $this->getSortColumn()) && !$this->showTree) {
|
||||
if (($sortColumn = $this->getSortColumn()) && !$this->showTree && in_array($sortColumn, array_keys($this->getVisibleColumns()))) {
|
||||
if (($column = array_get($this->allColumns, $sortColumn)) && $column->valueFrom) {
|
||||
$sortColumn = $this->isColumnPivot($column)
|
||||
? 'pivot_' . $column->valueFrom
|
||||
|
|
@ -1027,7 +1036,8 @@ class Lists extends WidgetBase
|
|||
$value = $record->attributes[$columnName];
|
||||
// Load the value from the relationship counter if useRelationCount is specified
|
||||
} elseif ($column->relation && @$column->config['useRelationCount']) {
|
||||
$value = $record->{"{$column->relation}_count"};
|
||||
$countAttributeName = \Str::snake($column->relation);
|
||||
$value = $record->{"{$countAttributeName}_count"};
|
||||
} else {
|
||||
$value = $record->{$columnName};
|
||||
}
|
||||
|
|
@ -1187,6 +1197,42 @@ class Lists extends WidgetBase
|
|||
return htmlentities($value, ENT_QUOTES, 'UTF-8', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an image value
|
||||
* @return string
|
||||
*/
|
||||
protected function evalImageTypeValue($record, $column, $value)
|
||||
{
|
||||
$config = $column->config;
|
||||
|
||||
// Get config options with defaults
|
||||
$width = isset($config['width']) ? $config['width'] : 50;
|
||||
$height = isset($config['height']) ? $config['height'] : 50;
|
||||
$options = isset($config['options']) ? $config['options'] : [];
|
||||
|
||||
// Handle attachMany relationships
|
||||
if (isset($record->attachMany[$column->columnName])) {
|
||||
$image = $value->first();
|
||||
|
||||
// Handle attachOne relationships
|
||||
} elseif (isset($record->attachOne[$column->columnName])) {
|
||||
$image = $value;
|
||||
|
||||
// Handle absolute URLs
|
||||
} elseif (str_contains($value, '://')) {
|
||||
$image = $value;
|
||||
|
||||
// Assume all other values to be from the media library
|
||||
} else {
|
||||
$image = MediaLibrary::url($value);
|
||||
}
|
||||
|
||||
if (!is_null($image)) {
|
||||
$imageUrl = ImageResizer::filterGetUrl($image, $width, $height, $options);
|
||||
return "<img src='$imageUrl' width='$width' height='$height' />";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process as number, proxy to text
|
||||
* @return string
|
||||
|
|
@ -1651,7 +1697,7 @@ class Lists extends WidgetBase
|
|||
*/
|
||||
protected function getSetupPerPageOptions()
|
||||
{
|
||||
$perPageOptions = [20, 40, 80, 100, 120];
|
||||
$perPageOptions = is_array($this->perPageOptions) ? $this->perPageOptions : [20, 40, 80, 100, 120];
|
||||
if (!in_array($this->recordsPerPage, $perPageOptions)) {
|
||||
$perPageOptions[] = $this->recordsPerPage;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<div
|
||||
id="<?= $this->getId(); ?>"
|
||||
class="control-filter <?= $cssClasses ?>"
|
||||
data-control="filterwidget"
|
||||
data-options-handler="<?= $this->getEventHandler('onFilterGetOptions') ?>"
|
||||
|
|
|
|||
|
|
@ -42,6 +42,11 @@
|
|||
}
|
||||
|
||||
FormWidget.prototype.dispose = function() {
|
||||
this.unbindDependants()
|
||||
this.unbindCheckboxList()
|
||||
this.unbindLazyTabs()
|
||||
this.unbindCollapsibleSections()
|
||||
|
||||
this.$el.off('dispose-control', this.proxy(this.dispose))
|
||||
this.$el.removeData('oc.formwidget')
|
||||
|
||||
|
|
@ -75,6 +80,14 @@
|
|||
|
||||
}
|
||||
|
||||
/*
|
||||
* Unbind checkboxlist handlers
|
||||
*/
|
||||
FormWidget.prototype.unbindCheckboxList = function() {
|
||||
this.$el.off('click', '[data-field-checkboxlist-all]')
|
||||
this.$el.off('click', '[data-field-checkboxlist-none]')
|
||||
}
|
||||
|
||||
/*
|
||||
* Get all fields elements that belong to this form, nested form
|
||||
* fields are removed from this collection.
|
||||
|
|
@ -94,13 +107,40 @@
|
|||
* Bind dependant fields
|
||||
*/
|
||||
FormWidget.prototype.bindDependants = function() {
|
||||
var self = this,
|
||||
fieldMap = this._getDependants()
|
||||
|
||||
/*
|
||||
* When a field is updated, refresh its dependents
|
||||
*/
|
||||
$.each(fieldMap, function(fieldName, toRefresh) {
|
||||
$(document).on('change.oc.formwidget',
|
||||
'[data-field-name="' + fieldName + '"]',
|
||||
$.proxy(self.onRefreshDependants, self, fieldName, toRefresh)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Dispose of the dependant field handlers
|
||||
*/
|
||||
FormWidget.prototype.unbindDependants = function() {
|
||||
var fieldMap = this._getDependants()
|
||||
|
||||
$.each(fieldMap, function(fieldName, toRefresh) {
|
||||
$(document).off('change.oc.formwidget', '[data-field-name="' + fieldName + '"]')
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Retrieve the dependant fields
|
||||
*/
|
||||
FormWidget.prototype._getDependants = function() {
|
||||
if (!$('[data-field-depends]', this.$el).length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this,
|
||||
fieldMap = {},
|
||||
var fieldMap = {},
|
||||
fieldElements = this.getFieldElements()
|
||||
|
||||
/*
|
||||
|
|
@ -119,15 +159,7 @@
|
|||
})
|
||||
})
|
||||
|
||||
/*
|
||||
* When a master is updated, refresh its slaves
|
||||
*/
|
||||
$.each(fieldMap, function(fieldName, toRefresh) {
|
||||
$(document).on('change.oc.formwidget',
|
||||
'[data-field-name="' + fieldName + '"]',
|
||||
$.proxy(self.onRefreshDependants, self, fieldName, toRefresh)
|
||||
);
|
||||
})
|
||||
return fieldMap
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -154,6 +186,9 @@
|
|||
data: refreshData
|
||||
}).success(function() {
|
||||
self.toggleEmptyTabs()
|
||||
$.each(toRefresh.fields, function(key, field) {
|
||||
$('[data-field-name="' + field + '"]').trigger('change')
|
||||
})
|
||||
})
|
||||
}, this.dependantUpdateInterval)
|
||||
|
||||
|
|
@ -203,6 +238,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Unbind the lazy tab handlers
|
||||
*/
|
||||
FormWidget.prototype.unbindLazyTabs = function() {
|
||||
var tabControl = $('[data-control=tab]', this.$el)
|
||||
|
||||
$('.nav-tabs', tabControl).off('click', '.tab-lazy [data-toggle="tab"]')
|
||||
}
|
||||
|
||||
/*
|
||||
* Hides tabs that have no content, it is possible this can be
|
||||
* called multiple times in a single cycle due to input.trigger.
|
||||
|
|
@ -262,6 +306,13 @@
|
|||
.nextUntil('.section-field').hide()
|
||||
}
|
||||
|
||||
/*
|
||||
* Unbinds collapsible section handlers
|
||||
*/
|
||||
FormWidget.prototype.unbindCollapsibleSections = function() {
|
||||
$('.section-field[data-field-collapsible]', this.$form).off('click')
|
||||
}
|
||||
|
||||
FormWidget.DEFAULTS = {
|
||||
refreshHandler: null,
|
||||
refreshData: {}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
$index++;
|
||||
$checkboxId = 'checkbox_'.$field->getId().'_'.$index;
|
||||
if (!in_array($value, $checkedValues)) continue;
|
||||
if (is_string($option)) $option = [$option];
|
||||
if (!is_array($option)) $option = [$option];
|
||||
?>
|
||||
<div class="checkbox custom-checkbox">
|
||||
<input
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
<?php
|
||||
$index++;
|
||||
$checkboxId = 'checkbox_'.$field->getId().'_'.$index;
|
||||
if (is_string($option)) $option = [$option];
|
||||
if (!is_array($option)) $option = [$option];
|
||||
?>
|
||||
<div class="checkbox custom-checkbox">
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -6,7 +6,19 @@
|
|||
<!-- Dropdown -->
|
||||
<?php if ($this->previewMode || $field->readOnly): ?>
|
||||
<div class="form-control" <?= $field->readOnly ? 'disabled="disabled"' : ''; ?>>
|
||||
<?= (isset($fieldOptions[$field->value])) ? e(trans($fieldOptions[$field->value])) : '' ?>
|
||||
<?php if (isset($fieldOptions[$field->value]) && is_array($fieldOptions[$field->value])): ?>
|
||||
<?php if (strpos($fieldOptions[$field->value][1], '.')): ?>
|
||||
<img src="<?= $fieldOptions[$field->value][1] ?>" alt="">
|
||||
<?= e(trans($fieldOptions[$field->value][0])) ?>
|
||||
<?php else: ?>
|
||||
<i class="<?= $fieldOptions[$field->value][1] ?>"></i>
|
||||
<?= e(trans($fieldOptions[$field->value][0])) ?>
|
||||
<?php endif ?>
|
||||
<?php elseif (isset($fieldOptions[$field->value]) && !is_array($fieldOptions[$field->value])): ?>
|
||||
<?= e(trans($fieldOptions[$field->value])) ?>
|
||||
<?php else: ?>
|
||||
<?= '' ?>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
<?php if ($field->readOnly): ?>
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -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,24 @@ class ServiceProvider extends ModuleServiceProvider
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Boots localization from an active theme for backend items.
|
||||
*/
|
||||
protected function bootBackendLocalization()
|
||||
{
|
||||
$theme = CmsTheme::getActiveTheme();
|
||||
|
||||
if (is_null($theme)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$langPath = $theme->getPath() . '/lang';
|
||||
|
||||
if (File::isDirectory($langPath)) {
|
||||
Lang::addNamespace('themes.' . $theme->getId(), $langPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers events for menu items.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@ use File;
|
|||
use Lang;
|
||||
use Config;
|
||||
use Request;
|
||||
use Cms\Helpers\File as FileHelper;
|
||||
use October\Rain\Extension\Extendable;
|
||||
use ApplicationException;
|
||||
use ValidationException;
|
||||
use Cms\Helpers\File as FileHelper;
|
||||
use October\Rain\Extension\Extendable;
|
||||
use October\Rain\Filesystem\PathResolver;
|
||||
|
||||
/**
|
||||
* The CMS theme asset file class.
|
||||
|
|
@ -285,15 +286,15 @@ class Asset extends Extendable
|
|||
$fileName = $this->fileName;
|
||||
}
|
||||
|
||||
// Limit paths to those under the assets directory
|
||||
$directory = realpath($this->theme->getPath() . '/' . $this->dirName . '/');
|
||||
$path = realpath($directory . '/' . $fileName);
|
||||
$directory = $this->theme->getPath() . '/' . $this->dirName . '/';
|
||||
$filePath = $directory . $fileName;
|
||||
|
||||
if ($path !== false && !starts_with($path, $directory)) {
|
||||
// Limit paths to those under the theme's assets directory
|
||||
if (!PathResolver::within($filePath, $directory)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $path;
|
||||
return PathResolver::resolve($filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -98,6 +98,16 @@ class CmsCompoundObject extends CmsObject
|
|||
*/
|
||||
public function beforeSave()
|
||||
{
|
||||
// Ignore line-ending only changes to the code property to avoid triggering safe mode
|
||||
// when no changes actually occurred, it was just the browser reformatting line endings
|
||||
if ($this->isDirty('code')) {
|
||||
$oldCode = str_replace("\n", "\r\n", str_replace("\r", '', $this->getOriginal('code')));
|
||||
$newCode = str_replace("\n", "\r\n", str_replace("\r", '', $this->code));
|
||||
if ($oldCode === $newCode) {
|
||||
$this->code = $this->getOriginal('code');
|
||||
}
|
||||
}
|
||||
|
||||
$this->checkSafeMode();
|
||||
}
|
||||
|
||||
|
|
@ -316,7 +326,8 @@ class CmsCompoundObject extends CmsObject
|
|||
|
||||
self::$objectComponentPropertyMap = $objectComponentMap;
|
||||
|
||||
Cache::put($key, base64_encode(serialize($objectComponentMap)), Config::get('cms.parsedPageCacheTTL', 10));
|
||||
$expiresAt = now()->addMinutes(Config::get('cms.parsedPageCacheTTL', 10));
|
||||
Cache::put($key, base64_encode(serialize($objectComponentMap)), $expiresAt);
|
||||
|
||||
if (array_key_exists($componentName, $objectComponentMap[$objectCode])) {
|
||||
return $objectComponentMap[$objectCode][$componentName];
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@ use App;
|
|||
use Lang;
|
||||
use Event;
|
||||
use Config;
|
||||
use October\Rain\Halcyon\Model as HalcyonModel;
|
||||
use Cms\Contracts\CmsObject as CmsObjectContract;
|
||||
use ApplicationException;
|
||||
use ValidationException;
|
||||
use Exception;
|
||||
use ValidationException;
|
||||
use ApplicationException;
|
||||
use Cms\Contracts\CmsObject as CmsObjectContract;
|
||||
use October\Rain\Filesystem\PathResolver;
|
||||
use October\Rain\Halcyon\Model as HalcyonModel;
|
||||
|
||||
/**
|
||||
* This is a base class for all CMS objects - content files, pages, partials and layouts.
|
||||
|
|
@ -234,7 +235,15 @@ class CmsObject extends HalcyonModel implements CmsObjectContract
|
|||
$fileName = $this->fileName;
|
||||
}
|
||||
|
||||
return $this->theme->getPath().'/'.$this->getObjectTypeDirName().'/'.$fileName;
|
||||
$directory = $this->theme->getPath() . '/' . $this->getObjectTypeDirName() . '/';
|
||||
$filePath = $directory . $fileName;
|
||||
|
||||
// Limit paths to those under the corresponding theme directory
|
||||
if (!PathResolver::within($filePath, $directory)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return PathResolver::resolve($filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<?php namespace Cms\Classes;
|
||||
|
||||
use ApplicationException;
|
||||
use October\Rain\Support\Collection as CollectionBase;
|
||||
|
||||
/**
|
||||
|
|
@ -37,15 +38,32 @@ class CmsObjectCollection extends CollectionBase
|
|||
|
||||
/**
|
||||
* Returns objects whose properties match the supplied value.
|
||||
* @param string $property
|
||||
* @param string $value
|
||||
* @param bool $strict
|
||||
*
|
||||
* Note that this deviates from Laravel 6's Illuminate\Support\Traits\EnumeratesValues::where() method signature,
|
||||
* which uses ($key, $operator = null, $value = null) as parameters and that this class extends.
|
||||
*
|
||||
* To ensure backwards compatibility with our current Halcyon functionality, this method retains the original
|
||||
* parameters and functions the same way as before, with handling for the $value and $strict parameters to ensure
|
||||
* they match the previously expected formats. This means that you cannot use operators for "where" queries on
|
||||
* CMS object collections.
|
||||
*
|
||||
* @param string $property
|
||||
* @param string $value
|
||||
* @param bool $strict
|
||||
* @return static
|
||||
*/
|
||||
public function where($property, $value, $strict = true)
|
||||
public function where($property, $value = null, $strict = null)
|
||||
{
|
||||
return $this->filter(function ($object) use ($property, $value, $strict) {
|
||||
if (empty($value) || !is_string($value)) {
|
||||
throw new ApplicationException('You must provide a string value to compare with when executing a "where" '
|
||||
. 'query for CMS object collections.');
|
||||
}
|
||||
|
||||
if (!isset($strict) || !is_bool($strict)) {
|
||||
$strict = true;
|
||||
}
|
||||
|
||||
return $this->filter(function ($object) use ($property, $value, $strict) {
|
||||
if (!array_key_exists($property, $object->settings)) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ class CodeParser
|
|||
$body = preg_replace('/^\s*function/m', 'public function', $body);
|
||||
|
||||
$namespaces = [];
|
||||
$pattern = '/(use\s+[a-z0-9_\\\\]+(\s+as\s+[a-z0-9_]+)?;\n?)/mi';
|
||||
$pattern = '/(use\s+[a-z0-9_\\\\]+(\s+as\s+[a-z0-9_]+)?;(\r\n|\n)?)/mi';
|
||||
preg_match_all($pattern, $body, $namespaces);
|
||||
$body = preg_replace($pattern, '', $body);
|
||||
|
||||
|
|
@ -141,14 +141,15 @@ class CodeParser
|
|||
$fileContents = '<?php '.PHP_EOL;
|
||||
|
||||
foreach ($namespaces[0] as $namespace) {
|
||||
if (str_contains($namespace, '\\')) {
|
||||
$fileContents .= $namespace;
|
||||
// Only allow compound or aliased use statements
|
||||
if (str_contains($namespace, '\\') || str_contains($namespace, ' as ')) {
|
||||
$fileContents .= trim($namespace).PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
$fileContents .= 'class '.$className.$parentClass.PHP_EOL;
|
||||
$fileContents .= '{'.PHP_EOL;
|
||||
$fileContents .= $body.PHP_EOL;
|
||||
$fileContents .= trim($body).PHP_EOL;
|
||||
$fileContents .= '}'.PHP_EOL;
|
||||
|
||||
$this->validate($fileContents);
|
||||
|
|
@ -224,7 +225,8 @@ class CodeParser
|
|||
$cached = $this->getCachedInfo() ?: [];
|
||||
$cached[$this->filePath] = $cacheItem;
|
||||
|
||||
Cache::put($this->dataCacheKey, base64_encode(serialize($cached)), 1440);
|
||||
$expiresAt = now()->addMinutes(1440);
|
||||
Cache::put($this->dataCacheKey, base64_encode(serialize($cached)), $expiresAt);
|
||||
|
||||
self::$cache[$this->filePath] = $result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ class Controller
|
|||
$url = Request::path();
|
||||
}
|
||||
|
||||
if (empty($url)) {
|
||||
if (trim($url) === '') {
|
||||
$url = '/';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?php namespace Cms\Classes;
|
||||
|
||||
use Lang;
|
||||
use BackendAuth;
|
||||
use ApplicationException;
|
||||
use October\Rain\Filesystem\Definitions as FileDefinitions;
|
||||
|
||||
|
|
@ -184,6 +185,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);
|
||||
|
||||
|
|
|
|||
|
|
@ -13,11 +13,6 @@ class Partial extends CmsCompoundObject
|
|||
*/
|
||||
protected $dirName = 'partials';
|
||||
|
||||
/**
|
||||
* @var array Allowable file extensions. TEMPORARY! DO NOT INCLUDE in Build 1.1.x, workaround for unsupported code
|
||||
*/
|
||||
protected $allowedExtensions = ['htm', 'html', 'css', 'js', 'svg'];
|
||||
|
||||
/**
|
||||
* Returns name of a PHP class to us a parent for the PHP class created for the object's PHP section.
|
||||
* @return string Returns the class name.
|
||||
|
|
|
|||
|
|
@ -127,10 +127,11 @@ class Router
|
|||
: $fileName;
|
||||
|
||||
$key = $this->getUrlListCacheKey();
|
||||
$expiresAt = now()->addMinutes(Config::get('cms.urlCacheTtl', 1));
|
||||
Cache::put(
|
||||
$key,
|
||||
base64_encode(serialize($urlList)),
|
||||
Config::get('cms.urlCacheTtl', 1)
|
||||
$expiresAt
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -251,7 +252,8 @@ class Router
|
|||
|
||||
$this->urlMap = $map;
|
||||
if ($cacheable) {
|
||||
Cache::put($key, base64_encode(serialize($map)), Config::get('cms.urlCacheTtl', 1));
|
||||
$expiresAt = now()->addMinutes(Config::get('cms.urlCacheTtl', 1));
|
||||
Cache::put($key, base64_encode(serialize($map)), $expiresAt);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
@ -304,10 +306,13 @@ class Router
|
|||
* @param string|null $default
|
||||
* @return string|null
|
||||
*/
|
||||
public function getParameter(string $name, string $default = null)
|
||||
public function getParameter($name, $default = null)
|
||||
{
|
||||
$value = $this->parameters[$name] ?? '';
|
||||
return $value !== '' ? $value : $default;
|
||||
if (isset($this->parameters[$name]) && ($this->parameters[$name] === '0' || !empty($this->parameters[$name]))) {
|
||||
return $this->parameters[$name];
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -158,7 +158,8 @@ class Theme
|
|||
if ($checkDatabase && App::hasDatabase()) {
|
||||
try {
|
||||
try {
|
||||
$dbResult = Cache::remember(self::ACTIVE_KEY, 1440, function () {
|
||||
$expiresAt = now()->addMinutes(1440);
|
||||
$dbResult = Cache::remember(self::ACTIVE_KEY, $expiresAt, function () {
|
||||
return Parameter::applyKey(self::ACTIVE_KEY)->value('value');
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,23 +8,18 @@
|
|||
"authors": [
|
||||
{
|
||||
"name": "Alexey Bobkov",
|
||||
"email": "aleksey.bobkov@gmail.com"
|
||||
"email": "aleksey.bobkov@gmail.com",
|
||||
"role": "Co-founder"
|
||||
},
|
||||
{
|
||||
"name": "Samuel Georges",
|
||||
"email": "daftspunky@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Luke Towers",
|
||||
"email": "octobercms@luketowers.ca",
|
||||
"homepage": "https://luketowers.ca",
|
||||
"role": "Maintainer"
|
||||
"email": "daftspunky@gmail.com",
|
||||
"role": "Co-founder"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.0",
|
||||
"composer/installers": "~1.0",
|
||||
"october/rain": "~1.0.469"
|
||||
"php": ">=7.2",
|
||||
"composer/installers": "~1.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue