This commit is contained in:
Shohrat 2023-02-15 20:52:41 +05:00
parent 6fb81c4df3
commit 63f1adb70d
8721 changed files with 507418 additions and 325391 deletions

5
.env
View File

@ -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

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
config/database.php

View File

@ -0,0 +1,148 @@
# QuanKim/JwtAuth custom plugin for CakePHP
[![Build Status](https://img.shields.io/travis/QuanKim/cakephp-jwt-auth/master.svg?style=flat-square)](https://travis-ci.org/QuanKim/cakephp-jwt-auth)
[![Coverage](https://img.shields.io/codecov/c/github/QuanKim/cakephp-jwt-auth.svg?style=flat-square)](https://codecov.io/github/QuanKim/cakephp-jwt-auth)
[![Total Downloads](https://img.shields.io/packagist/dt/QuanKim/cakephp-jwt-auth.svg?style=flat-square)](https://packagist.org/packages/QuanKim/cakephp-jwt-auth)
[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](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.

View File

@ -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"
}
}
}

View File

@ -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();
}
}

View File

@ -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');
}
);

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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
{
}

View File

@ -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']
]);
}
}
}
}

21
Plugin/PhpJwt/LICENSE Normal file
View File

@ -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.

11
Plugin/PhpJwt/README.md Normal file
View File

@ -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
```

View File

@ -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"
}
}
}

View File

@ -0,0 +1,10 @@
<?php
use Cake\Routing\Router;
Router::plugin(
'QuanKim/PhpJwt',
['path' => '/quan-kim/php-jwt'],
function ($routes) {
$routes->fallbacks('DashedRoute');
}
);

View File

@ -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>

View File

@ -0,0 +1,7 @@
<?php
namespace QuanKim\PhpJwt;
class BeforeValidException extends \UnexpectedValueException
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace QuanKim\PhpJwt;
class ExpiredException extends \UnexpectedValueException
{
}

397
Plugin/PhpJwt/src/JWT.php Normal file
View File

@ -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);
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace QuanKim\PhpJwt;
class SignatureInvalidException extends \UnexpectedValueException
{
}

View File

@ -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": {

5306
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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' => '',

View File

@ -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');
});
}

View File

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

View File

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

View File

@ -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

View File

@ -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']),

View File

@ -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);

View File

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

View File

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

View File

@ -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)) {

View File

@ -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;

View File

@ -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.

View File

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

View File

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

View File

@ -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": {

View File

@ -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'
];

View File

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

View File

@ -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}</>" : '';
}
}

View File

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

View File

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

View File

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

View File

@ -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();
}

View File

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

View File

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

View File

@ -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');
}
}

View File

@ -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)

View File

@ -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>

View File

@ -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>

View File

@ -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']

View File

@ -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()
})
}

View File

@ -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",

View File

@ -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}

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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() ?>"

View File

@ -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) {

View File

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

View File

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

View File

@ -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',

View File

@ -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 lutilisateur, dans longlet 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',

View File

@ -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',

View File

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

View File

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

View File

@ -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',

View File

@ -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' => 'Выбрано:',
],
];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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'
];
/**

View File

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

View File

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

View File

@ -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', '(.*)?');
})
;

View File

@ -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 = [];

View File

@ -1283,6 +1283,7 @@ class Form extends WidgetBase
{
/*
* Advanced usage, supplied options are callable
* [\Path\To\Class, methodName]
*/
if (is_array($fieldOptions) && is_callable($fieldOptions)) {
$fieldOptions = call_user_func($fieldOptions, $this, $field);
@ -1328,6 +1329,22 @@ class Form extends WidgetBase
* Field options are an explicit method reference
*/
elseif (is_string($fieldOptions)) {
// \Path\To\Class::staticMethodOptions
if (str_contains($fieldOptions, '::')) {
$options = explode('::', $fieldOptions);
if (count($options) === 2 && class_exists($options[0]) && method_exists($options[0], $options[1])) {
$result = $options[0]::{$options[1]}($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),

View File

@ -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;
}

View File

@ -1,4 +1,5 @@
<div
id="<?= $this->getId(); ?>"
class="control-filter <?= $cssClasses ?>"
data-control="filterwidget"
data-options-handler="<?= $this->getEventHandler('onFilterGetOptions') ?>"

View File

@ -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: {}

View File

@ -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

View File

@ -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

View File

@ -1,11 +1,15 @@
<?php namespace Cms;
use App;
use Url;
use Lang;
use File;
use Event;
use Backend;
use BackendMenu;
use BackendAuth;
use Backend\Models\UserRole;
use Cms\Classes\Theme as CmsTheme;
use Backend\Classes\WidgetManager;
use October\Rain\Support\ModuleServiceProvider;
use System\Classes\SettingsManager;
@ -54,6 +58,10 @@ class ServiceProvider extends ModuleServiceProvider
$this->bootMenuItemEvents();
$this->bootRichEditorEvents();
if (App::runningInBackend()) {
$this->bootBackendLocalization();
}
}
/**
@ -167,6 +175,19 @@ class ServiceProvider extends ModuleServiceProvider
]
]
]);
$manager->registerQuickActions('October.Cms', [
'preview' => [
'label' => 'backend::lang.tooltips.preview_website',
'icon' => 'icon-crosshairs',
'url' => Url::to('/'),
'order' => 10,
'attributes' => [
'target' => '_blank',
'rel' => 'noopener noreferrer',
],
],
]);
});
}
@ -284,6 +305,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.
*/

View File

@ -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);
}
/**

View File

@ -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];

View File

@ -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);
}
/**

View File

@ -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;
}

View File

@ -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;
}

View File

@ -145,7 +145,7 @@ class Controller
$url = Request::path();
}
if (empty($url)) {
if (trim($url) === '') {
$url = '/';
}

View File

@ -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);

View File

@ -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.

View File

@ -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;
}
/**

View File

@ -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');
});
}

View File

@ -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": {

View File

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

Some files were not shown because too many files have changed in this diff Show More