mirror of
https://github.com/espocrm/espocrm.git
synced 2026-03-11 19:37:02 +00:00
Compare commits
106 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
235d8f2264 | ||
|
|
9cfab5a7ea | ||
|
|
86a85ba177 | ||
|
|
36a4cb2451 | ||
|
|
6f60a73c62 | ||
|
|
72fd9184f4 | ||
|
|
0a5ae621b4 | ||
|
|
4e588f6d0e | ||
|
|
f60c5e29de | ||
|
|
1662cfd97d | ||
|
|
0fd8f9d29f | ||
|
|
bf033c31af | ||
|
|
aa788a17d9 | ||
|
|
036ad99ba6 | ||
|
|
7fda7abbe6 | ||
|
|
fa0cf9c9fd | ||
|
|
3c651f8633 | ||
|
|
2b5f1c28e8 | ||
|
|
7fea881d17 | ||
|
|
ada64bba0c | ||
|
|
8fe96b140a | ||
|
|
3b25a1a001 | ||
|
|
f4d98f177c | ||
|
|
9f4f38bb0d | ||
|
|
e76490880a | ||
|
|
e12bb1320c | ||
|
|
c84abfb542 | ||
|
|
656f66f567 | ||
|
|
eca5bdea71 | ||
|
|
2ab4173e85 | ||
|
|
195db973dc | ||
|
|
cd5537cc26 | ||
|
|
94008f5a53 | ||
|
|
bf410b2258 | ||
|
|
0f2a26b744 | ||
|
|
c11fd843d0 | ||
|
|
70a374004d | ||
|
|
0b218833af | ||
|
|
4a1e5c974c | ||
|
|
b08083173a | ||
|
|
c24e7a6939 | ||
|
|
ce99671583 | ||
|
|
d36e5cc0c1 | ||
|
|
13ae2d27c8 | ||
|
|
ea5c76f012 | ||
|
|
acb9b50d14 | ||
|
|
968fc7ad30 | ||
|
|
bb6d7598a1 | ||
|
|
cd9481670a | ||
|
|
505e9e278b | ||
|
|
7e59888fb2 | ||
|
|
74f2d7f1ea | ||
|
|
f319be61d2 | ||
|
|
2cc4c8f974 | ||
|
|
a3aa74013a | ||
|
|
509b3affd1 | ||
|
|
ad9b2b54dd | ||
|
|
247eb00963 | ||
|
|
c0b59a49bf | ||
|
|
b525afe154 | ||
|
|
b449263854 | ||
|
|
12550c3d0a | ||
|
|
803a4ffb7f | ||
|
|
57bfea4d70 | ||
|
|
86f460866a | ||
|
|
8515157916 | ||
|
|
5bd18dee49 | ||
|
|
88ea5e7d6c | ||
|
|
3839335508 | ||
|
|
b8e94b52aa | ||
|
|
eeda450405 | ||
|
|
d051bd7df2 | ||
|
|
690a265450 | ||
|
|
8bec0a3caf | ||
|
|
a89463367e | ||
|
|
05dbcbd917 | ||
|
|
774bde3e20 | ||
|
|
90589e0d26 | ||
|
|
be199235f1 | ||
|
|
1b2d67b027 | ||
|
|
7bc56fc864 | ||
|
|
c706ddc809 | ||
|
|
1b6b2ea140 | ||
|
|
c1db037fc2 | ||
|
|
838e9ea773 | ||
|
|
10efe3513c | ||
|
|
6d301092b2 | ||
|
|
5acb5da8fa | ||
|
|
3ad343b274 | ||
|
|
c5fd749b21 | ||
|
|
1f8e0f16c7 | ||
|
|
92babe7fbc | ||
|
|
fb684837f8 | ||
|
|
669413b184 | ||
|
|
a945a01767 | ||
|
|
b4664eafa5 | ||
|
|
34b06b83aa | ||
|
|
da14681077 | ||
|
|
53e91ad683 | ||
|
|
06434bef99 | ||
|
|
16fab0ced0 | ||
|
|
bc3e531447 | ||
|
|
af038e3306 | ||
|
|
1a06b83d9d | ||
|
|
bcc3cfd143 | ||
|
|
b73123c137 |
@@ -313,11 +313,11 @@ module.exports = function (grunt) {
|
||||
});
|
||||
|
||||
grunt.registerTask("unit-tests-run", function() {
|
||||
cp.execSync("vendor/bin/phpunit --bootstrap=vendor/autoload.php tests/unit", {stdio: 'inherit'});
|
||||
cp.execSync("vendor/bin/phpunit --bootstrap=./vendor/autoload.php tests/unit", {stdio: 'inherit'});
|
||||
});
|
||||
|
||||
grunt.registerTask("integration-tests-run", function() {
|
||||
cp.execSync("vendor/bin/phpunit --bootstrap=vendor/autoload.php tests/integration", {stdio: 'inherit'});
|
||||
cp.execSync("vendor/bin/phpunit --bootstrap=./vendor/autoload.php tests/integration", {stdio: 'inherit'});
|
||||
});
|
||||
|
||||
grunt.registerTask("zip", function() {
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace Espo\Controllers;
|
||||
|
||||
use \Espo\Core\Exceptions\Forbidden;
|
||||
use \Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
|
||||
class EmailAccount extends \Espo\Core\Controllers\Record
|
||||
{
|
||||
|
||||
@@ -93,12 +93,7 @@ class ExternalAccount extends \Espo\Core\Controllers\Record
|
||||
{
|
||||
list($integration, $userId) = explode('__', $params['id']);
|
||||
|
||||
if ($this->getUser()->id != $userId && !$this->getUser()->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$entity = $this->getEntityManager()->getEntity('ExternalAccount', $params['id']);
|
||||
return $entity->toArray();
|
||||
return $this->getRecordService()->read($params['id'])->getValueMap();
|
||||
}
|
||||
|
||||
public function actionUpdate($params, $data, $request)
|
||||
|
||||
@@ -69,29 +69,7 @@ class User extends \Espo\Core\Controllers\Record
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
if ($this->getConfig()->get('passwordRecoveryDisabled')) {
|
||||
throw new Forbidden("Password recovery disabled");
|
||||
}
|
||||
|
||||
$request = $this->getEntityManager()->getRepository('PasswordChangeRequest')->where([
|
||||
'requestId' => $data->requestId
|
||||
])->findOne();
|
||||
|
||||
if (!$request) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$userId = $request->get('userId');
|
||||
if (!$userId) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
if ($this->getService('User')->changePassword($userId, $data->password)) {
|
||||
$this->getEntityManager()->removeEntity($request);
|
||||
return [
|
||||
'url' => $request->get('url')
|
||||
];
|
||||
}
|
||||
return $this->getService('User')->changePasswordByRequest($data->requestId, $data->password);
|
||||
}
|
||||
|
||||
public function postActionPasswordChangeRequest($params, $data, $request)
|
||||
|
||||
@@ -260,6 +260,9 @@ class Application
|
||||
$route = $slim->router()->getCurrentRoute();
|
||||
$conditions = $route->getConditions();
|
||||
|
||||
$response = $slim->response();
|
||||
$response->headers->set('Content-Type', 'application/json');
|
||||
|
||||
if (isset($conditions['useController']) && $conditions['useController'] == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -58,6 +58,8 @@ class SetPassword extends Base
|
||||
|
||||
$password = $this->ask();
|
||||
|
||||
$password = trim($password);
|
||||
|
||||
if (!$password) {
|
||||
$this->out("Password can not be empty.\n");
|
||||
die;
|
||||
|
||||
@@ -140,8 +140,7 @@ class Upgrade extends Base
|
||||
|
||||
fwrite(\STDOUT, "\n");
|
||||
|
||||
$app = new \Espo\Core\Application();
|
||||
$currentVerison = $app->getContainer()->get('config')->get('version');
|
||||
$currentVerison = $this->getCurrentVersion();
|
||||
|
||||
fwrite(\STDOUT, "Upgrade is complete. Current version is {$currentVerison}.\n");
|
||||
|
||||
@@ -211,8 +210,7 @@ class Upgrade extends Base
|
||||
return;
|
||||
}
|
||||
|
||||
$app = new \Espo\Core\Application();
|
||||
$currentVerison = $app->getContainer()->get('config')->get('version');
|
||||
$currentVerison = $this->getCurrentVersion();
|
||||
|
||||
fwrite(\STDOUT, "Upgrade is complete. Current version is {$currentVerison}.\n");
|
||||
|
||||
@@ -406,4 +404,15 @@ class Upgrade extends Base
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getCurrentVersion()
|
||||
{
|
||||
$configData = include "data/config.php";
|
||||
|
||||
if (!$configData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $configData['version'] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,11 +29,12 @@
|
||||
|
||||
namespace Espo\Core\Controllers;
|
||||
|
||||
use \Espo\Core\Exceptions\Error;
|
||||
use \Espo\Core\Exceptions\Forbidden;
|
||||
use \Espo\Core\Exceptions\NotFound;
|
||||
use \Espo\Core\Exceptions\BadRequest;
|
||||
use \Espo\Core\Utils\Util;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Utils\Util;
|
||||
use Espo\Core\Exceptions\ForbiddenSilent;
|
||||
|
||||
class Record extends Base
|
||||
{
|
||||
@@ -88,7 +89,7 @@ class Record extends Base
|
||||
}
|
||||
|
||||
if (!$this->getAcl()->check($this->name, 'create')) {
|
||||
throw new Forbidden();
|
||||
throw new Forbidden("No create access for {$this->name}.");
|
||||
}
|
||||
|
||||
$service = $this->getRecordService();
|
||||
@@ -109,7 +110,7 @@ class Record extends Base
|
||||
}
|
||||
|
||||
if (!$this->getAcl()->check($this->name, 'edit')) {
|
||||
throw new Forbidden();
|
||||
throw new Forbidden("No edit access for {$this->name}.");
|
||||
}
|
||||
|
||||
$id = $params['id'];
|
||||
@@ -124,7 +125,7 @@ class Record extends Base
|
||||
public function actionList($params, $data, $request)
|
||||
{
|
||||
if (!$this->getAcl()->check($this->name, 'read')) {
|
||||
throw new Forbidden();
|
||||
throw new Forbidden("No read access for {$this->name}.");
|
||||
}
|
||||
|
||||
$params = [];
|
||||
@@ -156,7 +157,7 @@ class Record extends Base
|
||||
public function getActionListKanban($params, $data, $request)
|
||||
{
|
||||
if (!$this->getAcl()->check($this->name, 'read')) {
|
||||
throw new Forbidden();
|
||||
throw new Forbidden("No read access for {$this->name}.");
|
||||
}
|
||||
|
||||
$params = [];
|
||||
@@ -286,14 +287,14 @@ class Record extends Base
|
||||
}
|
||||
|
||||
if (!$this->getAcl()->check($this->name, 'edit')) {
|
||||
throw new Forbidden();
|
||||
throw new Forbidden("No edit access for {$this->name}.");
|
||||
}
|
||||
if (empty($data->attributes)) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
if ($this->getAcl()->get('massUpdatePermission') !== 'yes') {
|
||||
throw new Forbidden();
|
||||
throw new Forbidden("No massUpdatePermission.");
|
||||
}
|
||||
|
||||
$actionParams = $this->getMassActionParamsFromData($data);
|
||||
@@ -313,7 +314,7 @@ class Record extends Base
|
||||
|
||||
if (array_key_exists('where', $actionParams)) {
|
||||
if ($this->getAcl()->get('massUpdatePermission') !== 'yes') {
|
||||
throw new Forbidden();
|
||||
throw new Forbidden("No massUpdatePermission.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,7 +413,7 @@ class Record extends Base
|
||||
throw new BadRequest();
|
||||
}
|
||||
if (!$this->getAcl()->check($this->name, 'stream')) {
|
||||
throw new Forbidden();
|
||||
throw new Forbidden("No stream access for {$this->name}.");
|
||||
}
|
||||
$id = $params['id'];
|
||||
return $this->getRecordService()->follow($id);
|
||||
@@ -424,7 +425,7 @@ class Record extends Base
|
||||
throw new BadRequest();
|
||||
}
|
||||
if (!$this->getAcl()->check($this->name, 'read')) {
|
||||
throw new Forbidden();
|
||||
throw new Forbidden("No read access for {$this->name}.");
|
||||
}
|
||||
$id = $params['id'];
|
||||
return $this->getRecordService()->unfollow($id);
|
||||
@@ -444,7 +445,7 @@ class Record extends Base
|
||||
$attributes = $data->attributes;
|
||||
|
||||
if (!$this->getAcl()->check($this->name, 'edit')) {
|
||||
throw new Forbidden();
|
||||
throw new Forbidden("No edit access for {$this->name}.");
|
||||
}
|
||||
|
||||
return $this->getRecordService()->merge($targetId, $sourceIds, $attributes);
|
||||
@@ -480,7 +481,7 @@ class Record extends Base
|
||||
public function postActionMassUnfollow($params, $data, $request)
|
||||
{
|
||||
if (!$this->getAcl()->check($this->name, 'stream')) {
|
||||
throw new Forbidden();
|
||||
throw new Forbidden("No stream access for {$this->name}.");
|
||||
}
|
||||
|
||||
$actionParams = $this->getMassActionParamsFromData($data);
|
||||
|
||||
@@ -29,9 +29,11 @@
|
||||
|
||||
namespace Espo\Core\ExternalAccount;
|
||||
|
||||
use \Espo\Core\Exceptions\Error;
|
||||
use \Espo\Core\Exceptions\Forbidden;
|
||||
use \Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
|
||||
use Espo\Core\InjectableFactory;
|
||||
|
||||
class ClientManager
|
||||
{
|
||||
@@ -39,13 +41,17 @@ class ClientManager
|
||||
|
||||
protected $metadata;
|
||||
|
||||
protected $clientMap = array();
|
||||
protected $clientMap = [];
|
||||
|
||||
public function __construct($entityManager, $metadata, $config)
|
||||
protected $injectableFactory = null;
|
||||
|
||||
public function __construct(
|
||||
$entityManager, $metadata, $config, ?InjectableFactory $injectableFactory = null)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
$this->metadata = $metadata;
|
||||
$this->config = $config;
|
||||
$this->injectableFactory = $injectableFactory;
|
||||
}
|
||||
|
||||
protected function getMetadata()
|
||||
@@ -69,24 +75,68 @@ class ClientManager
|
||||
$externalAccountEntity = $this->clientMap[$hash]['externalAccountEntity'];
|
||||
$externalAccountEntity->set('accessToken', $data['accessToken']);
|
||||
$externalAccountEntity->set('tokenType', $data['tokenType']);
|
||||
$externalAccountEntity->set('expiresAt', $data['expiresAt'] ?? null);
|
||||
|
||||
if ($data['refreshToken'] ?? null) {
|
||||
$externalAccountEntity->set('refreshToken', $data['refreshToken']);
|
||||
}
|
||||
|
||||
$copy = $this->getEntityManager()->getEntity('ExternalAccount', $externalAccountEntity->id);
|
||||
if ($copy) {
|
||||
if (!$copy->get('enabled')) {
|
||||
throw new Error("External Account Client Manager: Account got disabled.");
|
||||
}
|
||||
|
||||
$copy->set('accessToken', $data['accessToken']);
|
||||
$copy->set('tokenType', $data['tokenType']);
|
||||
$this->getEntityManager()->saveEntity($copy, ['isTokenRenewal' => true]);
|
||||
$copy->set('expiresAt', $data['expiresAt'] ?? null);
|
||||
if ($data['refreshToken'] ?? null) {
|
||||
$copy->set('refreshToken', $data['refreshToken'] ?? null);
|
||||
}
|
||||
$this->getEntityManager()->saveEntity($copy, ['isTokenRenewal' => true, 'skipHooks' => true]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function create($integration, $userId)
|
||||
public function create(string $integration, string $userId)
|
||||
{
|
||||
$authMethod = $this->getMetadata()->get("integrations.{$integration}.authMethod");
|
||||
$methodName = 'create' . ucfirst($authMethod);
|
||||
return $this->$methodName($integration, $userId);
|
||||
|
||||
if (method_exists($this, $methodName)) {
|
||||
return $this->$methodName($integration, $userId);
|
||||
}
|
||||
|
||||
if (!$this->injectableFactory) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
$integrationEntity = $this->getEntityManager()->getEntity('Integration', $integration);
|
||||
$externalAccountEntity = $this->getEntityManager()->getEntity('ExternalAccount', $integration . '__' . $userId);
|
||||
|
||||
if (!$externalAccountEntity) {
|
||||
throw new Error("External Account {$integration} not found for {$userId}");
|
||||
}
|
||||
|
||||
if (!$integrationEntity->get('enabled')) return null;
|
||||
if (!$externalAccountEntity->get('enabled')) return null;
|
||||
|
||||
$className = $this->getMetadata()->get("integrations.{$integration}.clientClassName");
|
||||
$client = $this->injectableFactory->createByClassName($className);
|
||||
|
||||
$client->setup(
|
||||
$userId,
|
||||
$integrationEntity,
|
||||
$externalAccountEntity,
|
||||
$this
|
||||
);
|
||||
|
||||
$this->addToClientMap($client, $integrationEntity, $externalAccountEntity, $userId);
|
||||
|
||||
return $client;
|
||||
}
|
||||
|
||||
protected function createOAuth2($integration, $userId)
|
||||
protected function createOAuth2(string $integration, string $userId)
|
||||
{
|
||||
$integrationEntity = $this->getEntityManager()->getEntity('Integration', $integration);
|
||||
$externalAccountEntity = $this->getEntityManager()->getEntity('ExternalAccount', $integration . '__' . $userId);
|
||||
@@ -113,7 +163,7 @@ class ClientManager
|
||||
|
||||
$oauth2Client = new \Espo\Core\ExternalAccount\OAuth2\Client();
|
||||
|
||||
$client = new $className($oauth2Client, array(
|
||||
$params = [
|
||||
'endpoint' => $this->getMetadata()->get("integrations.{$integration}.params.endpoint"),
|
||||
'tokenEndpoint' => $this->getMetadata()->get("integrations.{$integration}.params.tokenEndpoint"),
|
||||
'clientId' => $integrationEntity->get('clientId'),
|
||||
@@ -122,7 +172,16 @@ class ClientManager
|
||||
'accessToken' => $externalAccountEntity->get('accessToken'),
|
||||
'refreshToken' => $externalAccountEntity->get('refreshToken'),
|
||||
'tokenType' => $externalAccountEntity->get('tokenType'),
|
||||
), $this);
|
||||
'expiresAt' => $externalAccountEntity->get('expiresAt'),
|
||||
];
|
||||
|
||||
foreach (get_object_vars($integrationEntity->getValueMap()) as $k => $v) {
|
||||
if (array_key_exists($k, $params)) continue;
|
||||
if ($integrationEntity->hasAttribute($k)) continue;
|
||||
$params[$k] = $v;
|
||||
}
|
||||
|
||||
$client = new $className($oauth2Client, $params, $this);
|
||||
|
||||
$this->addToClientMap($client, $integrationEntity, $externalAccountEntity, $userId);
|
||||
|
||||
@@ -139,5 +198,82 @@ class ClientManager
|
||||
'externalAccountEntity' => $externalAccountEntity,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getClientRecord($client) : \Espo\ORM\Entity
|
||||
{
|
||||
$data = $this->clientMap[spl_object_hash($client)];
|
||||
|
||||
if (!$data) {
|
||||
throw new Error("External Account Client Manager: Client not found in hash.");
|
||||
}
|
||||
|
||||
return $data['externalAccountEntity'];
|
||||
}
|
||||
|
||||
public function isClientLocked($client) : bool
|
||||
{
|
||||
$externalAccountEntity = $this->getClientRecord($client);
|
||||
$id = $externalAccountEntity->id;
|
||||
|
||||
$e = $this->getEntityManager()->getRepository('ExternalAccount')
|
||||
->select(['id', 'isLocked'])->where(['id' => $id])->findOne();
|
||||
|
||||
if (!$e) {
|
||||
throw new Error("External Account Client Manager: Client {$id} not found in DB.");
|
||||
}
|
||||
|
||||
return $e->get('isLocked');
|
||||
}
|
||||
|
||||
public function lockClient($client)
|
||||
{
|
||||
$externalAccountEntity = $this->getClientRecord($client);
|
||||
$id = $externalAccountEntity->id;
|
||||
|
||||
$e = $this->getEntityManager()->getRepository('ExternalAccount')
|
||||
->select(['id', 'isLocked'])->where(['id' => $id])->findOne();
|
||||
|
||||
if (!$e) {
|
||||
throw new Error("External Account Client Manager: Client {$id} not found in DB.");
|
||||
}
|
||||
|
||||
$e->set('isLocked', true);
|
||||
|
||||
$this->getEntityManager()->saveEntity($e, ['skipHooks' => true, 'silent' => true]);
|
||||
}
|
||||
|
||||
public function unlockClient($client)
|
||||
{
|
||||
$externalAccountEntity = $this->getClientRecord($client);
|
||||
$id = $externalAccountEntity->id;
|
||||
|
||||
$e = $this->getEntityManager()->getRepository('ExternalAccount')
|
||||
->select(['id', 'isLocked'])->where(['id' => $id])->findOne();
|
||||
|
||||
if (!$e) {
|
||||
throw new Error("External Account Client Manager: Client {$id} not found in DB.");
|
||||
}
|
||||
|
||||
$e->set('isLocked', false);
|
||||
|
||||
$this->getEntityManager()->saveEntity($e, ['skipHooks' => true, 'silent' => true]);
|
||||
}
|
||||
|
||||
public function reFetchClient($client)
|
||||
{
|
||||
$externalAccountEntity = $this->getClientRecord($client);
|
||||
$id = $externalAccountEntity->id;
|
||||
|
||||
$e = $this->getEntityManager()->getEntity('ExternalAccount', $id);
|
||||
|
||||
if (!$e) {
|
||||
throw new Error("External Account Client Manager: Client {$id} not found in DB.");
|
||||
}
|
||||
|
||||
$data = $e->getValueMap();
|
||||
|
||||
$externalAccountEntity->set($data);
|
||||
|
||||
$client->setParams(get_object_vars($data));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,9 +29,9 @@
|
||||
|
||||
namespace Espo\Core\ExternalAccount\Clients;
|
||||
|
||||
use \Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
|
||||
use \Espo\Core\ExternalAccount\OAuth2\Client;
|
||||
use Espo\Core\ExternalAccount\OAuth2\Client;
|
||||
|
||||
abstract class OAuth2Abstract implements IClient
|
||||
{
|
||||
@@ -39,7 +39,7 @@ abstract class OAuth2Abstract implements IClient
|
||||
|
||||
protected $manager = null;
|
||||
|
||||
protected $paramList = array(
|
||||
protected $paramList = [
|
||||
'endpoint',
|
||||
'tokenEndpoint',
|
||||
'clientId',
|
||||
@@ -48,7 +48,8 @@ abstract class OAuth2Abstract implements IClient
|
||||
'accessToken',
|
||||
'refreshToken',
|
||||
'redirectUri',
|
||||
);
|
||||
'expiresAt',
|
||||
];
|
||||
|
||||
protected $clientId = null;
|
||||
|
||||
@@ -60,7 +61,15 @@ abstract class OAuth2Abstract implements IClient
|
||||
|
||||
protected $redirectUri = null;
|
||||
|
||||
public function __construct($client, array $params = array(), $manager = null)
|
||||
protected $expiresAt = null;
|
||||
|
||||
const ACCESS_TOKEN_EXPIRATION_MARGIN = '20 seconds';
|
||||
|
||||
const LOCK_TIMEOUT = 5;
|
||||
|
||||
const LOCK_CHECK_STEP = 0.5;
|
||||
|
||||
public function __construct($client, array $params = [], $manager = null)
|
||||
{
|
||||
$this->client = $client;
|
||||
|
||||
@@ -90,7 +99,7 @@ abstract class OAuth2Abstract implements IClient
|
||||
public function setParams(array $params)
|
||||
{
|
||||
foreach ($this->paramList as $name) {
|
||||
if (!empty($params[$name])) {
|
||||
if (array_key_exists($name, $params)) {
|
||||
$this->setParam($name, $params[$name]);
|
||||
}
|
||||
}
|
||||
@@ -103,6 +112,28 @@ abstract class OAuth2Abstract implements IClient
|
||||
}
|
||||
}
|
||||
|
||||
protected function getAccessTokenDataFromResponseResult($result)
|
||||
{
|
||||
$data = [];
|
||||
|
||||
$data['accessToken'] = $result['access_token'];
|
||||
$data['tokenType'] = $result['token_type'];
|
||||
|
||||
$data['expiresAt'] = null;
|
||||
|
||||
if (isset($result['refresh_token']) && $result['refresh_token'] !== $this->refreshToken) {
|
||||
$data['refreshToken'] = $result['refresh_token'];
|
||||
}
|
||||
|
||||
if (isset($result['expires_in']) && is_numeric($result['expires_in'])) {
|
||||
$data['expiresAt'] = (new \DateTime())
|
||||
->modify('+' . $result['expires_in'] . ' seconds')
|
||||
->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function getAccessTokenFromAuthorizationCode($code)
|
||||
{
|
||||
$r = $this->client->getAccessToken($this->getParam('tokenEndpoint'), Client::GRANT_TYPE_AUTHORIZATION_CODE, [
|
||||
@@ -111,15 +142,16 @@ abstract class OAuth2Abstract implements IClient
|
||||
]);
|
||||
|
||||
if ($r['code'] == 200) {
|
||||
$data = [];
|
||||
if (!empty($r['result'])) {
|
||||
$data['accessToken'] = $r['result']['access_token'];
|
||||
$data['tokenType'] = $r['result']['token_type'];
|
||||
$data = $this->getAccessTokenDataFromResponseResult($r['result']);
|
||||
|
||||
$data['refreshToken'] = $r['result']['refresh_token'];
|
||||
|
||||
return $data;
|
||||
} else {
|
||||
$GLOBALS['log']->debug("OAuth getAccessTokenFromAuthorizationCode; Response: " . json_encode($r));
|
||||
return null;
|
||||
}
|
||||
return $data;
|
||||
} else {
|
||||
$GLOBALS['log']->debug("OAuth getAccessTokenFromAuthorizationCode; Response: " . json_encode($r));
|
||||
}
|
||||
@@ -144,9 +176,69 @@ abstract class OAuth2Abstract implements IClient
|
||||
}
|
||||
}
|
||||
|
||||
public function handleAccessTokenActuality()
|
||||
{
|
||||
if ($this->getParam('expiresAt')) {
|
||||
try {
|
||||
$dt = new \DateTime($this->getParam('expiresAt'));
|
||||
$dt->modify('-' . $this::ACCESS_TOKEN_EXPIRATION_MARGIN);
|
||||
} catch (\Exception $e) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($dt->format('U') <= (new \DateTime())->format('U')) {
|
||||
$GLOBALS['log']->debug("Oauth: Refreshing expired token for client {$this->clientId}.");
|
||||
|
||||
$until = microtime(true) + $this::LOCK_TIMEOUT;
|
||||
|
||||
if ($this->isLocked()) {
|
||||
while (true) {
|
||||
usleep($this::LOCK_CHECK_STEP * 1000000);
|
||||
|
||||
if (!$this->isLocked()) {
|
||||
$GLOBALS['log']->debug("Oauth: Waited until unlocked for client {$this->clientId}.");
|
||||
$this->reFetch();
|
||||
return;
|
||||
}
|
||||
|
||||
if (microtime(true) > $until) {
|
||||
$GLOBALS['log']->debug("Oauth: Waited until unlocked but timed out for client {$this->clientId}.");
|
||||
$this->unlock();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->refreshToken();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function isLocked()
|
||||
{
|
||||
return $this->manager->isClientLocked($this);
|
||||
}
|
||||
|
||||
protected function lock()
|
||||
{
|
||||
$this->manager->lockClient($this);
|
||||
}
|
||||
|
||||
protected function unlock()
|
||||
{
|
||||
$this->manager->unlockClient($this);
|
||||
}
|
||||
|
||||
protected function reFetch()
|
||||
{
|
||||
$this->manager->reFetchClient($this);
|
||||
}
|
||||
|
||||
public function request($url, $params = null, $httpMethod = Client::HTTP_METHOD_GET, $contentType = null, $allowRenew = true)
|
||||
{
|
||||
$httpHeaders = array();
|
||||
$this->handleAccessTokenActuality();
|
||||
|
||||
$httpHeaders = [];
|
||||
if (!empty($contentType)) {
|
||||
$httpHeaders['Content-Type'] = $contentType;
|
||||
switch ($contentType) {
|
||||
@@ -192,24 +284,43 @@ abstract class OAuth2Abstract implements IClient
|
||||
|
||||
protected function refreshToken()
|
||||
{
|
||||
if (!empty($this->refreshToken)) {
|
||||
$r = $this->client->getAccessToken($this->getParam('tokenEndpoint'), Client::GRANT_TYPE_REFRESH_TOKEN, array(
|
||||
'refresh_token' => $this->refreshToken,
|
||||
));
|
||||
if ($r['code'] == 200) {
|
||||
if (is_array($r['result'])) {
|
||||
if (!empty($r['result']['access_token'])) {
|
||||
$data = array();
|
||||
$data['accessToken'] = $r['result']['access_token'];
|
||||
$data['tokenType'] = $r['result']['token_type'];
|
||||
if (empty($this->refreshToken)) {
|
||||
throw new Error(
|
||||
"Oauth: Could not refresh token for client {$this->clientId}, because refreshToken is empty."
|
||||
);
|
||||
}
|
||||
|
||||
$this->setParams($data);
|
||||
$this->afterTokenRefreshed($data);
|
||||
return true;
|
||||
}
|
||||
$this->lock();
|
||||
|
||||
try {
|
||||
$r = $this->client->getAccessToken($this->getParam('tokenEndpoint'), Client::GRANT_TYPE_REFRESH_TOKEN, [
|
||||
'refresh_token' => $this->refreshToken,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$this->unlock();
|
||||
throw new Error("Oauth: Error while refreshing token: " . $e->getMessage());
|
||||
}
|
||||
|
||||
if ($r['code'] == 200) {
|
||||
if (is_array($r['result'])) {
|
||||
if (!empty($r['result']['access_token'])) {
|
||||
$data = $this->getAccessTokenDataFromResponseResult($r['result']);
|
||||
|
||||
$this->setParams($data);
|
||||
$this->afterTokenRefreshed($data);
|
||||
|
||||
$this->unlock();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->unlock();
|
||||
|
||||
$GLOBALS['log']->error("Oauth: Refreshing token failed for client {$this->clientId}: " . json_encode($r));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function handleErrorResponse($r)
|
||||
@@ -217,21 +328,20 @@ abstract class OAuth2Abstract implements IClient
|
||||
if ($r['code'] == 401 && !empty($r['result'])) {
|
||||
$result = $r['result'];
|
||||
if (strpos($r['header'], 'error=invalid_token') !== false) {
|
||||
return array(
|
||||
return [
|
||||
'action' => 'refreshToken'
|
||||
);
|
||||
];
|
||||
} else {
|
||||
return array(
|
||||
return [
|
||||
'action' => 'renew'
|
||||
);
|
||||
];
|
||||
}
|
||||
} else if ($r['code'] == 400 && !empty($r['result'])) {
|
||||
if ($r['result']['error'] == 'invalid_token') {
|
||||
return array(
|
||||
return [
|
||||
'action' => 'refreshToken'
|
||||
);
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,8 @@ class Client
|
||||
|
||||
protected $accessToken = null;
|
||||
|
||||
protected $expiresAt = null;
|
||||
|
||||
protected $authType = self::AUTH_TYPE_URI;
|
||||
|
||||
protected $tokenType = self::TOKEN_TYPE_URI;
|
||||
@@ -72,9 +74,9 @@ class Client
|
||||
|
||||
protected $certificateFile = null;
|
||||
|
||||
protected $curlOptions = array();
|
||||
protected $curlOptions = [];
|
||||
|
||||
public function __construct(array $params = array())
|
||||
public function __construct(array $params = [])
|
||||
{
|
||||
if (!extension_loaded('curl')) {
|
||||
throw new \Exception('CURL extension not found.');
|
||||
@@ -121,12 +123,17 @@ class Client
|
||||
$this->tokenType = $tokenType;
|
||||
}
|
||||
|
||||
public function setExpiresAt($value)
|
||||
{
|
||||
$this->expiresAt = $value;
|
||||
}
|
||||
|
||||
public function setAccessTokenSecret($accessTokenSecret)
|
||||
{
|
||||
$this->accessTokenSecret = $accessTokenSecret;
|
||||
}
|
||||
|
||||
public function request($url, $params = null, $httpMethod = self::HTTP_METHOD_GET, array $httpHeaders = array())
|
||||
public function request($url, $params = null, $httpMethod = self::HTTP_METHOD_GET, array $httpHeaders = [])
|
||||
{
|
||||
if ($this->accessToken) {
|
||||
switch ($this->tokenType) {
|
||||
@@ -148,7 +155,7 @@ class Client
|
||||
return $this->execute($url, $params, $httpMethod, $httpHeaders);
|
||||
}
|
||||
|
||||
private function execute($url, $params = null, $httpMethod, array $httpHeaders = array())
|
||||
private function execute($url, $params = null, $httpMethod, array $httpHeaders = [])
|
||||
{
|
||||
$curlOptions = array(
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
@@ -200,13 +207,11 @@ class Client
|
||||
|
||||
curl_setopt($ch, CURLOPT_HEADER, 1);
|
||||
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
|
||||
|
||||
if (!empty($this->certificateFile)) {
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
|
||||
curl_setopt($ch, CURLOPT_CAINFO, $this->certificateFile);
|
||||
} else {
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
|
||||
}
|
||||
|
||||
if (!empty($this->curlOptions)) {
|
||||
@@ -243,7 +248,7 @@ class Client
|
||||
{
|
||||
$params['grant_type'] = $grantType;
|
||||
|
||||
$httpHeaders = array();
|
||||
$httpHeaders = [];
|
||||
switch ($this->tokenType) {
|
||||
case self::AUTH_TYPE_URI:
|
||||
case self::AUTH_TYPE_FORM:
|
||||
@@ -261,4 +266,3 @@ class Client
|
||||
return $this->execute($url, $params, self::HTTP_METHOD_POST, $httpHeaders);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,9 +46,11 @@ class ArrayType extends BaseType
|
||||
|
||||
public function checkArray(\Espo\ORM\Entity $entity, string $field, $validationValue, $data) : bool
|
||||
{
|
||||
if (!$entity->has($field) || $entity->get($field) === null) return true;
|
||||
if (isset($data->$field) && $data->$field !== null && !is_array($data->$field)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return is_array($entity->get($field));
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function isNotEmpty(\Espo\ORM\Entity $entity, $field)
|
||||
|
||||
@@ -122,4 +122,9 @@ abstract class Base implements Injectable
|
||||
|
||||
return $eArgs;
|
||||
}
|
||||
}
|
||||
|
||||
protected function fetchRawArguments(\StdClass $item)
|
||||
{
|
||||
return $item->value ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,9 +62,9 @@ class DayOfWeekType extends \Espo\Core\Formula\Functions\Base
|
||||
if (empty($value)) return -1;
|
||||
|
||||
if (strlen($value) > 11) {
|
||||
$resultString = $this->getInjection('dateTime')->convertSystemDateTime($value, $timezone, 'e');
|
||||
$resultString = $this->getInjection('dateTime')->convertSystemDateTime($value, $timezone, 'd');
|
||||
} else {
|
||||
$resultString = $this->getInjection('dateTime')->convertSystemDate($value, 'e');
|
||||
$resultString = $this->getInjection('dateTime')->convertSystemDate($value, 'd');
|
||||
}
|
||||
|
||||
$result = intval($resultString);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2020 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Formula\Functions\NumberGroup;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
|
||||
class RandomIntType extends \Espo\Core\Formula\Functions\Base
|
||||
{
|
||||
public function process(\StdClass $item)
|
||||
{
|
||||
$args = $this->fetchArguments($item);
|
||||
|
||||
$min = $args[0] ?? 0;
|
||||
$max = $args[1] ?? PHP_INT_MAX;
|
||||
|
||||
if (!is_int($min) || !is_int($max) ) throw new Error("Non-integer arguments passed to function number\\randomInt.");
|
||||
|
||||
return random_int($min, $max);
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ class CountType extends \Espo\Core\Formula\Functions\Base
|
||||
while ($i < count($item->value) - 1) {
|
||||
$key = $this->evaluate($item->value[$i]);
|
||||
$value = $this->evaluate($item->value[$i + 1]);
|
||||
$whereClause[$key] = $value;
|
||||
$whereClause[] = [$key => $value];
|
||||
$i = $i + 2;
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ class ExistsType extends \Espo\Core\Formula\Functions\Base
|
||||
while ($i < count($item->value) - 1) {
|
||||
$key = $this->evaluate($item->value[$i]);
|
||||
$value = $this->evaluate($item->value[$i + 1]);
|
||||
$whereClause[$key] = $value;
|
||||
$whereClause[] = [$key => $value];
|
||||
$i = $i + 2;
|
||||
}
|
||||
|
||||
|
||||
48
application/Espo/Core/Formula/Functions/WhileType.php
Normal file
48
application/Espo/Core/Formula/Functions/WhileType.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2020 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Formula\Functions;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
|
||||
class WhileType extends Base
|
||||
{
|
||||
public function process(\StdClass $item)
|
||||
{
|
||||
$args = $this->fetchRawArguments($item);
|
||||
|
||||
if (count($args) < 2) {
|
||||
throw new Error("Function \'while\' should receieve 2 arguments.");
|
||||
}
|
||||
|
||||
while ($this->evaluate($args[0])) {
|
||||
$this->evaluate($args[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,6 +109,8 @@ class Parser
|
||||
{
|
||||
$isString = false;
|
||||
$isSingleQuote = false;
|
||||
$isComment = false;
|
||||
$isLineComment = false;
|
||||
|
||||
$modifiedString = $string;
|
||||
|
||||
@@ -116,27 +118,31 @@ class Parser
|
||||
|
||||
for ($i = 0; $i < strlen($string); $i++) {
|
||||
$isStringStart = false;
|
||||
if ($string[$i] === "'" && ($i === 0 || $string[$i - 1] !== "\\")) {
|
||||
if (!$isString) {
|
||||
$isString = true;
|
||||
$isSingleQuote = true;
|
||||
$isStringStart = true;
|
||||
} else {
|
||||
if ($isSingleQuote) {
|
||||
$isString = false;
|
||||
|
||||
if (!$isLineComment && !$isComment) {
|
||||
if ($string[$i] === "'" && ($i === 0 || $string[$i - 1] !== "\\")) {
|
||||
if (!$isString) {
|
||||
$isString = true;
|
||||
$isSingleQuote = true;
|
||||
$isStringStart = true;
|
||||
} else {
|
||||
if ($isSingleQuote) {
|
||||
$isString = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ($string[$i] === "\"" && ($i === 0 || $string[$i - 1] !== "\\")) {
|
||||
if (!$isString) {
|
||||
$isString = true;
|
||||
$isStringStart = true;
|
||||
$isSingleQuote = false;
|
||||
} else {
|
||||
if (!$isSingleQuote) {
|
||||
$isString = false;
|
||||
} else if ($string[$i] === "\"" && ($i === 0 || $string[$i - 1] !== "\\")) {
|
||||
if (!$isString) {
|
||||
$isString = true;
|
||||
$isStringStart = true;
|
||||
$isSingleQuote = false;
|
||||
} else {
|
||||
if (!$isSingleQuote) {
|
||||
$isString = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($isString) {
|
||||
if ($string[$i] === '(' || $string[$i] === ')') {
|
||||
$modifiedString[$i] = '_';
|
||||
@@ -144,24 +150,51 @@ class Parser
|
||||
$modifiedString[$i] = ' ';
|
||||
}
|
||||
} else {
|
||||
if ($string[$i] === '(') {
|
||||
$braceCounter++;
|
||||
}
|
||||
if ($string[$i] === ')') {
|
||||
$braceCounter--;
|
||||
}
|
||||
if (!$isLineComment && !$isComment) {
|
||||
|
||||
if ($braceCounter === 0) {
|
||||
if (!is_null($splitterIndexList)) {
|
||||
if ($string[$i] === ';') {
|
||||
$splitterIndexList[] = $i;
|
||||
if (!$isComment) {
|
||||
if ($i && $string[$i] === '/' && $string[$i - 1] === '/') {
|
||||
$isLineComment = true;
|
||||
}
|
||||
}
|
||||
if ($intoOneLine) {
|
||||
if ($string[$i] === "\r" || $string[$i] === "\n" || $string[$i] === "\t") {
|
||||
$string[$i] = ' ';
|
||||
|
||||
if (!$isLineComment) {
|
||||
if ($i && $string[$i] === '*' && $string[$i - 1] === '/') {
|
||||
$isComment = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($string[$i] === '(') {
|
||||
$braceCounter++;
|
||||
}
|
||||
if ($string[$i] === ')') {
|
||||
$braceCounter--;
|
||||
}
|
||||
|
||||
if ($braceCounter === 0) {
|
||||
if (!is_null($splitterIndexList)) {
|
||||
if ($string[$i] === ';') {
|
||||
$splitterIndexList[] = $i;
|
||||
}
|
||||
}
|
||||
if ($intoOneLine) {
|
||||
if ($string[$i] === "\r" || $string[$i] === "\n" || $string[$i] === "\t") {
|
||||
$string[$i] = ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($isLineComment) {
|
||||
if ($string[$i] === "\n") {
|
||||
$isLineComment = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($isComment) {
|
||||
if ($string[$i - 1] === "*" && $string[$i] === "/") {
|
||||
$isComment = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -367,9 +400,13 @@ class Parser
|
||||
}
|
||||
|
||||
if (is_numeric($expression)) {
|
||||
$value = filter_var($expression, FILTER_VALIDATE_INT) !== false ?
|
||||
(int) $expression :
|
||||
(float) $expression;
|
||||
|
||||
return (object) [
|
||||
'type' => 'value',
|
||||
'value' => ($expression == (int) $expression) ? (int) $expression : (float) $expression
|
||||
'value' => $value,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -451,6 +451,16 @@ class Htmlizer
|
||||
return $context['inverse'] ? $context['inverse']() : '';
|
||||
}
|
||||
},
|
||||
'ifMultipleOf' => function () {
|
||||
$args = func_get_args();
|
||||
$context = $args[count($args) - 1];
|
||||
|
||||
if ($args[0] % $args[1] === 0) {
|
||||
return $context['fn']();
|
||||
} else {
|
||||
return $context['inverse'] ? $context['inverse']() : '';
|
||||
}
|
||||
},
|
||||
'tableTag' => function () {
|
||||
$args = func_get_args();
|
||||
$context = $args[count($args) - 1];
|
||||
@@ -567,11 +577,13 @@ class Htmlizer
|
||||
$data[$k] = $value;
|
||||
}
|
||||
|
||||
$data['__config'] = $this->config;
|
||||
$data['__dateTime'] = $this->dateTime;
|
||||
$data['__metadata'] = $this->metadata;
|
||||
$data['__entityManager'] = $this->entityManager;
|
||||
$data['__language'] = $this->language;
|
||||
$data['__serviceFactory'] = $this->serviceFactory;
|
||||
$data['__entityType'] = $entity->getEntityType();
|
||||
|
||||
$html = $renderer($data);
|
||||
|
||||
|
||||
@@ -181,7 +181,9 @@ class MailMimeParser
|
||||
if ($bodyHtml) {
|
||||
$email->set('isHtml', true);
|
||||
$email->set('body', $bodyHtml);
|
||||
$email->set('bodyPlain', $bodyPlain);
|
||||
if ($bodyPlain) {
|
||||
$email->set('bodyPlain', $bodyPlain);
|
||||
}
|
||||
} else {
|
||||
$email->set('isHtml', false);
|
||||
$email->set('body', $bodyPlain);
|
||||
|
||||
@@ -62,6 +62,8 @@ class Sender
|
||||
|
||||
private $systemInboundEmailIsCached = false;
|
||||
|
||||
private $envelope = null;
|
||||
|
||||
public function __construct($config, $entityManager, $serviceFactory = null)
|
||||
{
|
||||
$this->config = $config;
|
||||
@@ -84,6 +86,7 @@ class Sender
|
||||
public function resetParams() : self
|
||||
{
|
||||
$this->params = [];
|
||||
$this->envelope = null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -132,7 +135,7 @@ class Sender
|
||||
if ($params['auth'] ?? false) {
|
||||
$authMechanism = $params['authMechanism'] ?? $params['smtpAuthMechanism'] ?? null;
|
||||
if ($authMechanism) {
|
||||
$authMechanism = preg_replace("([\.]{2,})", '', $params['authMechanism']);
|
||||
$authMechanism = preg_replace("([\.]{2,})", '', $authMechanism);
|
||||
if (in_array($authMechanism, ['login', 'crammd5', 'plain'])) {
|
||||
$options['connectionClass'] = $authMechanism;
|
||||
} else {
|
||||
@@ -163,6 +166,10 @@ class Sender
|
||||
|
||||
$smtpOptions = new SmtpOptions($options);
|
||||
$this->transport->setOptions($smtpOptions);
|
||||
|
||||
if ($this->envelope) {
|
||||
$this->transport->setEnvelope($this->envelope);
|
||||
}
|
||||
}
|
||||
|
||||
protected function applyGlobal()
|
||||
@@ -192,6 +199,14 @@ class Sender
|
||||
]);
|
||||
}
|
||||
|
||||
public function hasSystemSmtp()
|
||||
{
|
||||
if ($this->config->get('smtpServer')) return true;
|
||||
if ($this->getSystemInboundEmail()) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function getSystemInboundEmail()
|
||||
{
|
||||
$address = $this->config->get('outboundEmailFromAddress');
|
||||
@@ -457,10 +472,12 @@ class Sender
|
||||
$email->set('status', 'Sent');
|
||||
$email->set('dateSent', date("Y-m-d H:i:s"));
|
||||
} catch (\Exception $e) {
|
||||
$this->resetParams();
|
||||
$this->useGlobal();
|
||||
throw new Error($e->getMessage(), 500);
|
||||
}
|
||||
|
||||
$this->resetParams();
|
||||
$this->useGlobal();
|
||||
}
|
||||
|
||||
@@ -482,7 +499,8 @@ class Sender
|
||||
|
||||
public function setEnvelopeOptions(array $options) : self
|
||||
{
|
||||
$this->transport->setEnvelope(new Envelope($options));
|
||||
$this->envelope = new Envelope($options);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
301
application/Espo/Core/Password/Recovery.php
Normal file
301
application/Espo/Core/Password/Recovery.php
Normal file
@@ -0,0 +1,301 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2020 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Password;
|
||||
|
||||
use Espo\Core\Utils\Util;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Entities\PasswordChangeRequest;
|
||||
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
|
||||
class Recovery implements \Espo\Core\Interfaces\Injectable
|
||||
{
|
||||
use \Espo\Core\Traits\Injectable;
|
||||
|
||||
const REQUEST_DELAY = 3000; //ms
|
||||
|
||||
const REQUEST_LIFETIME = '3 hours';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->addDependencyList([
|
||||
'entityManager',
|
||||
'config',
|
||||
'mailSender',
|
||||
'htmlizerFactory',
|
||||
'templateFileManager',
|
||||
]);
|
||||
}
|
||||
|
||||
public function getRequest(string $id) : PasswordChangeRequest
|
||||
{
|
||||
$config = $this->getInjection('config');
|
||||
$em = $this->getInjection('entityManager');
|
||||
|
||||
if ($config->get('passwordRecoveryDisabled')) {
|
||||
throw new Forbidden("Password recovery: Disabled.");
|
||||
}
|
||||
|
||||
$request = $em->getRepository('PasswordChangeRequest')->where([
|
||||
'requestId' => $id,
|
||||
])->findOne();
|
||||
|
||||
if (!$request) {
|
||||
throw new NotFound("Password recovery: Request not found by id.");
|
||||
}
|
||||
|
||||
$userId = $request->get('userId');
|
||||
if (!$userId) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
public function removeRequest(string $id)
|
||||
{
|
||||
$em = $this->getInjection('entityManager');
|
||||
$request = $em->getRepository('PasswordChangeRequest')->where([
|
||||
'requestId' => $id,
|
||||
])->findOne();
|
||||
|
||||
if ($request) {
|
||||
$em->removeEntity($request);
|
||||
}
|
||||
}
|
||||
|
||||
public function request(string $emailAddress, ?string $userName = null, ?string $url) : bool
|
||||
{
|
||||
$config = $this->getInjection('config');
|
||||
$em = $this->getInjection('entityManager');
|
||||
|
||||
$noExposure = $config->get('passwordRecoveryNoExposure') ?? false;
|
||||
|
||||
if ($config->get('passwordRecoveryDisabled')) {
|
||||
throw new Forbidden("Password recovery: Disabled.");
|
||||
}
|
||||
|
||||
$user = $em->getRepository('User')->where([
|
||||
'userName' => $userName,
|
||||
'emailAddress' => $emailAddress,
|
||||
])->findOne();
|
||||
|
||||
if (!$user) {
|
||||
$this->fail("Password recovery: User {$emailAddress} not found.", 404);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$user->isActive()) {
|
||||
$this->fail("Password recovery: User {$user->id} is not active.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($user->isApi() || $user->isSystem() || $user->isSuperAdmin()) {
|
||||
$this->fail("Password recovery: User {$user->id} is not allowed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($config->get('passwordRecoveryForInternalUsersDisabled')) {
|
||||
if ($user->isRegular() || $user->isAdmin()) {
|
||||
$this->fail("Password recovery: User {$user->id} is not allowed, disabled for internal users.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($config->get('passwordRecoveryForAdminDisabled')) {
|
||||
if ($user->isAdmin()) {
|
||||
$this->fail("Password recovery: User {$user->id} is not allowed, disabled for admin users.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$user->isAdmin() && $config->get('authenticationMethod', 'Espo') !== 'Espo') {
|
||||
$this->fail("Password recovery: User {$user->id} is not allowed, authentication method is not 'Espo'.");
|
||||
return false;
|
||||
}
|
||||
|
||||
$passwordChangeRequest = $em->getRepository('PasswordChangeRequest')->where([
|
||||
'userId' => $user->id,
|
||||
])->findOne();
|
||||
|
||||
if ($passwordChangeRequest) {
|
||||
if (!$noExposure) {
|
||||
throw new Forbidden(json_encode(['reason' => 'Already-Sent']));
|
||||
}
|
||||
|
||||
$this->fail("Password recovery: Denied for {$user->id}, already sent.");
|
||||
return false;
|
||||
}
|
||||
|
||||
$requestId = Util::generateCryptId();
|
||||
|
||||
$passwordChangeRequest = $em->getEntity('PasswordChangeRequest');
|
||||
$passwordChangeRequest->set([
|
||||
'userId' => $user->id,
|
||||
'requestId' => $requestId,
|
||||
'url' => $url,
|
||||
]);
|
||||
|
||||
|
||||
$microtime = microtime(true);
|
||||
|
||||
$this->send($requestId, $emailAddress, $user);
|
||||
|
||||
$em->saveEntity($passwordChangeRequest);
|
||||
|
||||
if (!$passwordChangeRequest->id) throw new Error();
|
||||
|
||||
$lifetime = $config->get('passwordRecoveryRequestLifetime') ?? self::REQUEST_LIFETIME;
|
||||
|
||||
$dt = new \DateTime();
|
||||
$dt->modify('+' . $lifetime);
|
||||
|
||||
$em->createEntity('Job', [
|
||||
'serviceName' => 'User',
|
||||
'methodName' => 'removeChangePasswordRequestJob',
|
||||
'data' => ['id' => $passwordChangeRequest->id],
|
||||
'executeTime' => $dt->format('Y-m-d H:i:s'),
|
||||
'queue' => 'q1',
|
||||
]);
|
||||
|
||||
$timeDiff = $this->getDelay() - floor((microtime(true) - $microtime) / 1000);
|
||||
|
||||
if ($noExposure && $timeDiff > 0) {
|
||||
$this->delay($timeDiff);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function getDelay()
|
||||
{
|
||||
return $this->getInjection('config')->get('passwordRecoveryRequestDelay') ?? self::REQUEST_DELAY;
|
||||
}
|
||||
|
||||
protected function delay(?int $delay = null)
|
||||
{
|
||||
$delay = $delay ?? $this->getDelay();
|
||||
|
||||
usleep($delay * 1000);
|
||||
}
|
||||
|
||||
protected function send(string $requestId, string $emailAddress, User $user)
|
||||
{
|
||||
$config = $this->getInjection('config');
|
||||
$em = $this->getInjection('entityManager');
|
||||
$mailSender = $this->getInjection('mailSender');
|
||||
$htmlizerFactory = $this->getInjection('htmlizerFactory');
|
||||
|
||||
$templateFileManager = $this->getInjection('templateFileManager');
|
||||
|
||||
if (!$emailAddress) return;
|
||||
|
||||
$email = $em->getEntity('Email');
|
||||
|
||||
if (!$mailSender->hasSystemSmtp() && !$config->get('internalSmtpServer')) {
|
||||
throw new Error("Password recovery: SMTP credentials are not defined.");
|
||||
}
|
||||
|
||||
$subjectTpl = $templateFileManager->getTemplate('passwordChangeLink', 'subject', 'User');
|
||||
$bodyTpl = $templateFileManager->getTemplate('passwordChangeLink', 'body', 'User');
|
||||
|
||||
$siteUrl = $config->getSiteUrl();
|
||||
|
||||
if ($user->isPortal()) {
|
||||
$portal = $em->getRepository('Portal')->distinct()->join('users')->where([
|
||||
'isActive' => true,
|
||||
'users.id' => $user->id,
|
||||
])->findOne();
|
||||
if ($portal) {
|
||||
if ($portal->get('customUrl')) {
|
||||
$siteUrl = $portal->get('customUrl');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$data = [];
|
||||
$link = $siteUrl . '?entryPoint=changePassword&id=' . $requestId;
|
||||
$data['link'] = $link;
|
||||
|
||||
$htmlizer = $htmlizerFactory->create(true);
|
||||
|
||||
$subject = $htmlizer->render($user, $subjectTpl, null, $data, true);
|
||||
$body = $htmlizer->render($user, $bodyTpl, null, $data, true);
|
||||
|
||||
$email->set([
|
||||
'subject' => $subject,
|
||||
'body' => $body,
|
||||
'to' => $emailAddress,
|
||||
'isSystem' => true,
|
||||
]);
|
||||
|
||||
if ($mailSender->hasSystemSmtp()) {
|
||||
$mailSender->useGlobal();
|
||||
} else {
|
||||
$mailSender->useSmtp([
|
||||
'server' => $config->get('internalSmtpServer'),
|
||||
'port' => $config->get('internalSmtpPort'),
|
||||
'auth' => $config->get('internalSmtpAuth'),
|
||||
'username' => $config->get('internalSmtpUsername'),
|
||||
'password' => $config->get('internalSmtpPassword'),
|
||||
'security' => $config->get('internalSmtpSecurity'),
|
||||
'fromAddress' => $config->get('internalOutboundEmailFromAddress', $config->get('outboundEmailFromAddress')),
|
||||
]);
|
||||
}
|
||||
|
||||
$mailSender->send($email);
|
||||
}
|
||||
|
||||
private function fail(?string $msg = null, int $errorCode = 403)
|
||||
{
|
||||
$config = $this->getInjection('config');
|
||||
|
||||
$noExposure = $config->get('passwordRecoveryNoExposure') ?? false;
|
||||
|
||||
if ($msg) {
|
||||
$GLOBALS['log']->warning($msg);
|
||||
}
|
||||
|
||||
if (!$noExposure) {
|
||||
if ($errorCode === 403) {
|
||||
throw new Forbidden();
|
||||
} else if ($errorCode === 404) {
|
||||
throw new NotFound();
|
||||
} else {
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$this->delay();
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,11 @@
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
/************************************************************************
|
||||
* This file contains code parts copied from TCPDF software library
|
||||
* that is published under GNU Lesser General Public License.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Pdf;
|
||||
|
||||
define('K_TCPDF_EXTERNAL_CONFIG', true);
|
||||
|
||||
194
application/Espo/Core/TemplateHelpers/GoogleMaps.php
Normal file
194
application/Espo/Core/TemplateHelpers/GoogleMaps.php
Normal file
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2020 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\TemplateHelpers;
|
||||
|
||||
class GoogleMaps
|
||||
{
|
||||
public static function image()
|
||||
{
|
||||
$args = func_get_args();
|
||||
$context = $args[count($args) - 1];
|
||||
$hash = $context['hash'];
|
||||
$data = $context['data']['root'];
|
||||
|
||||
$em = $data['__entityManager'];
|
||||
$metadata = $data['__metadata'];
|
||||
$config = $data['__config'];
|
||||
|
||||
$entityType = $data['__entityType'];
|
||||
|
||||
$field = $hash['field'] ?? null;
|
||||
|
||||
$size = $hash['size'] ?? '400x400';
|
||||
$zoom = $hash['zoom'] ?? null;
|
||||
$language = $hash['language'] ?? $config->get('language');
|
||||
|
||||
if (strpos($size, 'x') === false) {
|
||||
$size = $size .'x' . $size;
|
||||
}
|
||||
|
||||
if ($field && $metadata->get(['entityDefs', $entityType, 'fields', $field, 'type']) !== 'address') {
|
||||
$GLOBALS['log']->warning("Template helper _googleMapsImage: Specified field is not of address type.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
!$field &&
|
||||
!array_key_exists('street', $hash) &&
|
||||
!array_key_exists('city', $hash) &&
|
||||
!array_key_exists('country', $hash) &&
|
||||
!array_key_exists('state', $hash) &&
|
||||
!array_key_exists('postalCode', $hash)
|
||||
) {
|
||||
$field = ($entityType === 'Account') ? 'billingAddress' : 'address';
|
||||
}
|
||||
|
||||
if ($field) {
|
||||
$street = $data[$field . 'Street'] ?? null;
|
||||
$city = $data[$field . 'City'] ?? null;
|
||||
$country = $data[$field . 'Country'] ?? null;
|
||||
$state = $data[$field . 'State'] ?? null;
|
||||
$postalCode = $data[$field . 'postalCode'] ?? null;
|
||||
} else {
|
||||
$street = $hash['street'] ?? null;
|
||||
$city = $hash['city'] ?? null;
|
||||
$country = $hash['country'] ?? null;
|
||||
$state = $hash['state'] ?? null;
|
||||
$postalCode = $hash['postalCode'] ?? null;
|
||||
}
|
||||
|
||||
$address = '';
|
||||
if ($street) {
|
||||
$address .= $street;
|
||||
}
|
||||
if ($city) {
|
||||
if ($address != '') {
|
||||
$address .= ', ';
|
||||
}
|
||||
$address .= $city;
|
||||
}
|
||||
if ($state) {
|
||||
if ($address != '') {
|
||||
$address .= ', ';
|
||||
}
|
||||
$address .= $state;
|
||||
}
|
||||
if ($postalCode) {
|
||||
if ($state || $city) {
|
||||
$address .= ' ';
|
||||
} else {
|
||||
if ($address) {
|
||||
$address .= ', ';
|
||||
}
|
||||
}
|
||||
$address .= $postalCode;
|
||||
}
|
||||
if ($country) {
|
||||
if ($address != '') {
|
||||
$address .= ', ';
|
||||
}
|
||||
$address .= $country;
|
||||
}
|
||||
|
||||
$address = urlencode($address);
|
||||
|
||||
$apiKey = $config->get('googleMapsApiKey');
|
||||
|
||||
if (!$apiKey) {
|
||||
$GLOBALS['log']->error("Template helper _googleMapsImage: No Google Maps API key.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$address) {
|
||||
$GLOBALS['log']->debug("Template helper _googleMapsImage: No address to display.");
|
||||
return null;
|
||||
}
|
||||
|
||||
$format = 'jpg;';
|
||||
|
||||
$url = "https://maps.googleapis.com/maps/api/staticmap?" .
|
||||
'center=' . $address .
|
||||
'format=' . $format .
|
||||
'&size=' . $size .
|
||||
'&key=' . $apiKey;
|
||||
|
||||
if ($zoom) {
|
||||
$url .= '&zoom=' . $zoom;
|
||||
}
|
||||
if ($language) {
|
||||
$url .= '&language=' . $language;
|
||||
}
|
||||
|
||||
$GLOBALS['log']->debug("Template helper _googleMapsImage: URL: {$url}.");
|
||||
|
||||
$image = \Espo\Core\TemplateHelpers\GoogleMaps::getImage($url);
|
||||
|
||||
if (!$image) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
$filePath = tempnam(sys_get_temp_dir(), 'google_maps_image');
|
||||
file_put_contents($filePath, $image);
|
||||
|
||||
list($width, $height) = explode('x', $size);
|
||||
|
||||
$tag = "<img src=\"{$filePath}\" width=\"{$width}\" height=\"{$height}\">";
|
||||
|
||||
return new LightnCandy\SafeString($tag);
|
||||
}
|
||||
|
||||
public static function getImage(string $url)
|
||||
{
|
||||
$headers = [];
|
||||
$headers[] = 'Accept: image/jpeg, image/pjpeg';
|
||||
$headers[] = 'Connection: Keep-Alive';
|
||||
|
||||
$agent = 'Mozilla/5.0';
|
||||
|
||||
$c = curl_init();
|
||||
|
||||
curl_setopt($c, \CURLOPT_URL, $url);
|
||||
curl_setopt($c, \CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($c, \CURLOPT_HEADER, 0);
|
||||
curl_setopt($c, \CURLOPT_USERAGENT, $agent);
|
||||
curl_setopt($c, \CURLOPT_TIMEOUT, 10);
|
||||
curl_setopt($c, \CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($c, \CURLOPT_FOLLOWLOCATION, 1);
|
||||
curl_setopt($c, \CURLOPT_BINARYTRANSFER, 1);
|
||||
|
||||
|
||||
$raw = curl_exec($c);
|
||||
curl_close($c);
|
||||
|
||||
return $raw;
|
||||
}
|
||||
}
|
||||
@@ -442,7 +442,7 @@ class Auth
|
||||
if ($authToken->get('secret')) {
|
||||
$sentSecret = $_COOKIE['auth-token-secret'] ?? null;
|
||||
if ($sentSecret === $authToken->get('secret')) {
|
||||
setcookie('auth-token-secret', null, -1, '/');
|
||||
$this->setSecretInCookie(null);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -487,15 +487,21 @@ class Auth
|
||||
$this->getEntityManager()->saveEntity($authLogRecord);
|
||||
}
|
||||
|
||||
protected function setSecretInCookie(string $secret)
|
||||
protected function setSecretInCookie(?string $secret)
|
||||
{
|
||||
if (!$secret) {
|
||||
$time = -1;
|
||||
} else {
|
||||
$time = strtotime('+1000 days');
|
||||
}
|
||||
|
||||
if (version_compare(\PHP_VERSION, '7.3.0') < 0) {
|
||||
setcookie('auth-token-secret', $secret, strtotime('+1000 days'), '/', '', false, true);
|
||||
setcookie('auth-token-secret', $secret, $time, '/', '', false, true);
|
||||
return;
|
||||
}
|
||||
|
||||
setcookie('auth-token-secret', $secret, [
|
||||
'expires' => strtotime('+1000 days'),
|
||||
'expires' => $time,
|
||||
'path' => '/',
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
|
||||
@@ -67,7 +67,7 @@ class Sender
|
||||
$handler = curl_init($webhook->get('url'));
|
||||
curl_setopt($handler, \CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($handler, \CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($handler, \CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($handler, \CURLOPT_SSL_VERIFYPEER, true);
|
||||
curl_setopt($handler, \CURLOPT_HEADER, true);
|
||||
curl_setopt($handler, \CURLOPT_CUSTOMREQUEST, 'POST');
|
||||
curl_setopt($handler, \CURLOPT_CONNECTTIMEOUT, $connectTimeout);
|
||||
|
||||
@@ -156,6 +156,7 @@ return [
|
||||
'ldapUserObjectClass',
|
||||
'maxEmailAccountCount',
|
||||
'massEmailMaxPerHourCount',
|
||||
'massEmailSiteUrl',
|
||||
'personalEmailMaxPortionSize',
|
||||
'inboundEmailMaxPortionSize',
|
||||
'authTokenLifetime',
|
||||
@@ -181,7 +182,10 @@ return [
|
||||
'authTokenPreventConcurrent',
|
||||
'emailParser',
|
||||
'passwordRecoveryDisabled',
|
||||
'passwordRecoveryNoExposure',
|
||||
'passwordRecoveryForAdminDisabled',
|
||||
'passwordRecoveryForInternalUsersDisabled',
|
||||
'passwordRecoveryRequestDelay',
|
||||
'latestVersion',
|
||||
],
|
||||
'superAdminItems' => [
|
||||
|
||||
@@ -140,21 +140,21 @@ class Email extends \Espo\Core\ORM\Entity
|
||||
|
||||
$body = $this->get('body');
|
||||
|
||||
$breaks = array("<br />","<br>","<br/>","<br />","<br />","<br/>","<br>");
|
||||
$breaks = ["<br />","<br>","<br/>","<br />","<br />","<br/>","<br>"];
|
||||
$body = str_ireplace($breaks, "\r\n", $body);
|
||||
$body = strip_tags($body);
|
||||
|
||||
$reList = [
|
||||
'/&(quot|#34);/i',
|
||||
'/&(amp|#38);/i',
|
||||
'/&(lt|#60);/i',
|
||||
'/&(gt|#62);/i',
|
||||
'/&(nbsp|#160);/i',
|
||||
'/&(iexcl|#161);/i',
|
||||
'/&(cent|#162);/i',
|
||||
'/&(pound|#163);/i',
|
||||
'/&(copy|#169);/i',
|
||||
'/&(reg|#174);/i'
|
||||
'&(quot|#34);',
|
||||
'&(amp|#38);',
|
||||
'&(lt|#60);',
|
||||
'&(gt|#62);',
|
||||
'&(nbsp|#160);',
|
||||
'&(iexcl|#161);',
|
||||
'&(cent|#162);',
|
||||
'&(pound|#163);',
|
||||
'&(copy|#169);',
|
||||
'&(reg|#174);',
|
||||
];
|
||||
$replaceList = [
|
||||
'',
|
||||
@@ -166,10 +166,12 @@ class Email extends \Espo\Core\ORM\Entity
|
||||
chr(162),
|
||||
chr(163),
|
||||
chr(169),
|
||||
chr(174)
|
||||
chr(174),
|
||||
];
|
||||
|
||||
$body = preg_replace($reList, $replaceList, $body);
|
||||
foreach ($reList as $i => $re) {
|
||||
$body = mb_ereg_replace($re, $replaceList[$i], $body, 'i');
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ class ProcessJobQueueQ0 extends \Espo\Core\Jobs\Base
|
||||
{
|
||||
public function run()
|
||||
{
|
||||
$limit = $this->getConfig()->get('jobQ1MaxPortion', 200);
|
||||
$limit = $this->getConfig()->get('jobQ0MaxPortion', 200);
|
||||
|
||||
$cronManager = new \Espo\Core\CronManager($this->getContainer());
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"seconds": {
|
||||
"type": "enumInt",
|
||||
"options": [0, 60, 120, 300, 600, 900, 1800, 3600, 7200, 10800, 18000, 86400, 172800, 259200, 432000],
|
||||
"options": [0, 60, 120, 300, 600, 900, 1800, 3600, 7200, 10800, 18000, 86400, 172800, 259200, 432000, 604800],
|
||||
"default": 0
|
||||
},
|
||||
"entityType": {
|
||||
|
||||
@@ -325,14 +325,14 @@ class MassEmail extends \Espo\Services\Record
|
||||
|
||||
$body = $emailData['body'];
|
||||
|
||||
$optOutUrl = $this->getConfig()->get('siteUrl') . '?entryPoint=unsubscribe&id=' . $queueItem->id;
|
||||
$optOutUrl = $this->getSiteUrl() . '?entryPoint=unsubscribe&id=' . $queueItem->id;
|
||||
$optOutLink = '<a href="'.$optOutUrl.'">'.$this->getLanguage()->translate('Unsubscribe', 'labels', 'Campaign').'</a>';
|
||||
|
||||
$body = str_replace('{optOutUrl}', $optOutUrl, $body);
|
||||
$body = str_replace('{optOutLink}', $optOutLink, $body);
|
||||
|
||||
foreach ($trackingUrlList as $trackingUrl) {
|
||||
$url = $this->getConfig()->get('siteUrl') . '?entryPoint=campaignUrl&id=' . $trackingUrl->id . '&queueItemId=' . $queueItem->id;
|
||||
$url = $this->getSiteUrl() . '?entryPoint=campaignUrl&id=' . $trackingUrl->id . '&queueItemId=' . $queueItem->id;
|
||||
$body = str_replace($trackingUrl->get('urlToUse'), $url, $body);
|
||||
}
|
||||
|
||||
@@ -346,7 +346,7 @@ class MassEmail extends \Espo\Services\Record
|
||||
|
||||
$trackImageAlt = $this->getLanguage()->translate('Campaign', 'scopeNames');
|
||||
|
||||
$trackOpenedUrl = $this->getConfig()->get('siteUrl') . '?entryPoint=campaignTrackOpened&id=' . $queueItem->id;
|
||||
$trackOpenedUrl = $this->getSiteUrl() . '?entryPoint=campaignTrackOpened&id=' . $queueItem->id;
|
||||
$trackOpenedHtml = '<img alt="'.$trackImageAlt.'" width="1" height="1" border="0" src="'.$trackOpenedUrl.'">';
|
||||
|
||||
if ($massEmail->get('campaignId') && $this->getConfig()->get('massEmailOpenTracking')) {
|
||||
@@ -387,7 +387,7 @@ class MassEmail extends \Espo\Services\Record
|
||||
$message->getHeaders()->addHeaderLine('Precedence', 'bulk');
|
||||
|
||||
if (!$this->getConfig()->get('massEmailDisableMandatoryOptOutLink')) {
|
||||
$optOutUrl = $this->getConfig()->getSiteUrl() . '?entryPoint=unsubscribe&id=' . $queueItem->id;
|
||||
$optOutUrl = $this->getSiteUrl() . '?entryPoint=unsubscribe&id=' . $queueItem->id;
|
||||
$message->getHeaders()->addHeaderLine('List-Unsubscribe', '<' . $optOutUrl . '>');
|
||||
}
|
||||
|
||||
@@ -589,4 +589,8 @@ class MassEmail extends \Espo\Services\Record
|
||||
return $dataList;
|
||||
}
|
||||
|
||||
protected function getSiteUrl()
|
||||
{
|
||||
return $this->getConfig()->get('massEmailSiteUrl') ?? $this->getConfig()->get('siteUrl');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ class EntityManager
|
||||
|
||||
public function getMapper($name)
|
||||
{
|
||||
if ($name{0} == '\\') {
|
||||
if ($name[0] == '\\') {
|
||||
$className = $name;
|
||||
} else {
|
||||
$className = $this->getMapperClassName($name);
|
||||
|
||||
@@ -269,7 +269,7 @@
|
||||
"systemRequirements": "System Requirements for EspoCRM.",
|
||||
"apiUsers": "Separate users for integration purposes.",
|
||||
"webhooks": "Manage webhooks.",
|
||||
"emailAddresses": "All emailes addresses stored in the system.",
|
||||
"emailAddresses": "All email addresses stored in the system.",
|
||||
"phoneNumbers": "All phone numbers stored in the system.",
|
||||
"dashboardTemplates": "Deploy dashboards to users.",
|
||||
"layoutSets": "Collections of layouts that can be assigned to teams & portals.",
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
"removeDuplicates": "This will permanently remove all imported records that were recognized as duplicates.",
|
||||
"confirmRevert": "This will remove all imported records permanently. Are you sure?",
|
||||
"confirmRemoveDuplicates": "This will permanently remove all imported records that were recognized as duplicates. Are you sure?",
|
||||
"confirmRemoveImportLog" : "This will remove the import log. All imported records will be kept. You won't be able to revert import results. Are you sure you?",
|
||||
"confirmRemoveImportLog" : "This will remove the import log. All imported records will be kept. You won't be able to revert import results. Are you sure?",
|
||||
"removeImportLog": "This will remove the import log. All imported records will be kept. Use it if you are sure that import is fine."
|
||||
},
|
||||
"fields": {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"log": "Log"
|
||||
},
|
||||
"labels": {
|
||||
"As often as possible": "As often as possible",
|
||||
"Create ScheduledJob": "Create Scheduled Job"
|
||||
},
|
||||
"options": {
|
||||
|
||||
@@ -126,6 +126,8 @@
|
||||
"useWebSocket": "Use WebSocket",
|
||||
"passwordRecoveryDisabled": "Disable password recovery",
|
||||
"passwordRecoveryForAdminDisabled": "Disable password recovery for admin users",
|
||||
"passwordRecoveryForInternalUsersDisabled": "Disable password recovery for internal users",
|
||||
"passwordRecoveryNoExposure": "Prevent email address exposure on password recovery form",
|
||||
"passwordGenerateLength": "Length of generated passwords",
|
||||
"passwordStrengthLength": "Minimum password length",
|
||||
"passwordStrengthLetterCount": "Number of letters required in password",
|
||||
@@ -160,6 +162,8 @@
|
||||
}
|
||||
},
|
||||
"tooltips": {
|
||||
"passwordRecoveryForInternalUsersDisabled": "Only portal users will be able to recover password.",
|
||||
"passwordRecoveryNoExposure": "It won't be possible to determine whether a specific email address is registered in the system.",
|
||||
"emailAddressLookupEntityTypeList": "For email address autocomplete.",
|
||||
"emailNotificationsDelay": "A message can be edited within the specified timeframe before the notification is sent.",
|
||||
"outboundEmailFromAddress": "The system email address.",
|
||||
|
||||
@@ -91,6 +91,7 @@
|
||||
"portals": "Portals which this user has access to."
|
||||
},
|
||||
"messages": {
|
||||
"passwordRecoverySentIfMatched": "Assuming the entered data matched any user account.",
|
||||
"passwordStrengthLength": "Must be at least {length} characters long.",
|
||||
"passwordStrengthLetterCount": "Must contain at least {count} letter(s).",
|
||||
"passwordStrengthNumberCount": "Must contain at least {count} digit(s).",
|
||||
|
||||
@@ -43,10 +43,11 @@
|
||||
"label":"SMTP",
|
||||
"rows": [
|
||||
[{"name": "useSmtp"}, false],
|
||||
[{"name": "smtpHost"}, {"name": "smtpPort"}],
|
||||
[{"name": "smtpAuth"}, {"name": "smtpSecurity"}],
|
||||
[{"name": "smtpUsername"}, {"name": "smtpAuthMechanism"}],
|
||||
[{"name": "smtpPassword"}, false],
|
||||
[{"name": "smtpHost"}, {"name": "smtpSecurity"}],
|
||||
[{"name": "smtpPort"}, {"name": "smtpAuth"}],
|
||||
[false, {"name": "smtpAuthMechanism"}],
|
||||
[false, {"name": "smtpUsername"}],
|
||||
[false, {"name": "smtpPassword"}],
|
||||
[
|
||||
{"name": "smtpTestSend", "customLabel": null, "view": "views/email-account/fields/test-send"}, false
|
||||
]
|
||||
|
||||
@@ -72,11 +72,11 @@
|
||||
"rows": [
|
||||
[{"name": "useSmtp"}, false],
|
||||
[{"name": "smtpIsShared"}, {"name": "smtpIsForMassEmail"}],
|
||||
[{"name": "smtpHost"}, {"name": "smtpPort"}],
|
||||
[{"name": "smtpAuth"}, {"name": "smtpSecurity"}],
|
||||
[{"name": "smtpUsername"}, {"name": "smtpAuthMechanism"}],
|
||||
[{"name": "smtpPassword"}, false],
|
||||
[{"name": "fromName"}, false],
|
||||
[{"name": "smtpHost"}, {"name": "smtpSecurity"}],
|
||||
[{"name": "smtpPort"}, {"name": "smtpAuth"}],
|
||||
[{"name": "fromName"}, {"name": "smtpAuthMechanism"}],
|
||||
[false, {"name": "smtpUsername"}],
|
||||
[false, {"name": "smtpPassword"}],
|
||||
[
|
||||
{"name": "smtpTestSend", "customLabel": null, "view": "views/inbound-email/fields/test-send"}, false
|
||||
]
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
[{"name": "passwordGenerateLength"}, false],
|
||||
[{"name": "passwordStrengthLength"}, {"name": "passwordStrengthLetterCount"}],
|
||||
[{"name": "passwordStrengthBothCases"}, {"name": "passwordStrengthNumberCount"}],
|
||||
[{"name": "passwordRecoveryDisabled"}, {"name": "passwordRecoveryForAdminDisabled"}]
|
||||
[{"name": "passwordRecoveryDisabled"}, {"name": "passwordRecoveryForAdminDisabled"}],
|
||||
[{"name": "passwordRecoveryNoExposure"}, {"name": "passwordRecoveryForInternalUsersDisabled"}]
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"googleMapsImage": "\\Espo\\Core\\TemplateHelpers\\GoogleMaps::image"
|
||||
}
|
||||
@@ -4,6 +4,9 @@
|
||||
"log": {
|
||||
"readOnly": true,
|
||||
"view": "views/scheduled-job/record/panels/log",
|
||||
"createDisabled": true,
|
||||
"selectDisabled": true,
|
||||
"viewDisabled": true,
|
||||
"unlinkDisabled": true
|
||||
}
|
||||
},
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
},
|
||||
"enabled": {
|
||||
"type": "bool"
|
||||
},
|
||||
"isLocked": {
|
||||
"type": "bool"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,6 +175,14 @@
|
||||
"passwordRecoveryForAdminDisabled": {
|
||||
"type": "bool"
|
||||
},
|
||||
"passwordRecoveryForInternalUsersDisabled": {
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
},
|
||||
"passwordRecoveryNoExposure": {
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
},
|
||||
"passwordGenerateLength": {
|
||||
"type": "int",
|
||||
"min": 6,
|
||||
|
||||
@@ -75,7 +75,7 @@ class Email extends \Espo\Core\SelectManagers\Base
|
||||
$skipIndex = true;
|
||||
}
|
||||
|
||||
$actualDatabaseType = $this->getConfig()->get('actualDatabaseType');
|
||||
/*$actualDatabaseType = strtolower($this->getConfig()->get('actualDatabaseType'));
|
||||
$actualDatabaseVersion = $this->getConfig()->get('actualDatabaseVersion');
|
||||
|
||||
if (
|
||||
@@ -84,7 +84,12 @@ class Email extends \Espo\Core\SelectManagers\Base
|
||||
$this->hasLinkJoined('teams', $result)
|
||||
) {
|
||||
$skipIndex = true;
|
||||
}*/
|
||||
|
||||
if ($this->hasLinkJoined('teams', $result)) {
|
||||
$skipIndex = true;
|
||||
}
|
||||
|
||||
if (!$skipIndex) {
|
||||
$result['useIndex'] = 'dateSent';
|
||||
}
|
||||
|
||||
@@ -297,7 +297,7 @@ class Attachment extends Record
|
||||
$opts[\CURLOPT_HEADER] = true;
|
||||
$opts[\CURLOPT_BINARYTRANSFER] = true;
|
||||
$opts[\CURLOPT_VERBOSE] = true;
|
||||
$opts[\CURLOPT_SSL_VERIFYPEER] = false;
|
||||
$opts[\CURLOPT_SSL_VERIFYPEER] = true;
|
||||
$opts[\CURLOPT_SSL_VERIFYHOST] = 2;
|
||||
$opts[\CURLOPT_RETURNTRANSFER] = true;
|
||||
$opts[\CURLOPT_FOLLOWLOCATION] = true;
|
||||
|
||||
@@ -92,6 +92,7 @@ class EmailAccount extends Record
|
||||
$entity = $this->getEntityManager()->getEntity('EmailAccount', $params['id']);
|
||||
if ($entity) {
|
||||
$params['password'] = $this->getCrypt()->decrypt($entity->get('password'));
|
||||
$params['imapHandler'] = $entity->get('imapHandler');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +109,13 @@ class EmailAccount extends Record
|
||||
|
||||
public function testConnection(array $params)
|
||||
{
|
||||
if (!empty($params['id'])) {
|
||||
$account = $this->getEntityManager()->getEntity('EmailAccount', $params['id']);
|
||||
if ($account) {
|
||||
$params['imapHandler'] = $account->get('imapHandler');
|
||||
}
|
||||
}
|
||||
|
||||
$storage = $this->createStorage($params);
|
||||
|
||||
$userId = $params['userId'] ?? null;
|
||||
@@ -132,7 +140,22 @@ class EmailAccount extends Record
|
||||
|
||||
$imapParams = null;
|
||||
|
||||
if ($emailAddress && $userId) {
|
||||
$handlerClassName = $params['imapHandler'] ?? null;
|
||||
|
||||
if ($handlerClassName && !empty($params['id'])) {
|
||||
try {
|
||||
$handler = $this->getInjection('injectableFactory')->createByClassName($handlerClassName);
|
||||
} catch (\Throwable $e) {
|
||||
$GLOBALS['log']->error(
|
||||
"EmailAccount: Could not create Imap Handler. Error: " . $e->getMessage()
|
||||
);
|
||||
}
|
||||
if (method_exists($handler, 'prepareProtocol')) {
|
||||
$imapParams = $handler->prepareProtocol($params['id'], $params);
|
||||
}
|
||||
}
|
||||
|
||||
if ($emailAddress && $userId && !$handlerClassName) {
|
||||
$emailAddress = strtolower($emailAddress);
|
||||
$userData = $this->getEntityManager()->getRepository('UserData')->getByUserId($userId);
|
||||
if ($userData) {
|
||||
@@ -218,6 +241,9 @@ class EmailAccount extends Record
|
||||
$params['ssl'] = true;
|
||||
}
|
||||
|
||||
$params['imapHandler'] = $emailAccount->get('imapHandler');
|
||||
$params['id'] = $emailAccount->id;
|
||||
|
||||
$storage = $this->createStorage($params);
|
||||
|
||||
return $storage;
|
||||
|
||||
@@ -29,11 +29,11 @@
|
||||
|
||||
namespace Espo\Services;
|
||||
|
||||
use \Espo\ORM\Entity;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use \Espo\Core\Exceptions\Error;
|
||||
use \Espo\Core\Exceptions\Forbidden;
|
||||
use \Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
|
||||
class ExternalAccount extends Record
|
||||
{
|
||||
@@ -56,7 +56,9 @@ class ExternalAccount extends Record
|
||||
throw new Error("{$integration} is disabled.");
|
||||
}
|
||||
|
||||
$factory = new \Espo\Core\ExternalAccount\ClientManager($this->getEntityManager(), $this->getMetadata(), $this->getConfig());
|
||||
$factory = new \Espo\Core\ExternalAccount\ClientManager(
|
||||
$this->getEntityManager(), $this->getMetadata(), $this->getConfig(), $this->getInjection('injectableFactory')
|
||||
);
|
||||
return $factory->create($integration, $id);
|
||||
}
|
||||
|
||||
@@ -92,6 +94,7 @@ class ExternalAccount extends Record
|
||||
$entity->clear('accessToken');
|
||||
$entity->clear('refreshToken');
|
||||
$entity->clear('tokenType');
|
||||
$entity->clear('expiresAt');
|
||||
foreach ($result as $name => $value) {
|
||||
$entity->set($name, $value);
|
||||
}
|
||||
@@ -109,4 +112,29 @@ class ExternalAccount extends Record
|
||||
throw new Error("Could not load client for {$integration}.");
|
||||
}
|
||||
}
|
||||
|
||||
public function read($id)
|
||||
{
|
||||
list($integration, $userId) = explode('__', $id);
|
||||
|
||||
if ($this->getUser()->id != $userId && !$this->getUser()->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$entity = $this->getEntityManager()->getEntity('ExternalAccount', $id);
|
||||
|
||||
if (!$entity) throw new NotFoundSilent("Record does not exist.");
|
||||
|
||||
list($integration, $id) = explode('__', $entity->id);
|
||||
|
||||
$externalAccountSecretAttributeList = $this->getMetadata()->get(
|
||||
['integrations', $integration, 'externalAccountSecretAttributeList']) ?? [];
|
||||
|
||||
foreach ($externalAccountSecretAttributeList as $a) {
|
||||
$entity->clear($a);
|
||||
}
|
||||
|
||||
return $entity;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
|
||||
namespace Espo\Services;
|
||||
|
||||
use \Espo\Core\Exceptions\Forbidden;
|
||||
use \Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
@@ -88,15 +88,6 @@ class GlobalSearch extends \Espo\Core\Services\Base
|
||||
|
||||
$fullTextSearchData = $selectManager->getFullTextSearchDataForTextFilter($query);
|
||||
|
||||
if ($fullTextSearchData) {
|
||||
$hasFullTextSearch = true;
|
||||
$selectParams['select'][] = [$fullTextSearchData['where'], '_relevance'];
|
||||
$selectParams['orderBy'] = [[$fullTextSearchData['where'], 'desc'], ['name']];
|
||||
} else {
|
||||
$selectParams['select'][] = ['VALUE:1.1', '_relevance'];
|
||||
$selectParams['orderBy'] = [['name']];
|
||||
}
|
||||
|
||||
if ($this->getMetadata()->get(['entityDefs', $entityType, 'fields', 'name', 'type']) === 'personName') {
|
||||
$selectParams['select'][] = 'firstName';
|
||||
$selectParams['select'][] = 'lastName';
|
||||
@@ -114,6 +105,15 @@ class GlobalSearch extends \Espo\Core\Services\Base
|
||||
|
||||
unset($selectParams['additionalSelect']);
|
||||
|
||||
if ($fullTextSearchData) {
|
||||
$hasFullTextSearch = true;
|
||||
$selectParams['select'][] = [$fullTextSearchData['where'], '_relevance'];
|
||||
$selectParams['orderBy'] = [[$fullTextSearchData['where'], 'desc'], ['name']];
|
||||
} else {
|
||||
$selectParams['select'][] = ['VALUE:1.1', '_relevance'];
|
||||
$selectParams['orderBy'] = [['name']];
|
||||
}
|
||||
|
||||
$itemSql = $this->getEntityManager()->getQuery()->createSelectQuery($entityType, $selectParams);
|
||||
|
||||
$unionPartList[] = "(\n" . $itemSql . "\n)";
|
||||
|
||||
@@ -299,7 +299,7 @@ class Import extends \Espo\Services\Record
|
||||
if ($entity) {
|
||||
$this->getEntityManager()->removeEntity($entity);
|
||||
}
|
||||
$this->getEntityManager()->getRepository($entity->getEntityType())->deleteFromDb($entityId);
|
||||
$this->getEntityManager()->getRepository($entityType)->deleteFromDb($entityId);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -164,7 +164,7 @@ class Layout extends \Espo\Core\Services\Base
|
||||
{
|
||||
$em = $this->getInjection('entityManager');
|
||||
$layoutSet = $em->getEntity('LayoutSet', $setId);
|
||||
if (!$layoutSet) throw new NotFound();
|
||||
if (!$layoutSet) throw new NotFound("LayoutSet {$setId} not found.");
|
||||
|
||||
$layoutList = $layoutSet->get('layoutList') ?? [];
|
||||
|
||||
|
||||
@@ -102,11 +102,14 @@ class Metadata extends \Espo\Core\Services\Base
|
||||
}
|
||||
|
||||
$foreignEntityType = $defs['entity'] ?? null;
|
||||
if ($this->getAcl()->check($foreignEntityType)) continue;
|
||||
|
||||
if ($this->getUser()->isPortal()) {
|
||||
if ($foreignEntityType === 'Account' || $foreignEntityType === 'Contact') {
|
||||
continue;
|
||||
if ($foreignEntityType) {
|
||||
if ($this->getAcl()->check($foreignEntityType)) continue;
|
||||
|
||||
if ($this->getUser()->isPortal()) {
|
||||
if ($foreignEntityType === 'Account' || $foreignEntityType === 'Contact') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,25 @@
|
||||
|
||||
namespace Espo\Services;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
|
||||
class ScheduledJob extends Record
|
||||
{
|
||||
protected $findLinkedLogCountQueryDisabled = true;
|
||||
|
||||
public function processValidation(Entity $entity, $data)
|
||||
{
|
||||
parent::processValidation($entity, $data);
|
||||
|
||||
$scheduling = $entity->get('scheduling');
|
||||
|
||||
try {
|
||||
$cronExpression = \Cron\CronExpression::factory($scheduling);
|
||||
$nextDate = $cronExpression->getNextRunDate()->format('Y-m-d H:i:s');
|
||||
} catch (\Exception $e) {
|
||||
throw new BadRequest("Not valid scheduling expression.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
namespace Espo\Services;
|
||||
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
@@ -146,7 +146,7 @@ class Settings extends \Espo\Core\Services\Base
|
||||
}
|
||||
|
||||
if (
|
||||
($this->getConfig()->get('smtpServer') || $this->getConfig()->get('internalSmtpServer'))
|
||||
($this->getConfig()->get('outboundEmailFromAddress') || $this->getConfig()->get('internalSmtpServer'))
|
||||
&&
|
||||
!$this->getConfig()->get('passwordRecoveryDisabled')
|
||||
) {
|
||||
|
||||
@@ -29,17 +29,15 @@
|
||||
|
||||
namespace Espo\Services;
|
||||
|
||||
use \Espo\Core\Exceptions\Forbidden;
|
||||
use \Espo\Core\Exceptions\Error;
|
||||
use \Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Utils\Util;
|
||||
|
||||
use \Espo\ORM\Entity;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class User extends Record
|
||||
{
|
||||
const PASSWORD_CHANGE_REQUEST_LIFETIME = 360; // minutes
|
||||
|
||||
protected function init()
|
||||
{
|
||||
parent::init();
|
||||
@@ -209,83 +207,30 @@ class User extends Record
|
||||
|
||||
public function passwordChangeRequest($userName, $emailAddress, $url = null)
|
||||
{
|
||||
if ($this->getConfig()->get('passwordRecoveryDisabled')) {
|
||||
throw new Forbidden("Password recovery disabled");
|
||||
}
|
||||
|
||||
$user = $this->getEntityManager()->getRepository('User')->where([
|
||||
'userName' => $userName,
|
||||
'emailAddress' => $emailAddress
|
||||
])->findOne();
|
||||
|
||||
if (empty($user)) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if (!$user->isActive()) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if ($user->isApi()) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if ($this->getConfig()->get('passwordRecoveryForAdminDisabled')) {
|
||||
if ($user->isAdmin()) {
|
||||
throw new NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
$userId = $user->id;
|
||||
|
||||
$passwordChangeRequest = $this->getEntityManager()->getRepository('PasswordChangeRequest')->where([
|
||||
'userId' => $userId
|
||||
])->findOne();
|
||||
if ($passwordChangeRequest) {
|
||||
throw new Forbidden(json_encode(['reason' => 'Already-Sent']));
|
||||
}
|
||||
|
||||
$requestId = Util::generateCryptId();
|
||||
|
||||
$passwordChangeRequest = $this->getEntityManager()->getEntity('PasswordChangeRequest');
|
||||
$passwordChangeRequest->set([
|
||||
'userId' => $userId,
|
||||
'requestId' => $requestId,
|
||||
'url' => $url
|
||||
]);
|
||||
|
||||
if (!$user->isAdmin() && $this->getConfig()->get('authenticationMethod', 'Espo') !== 'Espo') {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$this->sendChangePasswordLink($requestId, $emailAddress, $user);
|
||||
|
||||
$this->getEntityManager()->saveEntity($passwordChangeRequest);
|
||||
|
||||
if (!$passwordChangeRequest->id) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
$dt = new \DateTime();
|
||||
$dt->add(new \DateInterval('PT'. self::PASSWORD_CHANGE_REQUEST_LIFETIME . 'M'));
|
||||
|
||||
$job = $this->getEntityManager()->getEntity('Job');
|
||||
|
||||
$job->set([
|
||||
'serviceName' => 'User',
|
||||
'methodName' => 'removeChangePasswordRequestJob',
|
||||
'data' => [
|
||||
'id' => $passwordChangeRequest->id
|
||||
],
|
||||
'executeTime' => $dt->format('Y-m-d H:i:s'),
|
||||
'queue' => 'q1'
|
||||
]);
|
||||
|
||||
$this->getEntityManager()->saveEntity($job);
|
||||
|
||||
$recovery = $this->getContainer()->get('injectableFactory')->createByClassName('\\Espo\\Core\\Password\\Recovery');
|
||||
$recovery->request($emailAddress, $userName, $url);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function changePasswordByRequest(string $requestId, string $password)
|
||||
{
|
||||
$recovery = $this->getContainer()->get('injectableFactory')->createByClassName('\\Espo\\Core\\Password\\Recovery');
|
||||
|
||||
$request = $recovery->getRequest($requestId);
|
||||
|
||||
$userId = $request->get('userId');
|
||||
|
||||
if (!$userId) throw new Error();
|
||||
|
||||
$this->changePassword($userId, $password);
|
||||
|
||||
$recovery->removeRequest($requestId);
|
||||
|
||||
return (object) [
|
||||
'url' => $request->get('url'),
|
||||
];
|
||||
}
|
||||
|
||||
public function removeChangePasswordRequestJob($data)
|
||||
{
|
||||
if (empty($data->id)) {
|
||||
@@ -439,7 +384,7 @@ class User extends Record
|
||||
throw new Forbidden("Generate new password: Can't process because user desn't have email address.");
|
||||
}
|
||||
|
||||
if (!$this->getConfig()->get('smtpServer') && !$this->getConfig()->get('internalSmtpServer')) {
|
||||
if (!$this->getMailSender()->hasSystemSmtp() && !$this->getConfig()->get('internalSmtpServer')) {
|
||||
throw new Forbidden("Generate new password: Can't process because SMTP is not configured.");
|
||||
}
|
||||
|
||||
@@ -585,7 +530,7 @@ class User extends Record
|
||||
|
||||
$email = $this->getEntityManager()->getEntity('Email');
|
||||
|
||||
if (!$this->getConfig()->get('smtpServer') && !$this->getConfig()->get('internalSmtpServer')) {
|
||||
if (!$this->getMailSender()->hasSystemSmtp() && !$this->getConfig()->get('internalSmtpServer')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -643,7 +588,7 @@ class User extends Record
|
||||
'to' => $emailAddress
|
||||
]);
|
||||
|
||||
if ($this->getConfig()->get('smtpServer')) {
|
||||
if ($this->getMailSender()->hasSystemSmtp()) {
|
||||
$this->getMailSender()->useGlobal();
|
||||
} else {
|
||||
$this->getMailSender()->useSmtp(array(
|
||||
@@ -659,69 +604,7 @@ class User extends Record
|
||||
$this->getMailSender()->send($email);
|
||||
}
|
||||
|
||||
protected function sendChangePasswordLink($requestId, $emailAddress, Entity $user)
|
||||
{
|
||||
if (empty($emailAddress)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email = $this->getEntityManager()->getEntity('Email');
|
||||
|
||||
if (!$this->getConfig()->get('smtpServer') && !$this->getConfig()->get('internalSmtpServer')) {
|
||||
throw new Error("SMTP credentials are not defined.");
|
||||
}
|
||||
|
||||
$templateFileManager = $this->getContainer()->get('templateFileManager');
|
||||
|
||||
$subjectTpl = $templateFileManager->getTemplate('passwordChangeLink', 'subject', 'User');
|
||||
$bodyTpl = $templateFileManager->getTemplate('passwordChangeLink', 'body', 'User');
|
||||
|
||||
$siteUrl = $this->getConfig()->getSiteUrl();
|
||||
|
||||
if ($user->isPortal()) {
|
||||
$portal = $this->getEntityManager()->getRepository('Portal')->distinct()->join('users')->where([
|
||||
'isActive' => true,
|
||||
'users.id' => $user->id,
|
||||
])->findOne();
|
||||
if ($portal) {
|
||||
if ($portal->get('customUrl')) {
|
||||
$siteUrl = $portal->get('customUrl');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$data = [];
|
||||
$link = $siteUrl . '?entryPoint=changePassword&id=' . $requestId;
|
||||
$data['link'] = $link;
|
||||
|
||||
$htmlizer = new \Espo\Core\Htmlizer\Htmlizer($this->getFileManager(), $this->getDateTime(), $this->getNumber(), null);
|
||||
|
||||
$subject = $htmlizer->render($user, $subjectTpl, null, $data, true);
|
||||
$body = $htmlizer->render($user, $bodyTpl, null, $data, true);
|
||||
|
||||
$email->set([
|
||||
'subject' => $subject,
|
||||
'body' => $body,
|
||||
'to' => $emailAddress,
|
||||
'isSystem' => true
|
||||
]);
|
||||
|
||||
if ($this->getConfig()->get('smtpServer')) {
|
||||
$this->getMailSender()->useGlobal();
|
||||
} else {
|
||||
$this->getMailSender()->useSmtp([
|
||||
'server' => $this->getConfig()->get('internalSmtpServer'),
|
||||
'port' => $this->getConfig()->get('internalSmtpPort'),
|
||||
'auth' => $this->getConfig()->get('internalSmtpAuth'),
|
||||
'username' => $this->getConfig()->get('internalSmtpUsername'),
|
||||
'password' => $this->getConfig()->get('internalSmtpPassword'),
|
||||
'security' => $this->getConfig()->get('internalSmtpSecurity'),
|
||||
'fromAddress' => $this->getConfig()->get('internalOutboundEmailFromAddress', $this->getConfig()->get('outboundEmailFromAddress'))
|
||||
]);
|
||||
}
|
||||
|
||||
$this->getMailSender()->send($email);
|
||||
}
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
|
||||
1520
client/lib/bootstrap-datepicker.js
vendored
1520
client/lib/bootstrap-datepicker.js
vendored
File diff suppressed because it is too large
Load Diff
1
client/lib/cronstrue-i18n.min.js
vendored
Normal file
1
client/lib/cronstrue-i18n.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -39,9 +39,10 @@ Espo.define('crm:views/meeting/popup-notification', 'views/popup-notification',
|
||||
closeButton: true,
|
||||
|
||||
setup: function () {
|
||||
this.wait(true);
|
||||
|
||||
if (this.notificationData.entityType) {
|
||||
|
||||
this.wait(true);
|
||||
|
||||
this.getModelFactory().create(this.notificationData.entityType, function (model) {
|
||||
|
||||
var dateAttribute = 'dateStart';
|
||||
|
||||
@@ -3,20 +3,8 @@
|
||||
<% _.each(layout, function (panel, columnNumber) { %>
|
||||
<% hasHiddenPanel = panel.hidden || hasHiddenPanel; %>
|
||||
<div class="panel panel-<%= panel.style %><% if (panel.name) { %>{{#if hiddenPanels.<%= panel.name %>}} hidden{{/if}}<% } %>"<% if (panel.name) print(' data-name="'+panel.name+'"') %>>
|
||||
<%
|
||||
var panelLabelString = null;
|
||||
if ('customLabel' in panel) {
|
||||
if (panel.customLabel) {
|
||||
panelLabelString = panel.customLabel;
|
||||
}
|
||||
} else {
|
||||
if (panel.label) {
|
||||
panelLabelString = "{{translate \"" + panel.label + "\" scope=\""+model.name+"\"}}";
|
||||
}
|
||||
}
|
||||
%>
|
||||
<% if (panelLabelString) { %>
|
||||
<div class="panel-heading"><h4 class="panel-title"><%= panelLabelString %></h4></div>
|
||||
<% if (panel.label) { %>
|
||||
<div class="panel-heading"><h4 class="panel-title"><%= panel.label %></h4></div>
|
||||
<% } %>
|
||||
<div class="panel-body panel-body-form">
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<header data-name="{{name}}">
|
||||
<label data-is-custom="{{#if isCustomLabel}}true{{/if}}">{{label}}</label>
|
||||
<label data-is-custom="{{#if isCustomLabel}}true{{/if}}" data-label="{{label}}">{{labelTranslated}}</label>
|
||||
<a href="javascript:" data-action="edit-panel-label" class="edit-panel-label"><i class="fas fa-pencil-alt fa-sm"></i></a>
|
||||
<a href="javascript:" style="float: right;" data-action="removePanel" class="remove-panel" data-number="{{number}}"><i class="fas fa-times"></i></a>
|
||||
</header>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="no-margin">
|
||||
<table class="table table-bordered no-margin">
|
||||
<table class="table table-bordered no-margin scope-level">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th width="20%">{{translate 'Access' scope='Role'}}</th>
|
||||
@@ -25,7 +25,12 @@
|
||||
{{#each ../list}}
|
||||
<td>
|
||||
{{#if levelList}}
|
||||
<select name="{{name}}" class="form-control{{#ifNotEqual ../../../access 'enabled'}} hidden{{/ifNotEqual}}" data-scope="{{../../name}}"{{#ifNotEqual ../../access 'enabled'}} disabled{{/ifNotEqual}} title="{{translate action scope='Role' category='actions'}}">
|
||||
<select name="{{name}}"
|
||||
class="form-control scope-action{{#ifNotEqual ../../../access 'enabled'}} hidden{{/ifNotEqual}}"
|
||||
data-scope="{{../../name}}"
|
||||
{{#ifNotEqual ../../access 'enabled'}} disabled{{/ifNotEqual}}
|
||||
title="{{translate action scope='Role' category='actions'}}"
|
||||
data-role-action="{{action}}">
|
||||
{{options levelList level field='levelList' scope='Role'}}
|
||||
</select>
|
||||
{{/if}}
|
||||
@@ -35,6 +40,18 @@
|
||||
</tr>
|
||||
{{/each}}
|
||||
</table>
|
||||
|
||||
<div class="sticky-header-scope hidden sticky-head">
|
||||
<table class="table borderless no-margin">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th width="20%">{{translate 'Access' scope='Role'}}</th>
|
||||
{{#each actionList}}
|
||||
<th width="11%">{{translate this scope='Role' category='actions'}}</th>
|
||||
{{/each}}
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,7 +63,7 @@
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="no-margin">
|
||||
<table class="table table-bordered no-margin">
|
||||
<table class="table table-bordered no-margin field-level">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th width="20%"></th>
|
||||
@@ -67,7 +84,13 @@
|
||||
<td><b>{{translate name category='fields' scope=../name}}</b></td>
|
||||
{{#each list}}
|
||||
<td>
|
||||
<select name="field-{{../../name}}-{{../name}}" class="form-control" data-field="{{../name}}" data-scope="{{../../name}}" data-action="{{name}}" title="{{translate name scope='Role' category='actions'}}">{{options ../../../fieldLevelList value scope='Role' field='accessList'}}</select>
|
||||
<select
|
||||
name="field-{{../../name}}-{{../name}}"
|
||||
class="form-control field-action"
|
||||
data-field="{{../name}}"
|
||||
data-scope="{{../../name}}"
|
||||
data-action="{{name}}"
|
||||
title="{{translate name scope='Role' category='actions'}}">{{options ../../../fieldLevelList value scope='Role' field='accessList'}}</select>
|
||||
</td>
|
||||
{{/each}}
|
||||
<td colspan="2">
|
||||
@@ -77,6 +100,19 @@
|
||||
{{/each}}
|
||||
{{/each}}
|
||||
</table>
|
||||
|
||||
<div class="sticky-header-field hidden sticky-head">
|
||||
<table class="table borderless no-margin">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th width="20%"></th>
|
||||
{{#each fieldActionList}}
|
||||
<th width="11%">{{translate this scope='Role' category='actions'}}</th>
|
||||
{{/each}}
|
||||
<th width="33%"></th>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,10 @@
|
||||
{{#each ../list}}
|
||||
<td>
|
||||
{{#ifNotEqual ../../access 'not-set'}}
|
||||
<span style="color: {{prop ../../../../colors level}};">{{translateOption level field='levelList' scope='Role'}}</span>
|
||||
<span
|
||||
style="color: {{prop ../../../../colors level}};"
|
||||
title="{{translate action scope='Role' category='actions'}}"
|
||||
>{{translateOption level field='levelList' scope='Role'}}</span>
|
||||
{{/ifNotEqual}}
|
||||
</td>
|
||||
{{/each}}
|
||||
@@ -68,7 +71,9 @@
|
||||
<td><b>{{translate name category='fields' scope=../name}}</b></td>
|
||||
{{#each list}}
|
||||
<td>
|
||||
<span style="color: {{prop ../../../colors value}};">{{translateOption value scope='Role' field='accessList'}}</span>
|
||||
<span
|
||||
title="{{translate name scope='Role' category='actions'}}"
|
||||
style="color: {{prop ../../../colors value}};">{{translateOption value scope='Role' field='accessList'}}</span>
|
||||
</td>
|
||||
{{/each}}
|
||||
<td colspan="3"></td>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="container content">
|
||||
<div class="col-md-4 col-md-offset-4 col-sm-8 col-sm-offset-2">
|
||||
<div class="col-md-4 col-md-offset-3 col-sm-8 col-sm-offset-2">
|
||||
<div class="panel panel-default password-change">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">{{translate 'Change Password' scope='User'}}</h4>
|
||||
|
||||
@@ -551,13 +551,13 @@ define(
|
||||
setCookieAuth: function (username, token) {
|
||||
var date = new Date();
|
||||
date.setTime(date.getTime() + (1000 * 24*60*60*1000));
|
||||
document.cookie = 'auth-username='+username+'; expires='+date.toGMTString()+'; path=/';
|
||||
document.cookie = 'auth-token='+token+'; expires='+date.toGMTString()+'; path=/';
|
||||
document.cookie = 'auth-username='+username+'; SameSite=Lax; expires='+date.toGMTString()+'; path=/';
|
||||
document.cookie = 'auth-token='+token+'; SameSite=Lax; expires='+date.toGMTString()+'; path=/';
|
||||
},
|
||||
|
||||
unsetCookieAuth: function () {
|
||||
document.cookie = 'auth-username' + '=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/';
|
||||
document.cookie = 'auth-token' + '=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/';
|
||||
document.cookie = 'auth-username' + '=; SameSite=Lax; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/';
|
||||
document.cookie = 'auth-token' + '=; SameSite=Lax; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/';
|
||||
},
|
||||
|
||||
initUserData: function (options, callback) {
|
||||
@@ -671,7 +671,7 @@ define(
|
||||
if (self.auth) {
|
||||
self.logout();
|
||||
} else {
|
||||
Espo.Ui.error(self.language.translate('Auth error'));
|
||||
console.error('Error 401: Unauthorized.');
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -184,7 +184,7 @@
|
||||
},
|
||||
|
||||
getEntityTypeFieldParam: function (entityType, field, param) {
|
||||
this.metadata.get(['entityDefs', entityType, 'fields', field, param]);
|
||||
return this.metadata.get(['entityDefs', entityType, 'fields', field, param]);
|
||||
},
|
||||
|
||||
getViewName: function (fieldType) {
|
||||
|
||||
@@ -490,7 +490,7 @@ define('ui', [], function () {
|
||||
},
|
||||
|
||||
error: function (message) {
|
||||
Espo.Ui.notify(message, 'error', 2000);
|
||||
Espo.Ui.notify(message, 'error', 4000);
|
||||
},
|
||||
|
||||
info: function (message) {
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
define('view-helper', ['lib!client/lib/purify.min.js'], function () {
|
||||
|
||||
var ViewHelper = function (options) {
|
||||
this.urlRegex = /(^|[^\(])(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
|
||||
this._registerHandlebarsHelpers();
|
||||
|
||||
this.mdBeforeList = [
|
||||
@@ -125,7 +124,8 @@ define('view-helper', ['lib!client/lib/purify.min.js'], function () {
|
||||
className += ' ' + additionalClassName;
|
||||
}
|
||||
|
||||
return '<img class="'+className+'" width="'+width+'" src="'+basePath+'?entryPoint=avatar&size='+size+'&id=' + id + '&t='+t+'">';
|
||||
return '<img class="'+className+'" width="'+width+'" src="'+basePath+
|
||||
'?entryPoint=avatar&size='+size+'&id=' + id + '&t='+t+'">';
|
||||
},
|
||||
|
||||
_registerHandlebarsHelpers: function () {
|
||||
@@ -315,8 +315,6 @@ define('view-helper', ['lib!client/lib/purify.min.js'], function () {
|
||||
transfromMarkdownText: function (text, options) {
|
||||
text = text || '';
|
||||
|
||||
text = text.replace(this.urlRegex, '$1[$2]($2)');
|
||||
|
||||
text = Handlebars.Utils.escapeExpression(text).replace(/>+/g, '>');
|
||||
|
||||
this.mdBeforeList.forEach(function (item) {
|
||||
|
||||
@@ -55,6 +55,11 @@ define('views/admin/authentication', 'views/settings/record/edit', function (Dep
|
||||
this.listenTo(this.model, 'change:auth2FA', function () {
|
||||
this.manage2FAFields();
|
||||
}, this);
|
||||
|
||||
this.managePasswordRecoveryFields();
|
||||
this.listenTo(this.model, 'change:passwordRecoveryDisabled', function () {
|
||||
this.managePasswordRecoveryFields();
|
||||
}, this);
|
||||
},
|
||||
|
||||
setupBeforeFinal: function () {
|
||||
@@ -126,5 +131,17 @@ define('views/admin/authentication', 'views/settings/record/edit', function (Dep
|
||||
}
|
||||
},
|
||||
|
||||
managePasswordRecoveryFields: function () {
|
||||
if (!this.model.get('passwordRecoveryDisabled')) {
|
||||
this.showField('passwordRecoveryForAdminDisabled');
|
||||
this.showField('passwordRecoveryForInternalUsersDisabled');
|
||||
this.showField('passwordRecoveryNoExposure');
|
||||
} else {
|
||||
this.hideField('passwordRecoveryForAdminDisabled');
|
||||
this.hideField('passwordRecoveryForInternalUsersDisabled');
|
||||
this.hideField('passwordRecoveryNoExposure');
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -297,10 +297,10 @@ define('views/admin/layouts/grid', ['views/admin/layouts/base', 'res!client/css/
|
||||
|
||||
data.isCustomLabel = false;
|
||||
if (data.customLabel) {
|
||||
data.label = data.customLabel;
|
||||
data.labelTranslated = data.customLabel;
|
||||
data.isCustomLabel = true;
|
||||
} else {
|
||||
data.label = this.translate(data.label, 'labels', this.scope);
|
||||
data.labelTranslated = this.translate(data.label, 'labels', this.scope);
|
||||
}
|
||||
|
||||
data.style = data.style || null;
|
||||
@@ -437,7 +437,7 @@ define('views/admin/layouts/grid', ['views/admin/layouts/base', 'res!client/css/
|
||||
if ($label.attr('data-is-custom')) {
|
||||
o.customLabel = $label.text();
|
||||
} else {
|
||||
o.label = $label.text();
|
||||
o.label = $label.data('label');
|
||||
}
|
||||
$(el).find('ul.rows > li').each(function (i, li) {
|
||||
var row = [];
|
||||
|
||||
@@ -503,7 +503,8 @@ define('views/admin/link-manager/modals/edit',
|
||||
if (view) {
|
||||
view.disabled = true;
|
||||
}
|
||||
this.$el.find('.cell[data-name=' + name+']').addClass('hidden');
|
||||
|
||||
this.$el.find('.cell[data-name=' + name+']').addClass('hidden-cell');
|
||||
},
|
||||
|
||||
showField: function (name) {
|
||||
@@ -511,7 +512,8 @@ define('views/admin/link-manager/modals/edit',
|
||||
if (view) {
|
||||
view.disabled = false;
|
||||
}
|
||||
this.$el.find('.cell[data-name=' + name+']').removeClass('hidden');
|
||||
|
||||
this.$el.find('.cell[data-name=' + name+']').removeClass('hidden-cell');
|
||||
},
|
||||
|
||||
handleLinkTypeChange: function () {
|
||||
|
||||
@@ -26,45 +26,45 @@
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
Espo.define('views/admin/notifications', 'views/settings/record/edit', function (Dep) {
|
||||
define('views/admin/notifications', 'views/settings/record/edit', function (Dep) {
|
||||
|
||||
return Dep.extend({
|
||||
|
||||
layoutName: 'notifications',
|
||||
|
||||
dependencyDefs: {
|
||||
'assignmentEmailNotifications': {
|
||||
map: {
|
||||
true: [
|
||||
{
|
||||
action: 'show',
|
||||
fields: ['assignmentEmailNotificationsEntityList']
|
||||
}
|
||||
]
|
||||
dynamicLogicDefs: {
|
||||
fields: {
|
||||
assignmentEmailNotificationsEntityList: {
|
||||
visible: {
|
||||
conditionGroup: [
|
||||
{
|
||||
type: 'isTrue',
|
||||
attribute: 'assignmentEmailNotifications',
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
adminNotificationsNewVersion: {
|
||||
visible: {
|
||||
conditionGroup: [
|
||||
{
|
||||
type: 'isTrue',
|
||||
attribute: 'adminNotifications',
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
adminNotificationsNewExtensionVersion: {
|
||||
visible: {
|
||||
conditionGroup: [
|
||||
{
|
||||
type: 'isTrue',
|
||||
attribute: 'adminNotifications',
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
default: [
|
||||
{
|
||||
action: 'hide',
|
||||
fields: ['assignmentEmailNotificationsEntityList']
|
||||
}
|
||||
]
|
||||
},
|
||||
'adminNotifications': {
|
||||
map: {
|
||||
true: [
|
||||
{
|
||||
action: 'show',
|
||||
fields: ['adminNotificationsNewVersion', 'adminNotificationsNewExtensionVersion']
|
||||
}
|
||||
]
|
||||
},
|
||||
default: [
|
||||
{
|
||||
action: 'hide',
|
||||
fields: ['adminNotificationsNewVersion', 'adminNotificationsNewExtensionVersion']
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
setup: function () {
|
||||
@@ -89,6 +89,4 @@ Espo.define('views/admin/notifications', 'views/settings/record/edit', function
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -255,6 +255,11 @@ define('views/detail', 'views/main', function (Dep) {
|
||||
view.render();
|
||||
view.notify(false);
|
||||
this.listenToOnce(view, 'after:save', function () {
|
||||
if (data.fromSelectRelated) {
|
||||
setTimeout(function () {
|
||||
this.clearView('dialogSelectRelated');
|
||||
}.bind(this), 25);
|
||||
}
|
||||
this.updateRelationshipPanel(link);
|
||||
this.model.trigger('after:relate');
|
||||
}, this);
|
||||
@@ -287,7 +292,7 @@ define('views/detail', 'views/main', function (Dep) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var foreignLink = this.model.defs['links'][link].foreign;
|
||||
var foreignLink = (this.model.defs['links'][link] || {}).foreign;
|
||||
if (foreignLink && scope) {
|
||||
var foreignLinkType = this.getMetadata().get(['entityDefs', scope, 'links', foreignLink, 'type']);
|
||||
var foreignLinkFieldType = this.getMetadata().get(['entityDefs', scope, 'fields', foreignLink, 'type']);
|
||||
@@ -329,18 +334,27 @@ define('views/detail', 'views/main', function (Dep) {
|
||||
var viewName = this.getMetadata().get('clientDefs.' + scope + '.modalViews.select') || 'views/modals/select-records';
|
||||
|
||||
this.notify('Loading...');
|
||||
this.createView('dialog', viewName, {
|
||||
this.createView('dialogSelectRelated', viewName, {
|
||||
scope: scope,
|
||||
multiple: true,
|
||||
createButton: false,
|
||||
createButton: data.createButton || false,
|
||||
triggerCreateEvent: true,
|
||||
filters: filters,
|
||||
massRelateEnabled: massRelateEnabled,
|
||||
primaryFilterName: primaryFilterName,
|
||||
boolFilterList: boolFilterList
|
||||
boolFilterList: boolFilterList,
|
||||
}, function (dialog) {
|
||||
dialog.render();
|
||||
this.notify(false);
|
||||
dialog.once('select', function (selectObj) {
|
||||
Espo.Ui.notify(false);
|
||||
|
||||
this.listenTo(dialog, 'create', function () {
|
||||
this.actionCreateRelated({
|
||||
link: data.link,
|
||||
fromSelectRelated: true,
|
||||
});
|
||||
}, this);
|
||||
|
||||
this.listenToOnce(dialog, 'select', function (selectObj) {
|
||||
var data = {};
|
||||
if (Object.prototype.toString.call(selectObj) === '[object Array]') {
|
||||
var ids = [];
|
||||
@@ -356,21 +370,21 @@ define('views/detail', 'views/main', function (Dep) {
|
||||
data.id = selectObj.id;
|
||||
}
|
||||
}
|
||||
$.ajax({
|
||||
url: this.scope + '/' + this.model.id + '/' + link,
|
||||
type: 'POST',
|
||||
data: JSON.stringify(data),
|
||||
success: function () {
|
||||
Espo.Ajax.postRequest(this.scope + '/' + this.model.id + '/' + link, data)
|
||||
.then(
|
||||
function () {
|
||||
this.notify('Linked', 'success');
|
||||
this.updateRelationshipPanel(link);
|
||||
this.model.trigger('after:relate');
|
||||
}.bind(this),
|
||||
error: function () {
|
||||
}.bind(this)
|
||||
)
|
||||
.fail(
|
||||
function () {
|
||||
this.notify('Error occurred', 'error');
|
||||
}.bind(this)
|
||||
});
|
||||
}.bind(this));
|
||||
}.bind(this));
|
||||
);
|
||||
}, this);
|
||||
});
|
||||
},
|
||||
|
||||
actionDuplicate: function () {
|
||||
|
||||
@@ -111,7 +111,7 @@ define('views/email-account/record/detail', 'views/record/detail', function (Dep
|
||||
initSmtpFieldsControl: function () {
|
||||
this.controlSmtpFields();
|
||||
this.listenTo(this.model, 'change:useSmtp', this.controlSmtpFields, this);
|
||||
this.listenTo(this.model, 'change:smtpAuth', this.controlSmtpAuthField, this);
|
||||
this.listenTo(this.model, 'change:smtpAuth', this.controlSmtpFields, this);
|
||||
},
|
||||
|
||||
controlSmtpFields: function () {
|
||||
|
||||
@@ -168,6 +168,9 @@ define('views/email/detail', ['views/detail', 'email-helper'], function (Dep, Em
|
||||
}, function (view) {
|
||||
view.render();
|
||||
view.notify(false);
|
||||
this.listenTo(view, 'before:save', function () {
|
||||
this.getView('record').blockUpdateWebSocket(true);
|
||||
}, this);
|
||||
this.listenToOnce(view, 'after:save', function () {
|
||||
this.model.fetch();
|
||||
this.removeMenuItem('createContact');
|
||||
@@ -227,6 +230,9 @@ define('views/email/detail', ['views/detail', 'email-helper'], function (Dep, Em
|
||||
this.removeMenuItem('createCase');
|
||||
view.close();
|
||||
}, this);
|
||||
this.listenTo(view, 'before:save', function () {
|
||||
this.getView('record').blockUpdateWebSocket(true);
|
||||
}, this);
|
||||
});
|
||||
}.bind(this));
|
||||
},
|
||||
@@ -306,6 +312,9 @@ define('views/email/detail', ['views/detail', 'email-helper'], function (Dep, Em
|
||||
this.removeMenuItem('createLead');
|
||||
view.close();
|
||||
}.bind(this));
|
||||
this.listenTo(view, 'before:save', function () {
|
||||
this.getView('record').blockUpdateWebSocket(true);
|
||||
}, this);
|
||||
}.bind(this));
|
||||
|
||||
},
|
||||
|
||||
@@ -321,7 +321,10 @@ define('views/fields/array', ['views/fields/base', 'lib!Selectize'], function (D
|
||||
highlight: false,
|
||||
searchField: ['label'],
|
||||
plugins: ['remove_button'],
|
||||
score: function (search) {
|
||||
};
|
||||
|
||||
if (!this.matchAnyWord) {
|
||||
selectizeOptions.score = function (search) {
|
||||
var score = this.getScoreFunction(search);
|
||||
search = search.toLowerCase();
|
||||
return function (item) {
|
||||
@@ -330,8 +333,8 @@ define('views/fields/array', ['views/fields/base', 'lib!Selectize'], function (D
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
if (this.allowCustomOptions) {
|
||||
selectizeOptions.persist = false;
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
Espo.define('views/fields/attachment-multiple', 'views/fields/base', function (Dep) {
|
||||
define('views/fields/attachment-multiple', 'views/fields/base', function (Dep) {
|
||||
|
||||
return Dep.extend({
|
||||
|
||||
@@ -73,6 +73,7 @@ Espo.define('views/fields/attachment-multiple', 'views/fields/base', function (D
|
||||
this.deleteAttachment(id);
|
||||
}
|
||||
$div.parent().remove();
|
||||
this.$el.find('input.file').val(null);
|
||||
},
|
||||
'change input.file': function (e) {
|
||||
var $file = $(e.currentTarget);
|
||||
@@ -204,6 +205,10 @@ Espo.define('views/fields/attachment-multiple', 'views/fields/base', function (D
|
||||
$(window).off('resize.' + this.cid);
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
this.on('inline-edit-off', function () {
|
||||
this.isUploading = false;
|
||||
}, this);
|
||||
},
|
||||
|
||||
setupSearch: function () {
|
||||
@@ -413,30 +418,37 @@ Espo.define('views/fields/attachment-multiple', 'views/fields/base', function (D
|
||||
attachment.set('file', e.target.result);
|
||||
attachment.set('field', this.name);
|
||||
|
||||
attachment.save({}, {timeout: 0}).then(function () {
|
||||
if (canceledList.indexOf(attachment.cid) === -1) {
|
||||
$attachmentBox.trigger('ready');
|
||||
this.pushAttachment(attachment);
|
||||
$attachmentBox.attr('data-id', attachment.id);
|
||||
uploadedCount++;
|
||||
if (uploadedCount == totalCount && this.isUploading) {
|
||||
this.isUploading = false;
|
||||
this.model.trigger('attachment-uploaded:' + this.name);
|
||||
this.afterAttachmentsUploaded.call(this);
|
||||
}
|
||||
}
|
||||
}.bind(this)).fail(function () {
|
||||
$attachmentBox.remove();
|
||||
totalCount--;
|
||||
if (!totalCount) {
|
||||
this.isUploading = false;
|
||||
this.$el.find('.uploading-message').remove();
|
||||
}
|
||||
if (uploadedCount == totalCount && this.isUploading) {
|
||||
this.isUploading = false;
|
||||
this.afterAttachmentsUploaded.call(this);
|
||||
}
|
||||
}.bind(this));
|
||||
attachment
|
||||
.save({}, {timeout: 0})
|
||||
.then(
|
||||
function () {
|
||||
if (canceledList.indexOf(attachment.cid) === -1) {
|
||||
$attachmentBox.trigger('ready');
|
||||
this.pushAttachment(attachment);
|
||||
$attachmentBox.attr('data-id', attachment.id);
|
||||
uploadedCount++;
|
||||
if (uploadedCount == totalCount && this.isUploading) {
|
||||
this.model.trigger('attachment-uploaded:' + this.name);
|
||||
this.afterAttachmentsUploaded.call(this);
|
||||
this.isUploading = false;
|
||||
}
|
||||
}
|
||||
}.bind(this)
|
||||
)
|
||||
.fail(
|
||||
function () {
|
||||
$attachmentBox.remove();
|
||||
totalCount--;
|
||||
if (!totalCount) {
|
||||
this.isUploading = false;
|
||||
this.$el.find('.uploading-message').remove();
|
||||
}
|
||||
if (uploadedCount == totalCount && this.isUploading) {
|
||||
this.isUploading = false;
|
||||
this.afterAttachmentsUploaded.call(this);
|
||||
}
|
||||
}.bind(this)
|
||||
);
|
||||
}.bind(this);
|
||||
fileReader.readAsDataURL(file);
|
||||
}, this);
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
Espo.define('views/fields/base', 'view', function (Dep) {
|
||||
define('views/fields/base', 'view', function (Dep) {
|
||||
|
||||
return Dep.extend({
|
||||
|
||||
@@ -359,20 +359,19 @@ Espo.define('views/fields/base', 'view', function (Dep) {
|
||||
}
|
||||
|
||||
if (this.mode != 'search') {
|
||||
this.attributeList = this.getAttributeList();
|
||||
this.attributeList = this.getAttributeList(); // for backward compatibility, to be removed
|
||||
|
||||
this.listenTo(this.model, 'change', function (model, options) {
|
||||
if (this.isRendered() || this.isBeingRendered()) {
|
||||
if (options.ui) {
|
||||
return;
|
||||
}
|
||||
|
||||
var changed = false;
|
||||
this.attributeList.forEach(function (attribute) {
|
||||
this.getAttributeList().forEach(function (attribute) {
|
||||
if (model.hasChanged(attribute)) {
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
}, this);
|
||||
|
||||
if (changed && !options.skipReRender) {
|
||||
this.reRender();
|
||||
|
||||
@@ -41,14 +41,17 @@ define('views/fields/complex-created', 'views/fields/base', function (Dep) {
|
||||
return [this.fieldAt, this.fieldBy];
|
||||
},
|
||||
|
||||
setup: function () {
|
||||
Dep.prototype.setup.call(this);
|
||||
|
||||
init: function () {
|
||||
this.baseName = this.options.baseName || this.baseName;
|
||||
|
||||
this.fieldAt = this.baseName + 'At';
|
||||
this.fieldBy = this.baseName + 'By';
|
||||
|
||||
Dep.prototype.init.call(this);
|
||||
},
|
||||
|
||||
setup: function () {
|
||||
Dep.prototype.setup.call(this);
|
||||
|
||||
this.createField('at');
|
||||
this.createField('by');
|
||||
},
|
||||
|
||||
@@ -98,41 +98,55 @@ define('views/fields/date', 'views/fields/base', function (Dep) {
|
||||
}
|
||||
|
||||
if (this.mode == 'list' || this.mode == 'detail' || this.mode == 'listLink') {
|
||||
if (this.getConfig().get('readableDateFormatDisabled') || this.params.useNumericFormat) {
|
||||
return this.getDateTime().toDisplayDate(value);
|
||||
}
|
||||
|
||||
var d = moment.tz(value + ' OO:OO:00', this.getDateTime().internalDateTimeFormat, this.getDateTime().getTimeZone());
|
||||
|
||||
var today = moment().tz(this.getDateTime().getTimeZone()).startOf('day');
|
||||
var dt = today.clone();
|
||||
|
||||
var ranges = {
|
||||
'today': [dt.unix(), dt.add(1, 'days').unix()],
|
||||
'tomorrow': [dt.unix(), dt.add(1, 'days').unix()],
|
||||
'yesterday': [dt.add(-3, 'days').unix(), dt.add(1, 'days').unix()]
|
||||
};
|
||||
|
||||
if (d.unix() >= ranges['today'][0] && d.unix() < ranges['today'][1]) {
|
||||
return this.translate('Today');
|
||||
} else if (d.unix() >= ranges['tomorrow'][0] && d.unix() < ranges['tomorrow'][1]) {
|
||||
return this.translate('Tomorrow');
|
||||
} else if (d.unix() >= ranges['yesterday'][0] && d.unix() < ranges['yesterday'][1]) {
|
||||
return this.translate('Yesterday');
|
||||
}
|
||||
|
||||
var readableFormat = this.getDateTime().getReadableDateFormat();
|
||||
|
||||
if (d.format('YYYY') == today.format('YYYY')) {
|
||||
return d.format(readableFormat);
|
||||
} else {
|
||||
return d.format(readableFormat + ', YYYY');
|
||||
}
|
||||
return this.convertDateValueForDetail(value);
|
||||
}
|
||||
|
||||
return this.getDateTime().toDisplayDate(value);
|
||||
},
|
||||
|
||||
convertDateValueForDetail: function (value) {
|
||||
if (this.getConfig().get('readableDateFormatDisabled') || this.params.useNumericFormat) {
|
||||
return this.getDateTime().toDisplayDate(value);
|
||||
}
|
||||
|
||||
var timezone = this.getDateTime().getTimeZone();
|
||||
var internalDateTimeFormat = this.getDateTime().internalDateTimeFormat;
|
||||
var readableFormat = this.getDateTime().getReadableDateFormat();
|
||||
var valueWithTime = value + ' 00:00:00';
|
||||
|
||||
var today = moment().tz(timezone).startOf('day');
|
||||
var dateTime = moment.tz(valueWithTime, internalDateTimeFormat, timezone);
|
||||
|
||||
var temp = today.clone();
|
||||
|
||||
var ranges = {
|
||||
'today': [temp.unix(), temp.add(1, 'days').unix()],
|
||||
'tomorrow': [temp.unix(), temp.add(1, 'days').unix()],
|
||||
'yesterday': [temp.add(-3, 'days').unix(), temp.add(1, 'days').unix()],
|
||||
};
|
||||
|
||||
if (dateTime.unix() >= ranges['today'][0] && dateTime.unix() < ranges['today'][1]) {
|
||||
return this.translate('Today');
|
||||
}
|
||||
|
||||
if (dateTime.unix() >= ranges['tomorrow'][0] && dateTime.unix() < ranges['tomorrow'][1]) {
|
||||
return this.translate('Tomorrow');
|
||||
}
|
||||
|
||||
if (dateTime.unix() >= ranges['yesterday'][0] && dateTime.unix() < ranges['yesterday'][1]) {
|
||||
return this.translate('Yesterday');
|
||||
}
|
||||
|
||||
// Need to use UTC, otherwise there's a DST issue with old dates.
|
||||
var dateTime = moment.utc(valueWithTime, internalDateTimeFormat);
|
||||
|
||||
if (dateTime.format('YYYY') == today.format('YYYY')) {
|
||||
return dateTime.format(readableFormat);
|
||||
}
|
||||
|
||||
return dateTime.format(readableFormat + ', YYYY');
|
||||
},
|
||||
|
||||
getDateStringValue: function () {
|
||||
if (this.mode === 'detail' && !this.model.has(this.name)) {
|
||||
return '...';
|
||||
@@ -161,7 +175,7 @@ define('views/fields/date', 'views/fields/base', function (Dep) {
|
||||
weekStart: this.getDateTime().weekStart,
|
||||
autoclose: true,
|
||||
todayHighlight: true,
|
||||
keyboardNavigation: false,
|
||||
keyboardNavigation: true,
|
||||
todayBtn: this.getConfig().get('datepickerTodayButton') || false,
|
||||
};
|
||||
|
||||
@@ -181,15 +195,11 @@ define('views/fields/date', 'views/fields/base', function (Dep) {
|
||||
|
||||
options.language = language;
|
||||
|
||||
var $datePicker = this.$element.datepicker(options).on('show', function (e) {
|
||||
$('body > .datepicker.datepicker-dropdown').css('z-index', 1200);
|
||||
}.bind(this));
|
||||
var $datePicker = this.$element.datepicker(options);
|
||||
|
||||
if (this.mode == 'search') {
|
||||
var $elAdd = this.$el.find('input.additional');
|
||||
$elAdd.datepicker(options).on('show', function (e) {
|
||||
$('body > .datepicker.datepicker-dropdown').css('z-index', 1200);
|
||||
}.bind(this));
|
||||
$elAdd.datepicker(options);
|
||||
$elAdd.parent().find('button.date-picker-btn').on('click', function (e) {
|
||||
$elAdd.datepicker('show');
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
Espo.define('views/fields/file', 'views/fields/link', function (Dep) {
|
||||
define('views/fields/file', 'views/fields/link', function (Dep) {
|
||||
|
||||
return Dep.extend({
|
||||
|
||||
@@ -62,6 +62,7 @@ Espo.define('views/fields/file', 'views/fields/link', function (Dep) {
|
||||
var $div = $(e.currentTarget).parent();
|
||||
this.deleteAttachment();
|
||||
$div.parent().remove();
|
||||
this.$el.find('input.file').val(null);
|
||||
},
|
||||
'change input.file': function (e) {
|
||||
var $file = $(e.currentTarget);
|
||||
@@ -179,6 +180,10 @@ Espo.define('views/fields/file', 'views/fields/link', function (Dep) {
|
||||
$(window).off('resize.' + this.cid);
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
this.on('inline-edit-off', function () {
|
||||
this.isUploading = false;
|
||||
}, this);
|
||||
},
|
||||
|
||||
afterRender: function () {
|
||||
@@ -340,8 +345,8 @@ Espo.define('views/fields/file', 'views/fields/link', function (Dep) {
|
||||
}
|
||||
if (exceedsMaxFileSize) {
|
||||
var msg = this.translate('fieldMaxFileSizeError', 'messages')
|
||||
.replace('{field}', this.getLabelText())
|
||||
.replace('{max}', maxFileSize);
|
||||
.replace('{field}', this.getLabelText())
|
||||
.replace('{max}', maxFileSize);
|
||||
this.showValidationMessage(msg, '.attachment-button label');
|
||||
return;
|
||||
}
|
||||
@@ -357,6 +362,7 @@ Espo.define('views/fields/file', 'views/fields/link', function (Dep) {
|
||||
isCanceled = true;
|
||||
this.$el.find('.attachment-button').removeClass('hidden');
|
||||
this.isUploading = false;
|
||||
this.$el.find('input.file').val(null);
|
||||
}.bind(this));
|
||||
|
||||
var fileReader = new FileReader();
|
||||
@@ -370,18 +376,25 @@ Espo.define('views/fields/file', 'views/fields/link', function (Dep) {
|
||||
attachment.set('file', result);
|
||||
attachment.set('field', this.name);
|
||||
|
||||
attachment.save({}, {timeout: 0}).then(function () {
|
||||
this.isUploading = false;
|
||||
if (!isCanceled) {
|
||||
$attachmentBox.trigger('ready');
|
||||
this.setAttachment(attachment);
|
||||
}
|
||||
}.bind(this)).fail(function () {
|
||||
$attachmentBox.remove();
|
||||
this.$el.find('.uploading-message').remove();
|
||||
this.$el.find('.attachment-button').removeClass('hidden');
|
||||
this.isUploading = false;
|
||||
}.bind(this));
|
||||
attachment
|
||||
.save({}, {timeout: 0})
|
||||
.then(
|
||||
function () {
|
||||
if (!isCanceled && this.isUploading) {
|
||||
$attachmentBox.trigger('ready');
|
||||
this.setAttachment(attachment);
|
||||
this.isUploading = false;
|
||||
}
|
||||
}.bind(this)
|
||||
)
|
||||
.fail(
|
||||
function () {
|
||||
$attachmentBox.remove();
|
||||
this.$el.find('.uploading-message').remove();
|
||||
this.$el.find('.attachment-button').removeClass('hidden');
|
||||
this.isUploading = false;
|
||||
}.bind(this)
|
||||
);
|
||||
}.bind(this));
|
||||
}.bind(this);
|
||||
fileReader.readAsDataURL(file);
|
||||
|
||||
@@ -38,6 +38,8 @@ define('views/fields/multi-enum', ['views/fields/array', 'lib!Selectize'], funct
|
||||
|
||||
editTemplate: 'fields/multi-enum/edit',
|
||||
|
||||
matchAnyWord: false,
|
||||
|
||||
events: {
|
||||
},
|
||||
|
||||
@@ -172,7 +174,10 @@ define('views/fields/multi-enum', ['views/fields/array', 'lib!Selectize'], funct
|
||||
searchField: ['label'],
|
||||
plugins: pluginList,
|
||||
copyClassesToDropdown: true,
|
||||
score: function (search) {
|
||||
};
|
||||
|
||||
if (!this.matchAnyWord) {
|
||||
selectizeOptions.score = function (search) {
|
||||
var score = this.getScoreFunction(search);
|
||||
search = search.toLowerCase();
|
||||
return function (item) {
|
||||
@@ -181,8 +186,8 @@ define('views/fields/multi-enum', ['views/fields/array', 'lib!Selectize'], funct
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
if (this.allowCustomOptions) {
|
||||
selectizeOptions.persist = false;
|
||||
|
||||
@@ -51,7 +51,7 @@ define('views/fields/person-name', 'views/fields/varchar', function (Dep) {
|
||||
data.middleValue = this.model.get(this.middleField);
|
||||
data.salutationOptions = this.model.getFieldParam(this.salutationField, 'options');
|
||||
|
||||
if (this.model === 'edit') {
|
||||
if (this.mode === 'edit') {
|
||||
data.firstMaxLength = this.model.getFieldParam(this.firstField, 'maxLength');
|
||||
data.lastMaxLength = this.model.getFieldParam(this.lastField, 'maxLength');
|
||||
data.middleMaxLength = this.model.getFieldParam(this.middleField, 'maxLength');
|
||||
|
||||
@@ -47,7 +47,7 @@ define('views/inbound-email/record/detail', 'views/record/detail', function (Dep
|
||||
this.controlSmtpFields();
|
||||
this.controlSentFolderField();
|
||||
this.listenTo(this.model, 'change:useSmtp', this.controlSmtpFields, this);
|
||||
this.listenTo(this.model, 'change:smtpAuth', this.controlSmtpAuthField, this);
|
||||
this.listenTo(this.model, 'change:smtpAuth', this.controlSmtpFields, this);
|
||||
this.listenTo(this.model, 'change:storeSentEmails', this.controlSentFolderField, this);
|
||||
},
|
||||
|
||||
|
||||
@@ -199,7 +199,7 @@ define('views/main', 'view', function (Dep) {
|
||||
var type = false;
|
||||
|
||||
['actions', 'dropdown', 'buttons'].forEach(function (t) {
|
||||
this.menu[t].forEach(function (item, i) {
|
||||
(this.menu[t] || []).forEach(function (item, i) {
|
||||
item = item || {};
|
||||
if (item.name == name) {
|
||||
index = i;
|
||||
@@ -244,7 +244,7 @@ define('views/main', 'view', function (Dep) {
|
||||
|
||||
hideHeaderActionItem: function (name) {
|
||||
['actions', 'dropdown', 'buttons'].forEach(function (t) {
|
||||
this.menu[t].forEach(function (item, i) {
|
||||
(this.menu[t] || []).forEach(function (item, i) {
|
||||
item = item || {};
|
||||
if (item.name == name) {
|
||||
item.hidden = true;
|
||||
@@ -260,7 +260,7 @@ define('views/main', 'view', function (Dep) {
|
||||
|
||||
showHeaderActionItem: function (name) {
|
||||
['actions', 'dropdown', 'buttons'].forEach(function (t) {
|
||||
this.menu[t].forEach(function (item, i) {
|
||||
(this.menu[t] || []).forEach(function (item, i) {
|
||||
item = item || {};
|
||||
if (item.name == name) {
|
||||
item.hidden = false;
|
||||
@@ -276,7 +276,7 @@ define('views/main', 'view', function (Dep) {
|
||||
|
||||
hasMenuVisibleDropdownItems: function () {
|
||||
var hasItems = false;
|
||||
this.menu.dropdown.forEach(function (item) {
|
||||
(this.menu.dropdown || []).forEach(function (item) {
|
||||
if (!item.hidden) hasItems = true;
|
||||
});
|
||||
return hasItems;
|
||||
|
||||
@@ -169,6 +169,10 @@ Espo.define('views/modals/edit', 'views/modal', function (Dep) {
|
||||
this.dialog.close();
|
||||
}, this);
|
||||
|
||||
editView.once('before:save', function () {
|
||||
this.trigger('before:save', model);
|
||||
}, this);
|
||||
|
||||
var $buttons = this.dialog.$el.find('.modal-footer button');
|
||||
$buttons.addClass('disabled').attr('disabled', 'disabled');
|
||||
|
||||
|
||||
@@ -144,6 +144,8 @@ define('views/modals/password-change-request', 'views/modal', function (Dep) {
|
||||
|
||||
var msg = this.translate('uniqueLinkHasBeenSent', 'messages', 'User');
|
||||
|
||||
msg += ' ' + this.translate('passwordRecoverySentIfMatched', 'messages', 'User');
|
||||
|
||||
this.$el.find('.cell-userName').addClass('hidden');
|
||||
this.$el.find('.cell-emailAddress').addClass('hidden');
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ define('views/modals/select-records', ['views/modal', 'search-manager'], functio
|
||||
|
||||
events: {
|
||||
'click button[data-action="create"]': function () {
|
||||
this.create();
|
||||
this.create();
|
||||
},
|
||||
'click .list a': function (e) {
|
||||
e.preventDefault();
|
||||
@@ -256,6 +256,11 @@ define('views/modals/select-records', ['views/modal', 'search-manager'], functio
|
||||
},
|
||||
|
||||
create: function () {
|
||||
if (this.options.triggerCreateEvent) {
|
||||
this.trigger('create');
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
this.notify('Loading...');
|
||||
@@ -285,4 +290,3 @@ define('views/modals/select-records', ['views/modal', 'search-manager'], functio
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1487,10 +1487,19 @@ define('views/record/detail', ['views/record/base', 'view-record-helper'], funct
|
||||
|
||||
for (var p in simplifiedLayout) {
|
||||
var panel = {};
|
||||
panel.label = simplifiedLayout[p].label || null;
|
||||
|
||||
if ('customLabel' in simplifiedLayout[p]) {
|
||||
panel.customLabel = simplifiedLayout[p].customLabel;
|
||||
panel.label = simplifiedLayout[p].customLabel;
|
||||
if (panel.label) {
|
||||
panel.label = this.getLanguage().translate(panel.label, 'panelCustomLabels', this.entityType);
|
||||
}
|
||||
} else {
|
||||
panel.label = simplifiedLayout[p].label || null;
|
||||
if (panel.label) {
|
||||
panel.label = this.getLanguage().translate(panel.label, 'labels', this.entityType);
|
||||
}
|
||||
}
|
||||
|
||||
panel.name = simplifiedLayout[p].name || null;
|
||||
panel.style = simplifiedLayout[p].style || 'default';
|
||||
panel.rows = [];
|
||||
@@ -1837,8 +1846,14 @@ define('views/record/detail', ['views/record/base', 'view-record-helper'], funct
|
||||
}
|
||||
},
|
||||
|
||||
blockUpdateWebSocket: function () {
|
||||
blockUpdateWebSocket: function (toUnblock) {
|
||||
this.updateWebSocketIsBlocked = true;
|
||||
|
||||
if (toUnblock) {
|
||||
setTimeout(function () {
|
||||
this.unblockUpdateWebSocket();
|
||||
}.bind(this), this.blockUpdateWebSocketPeriod || 500);
|
||||
}
|
||||
},
|
||||
|
||||
unblockUpdateWebSocket: function () {
|
||||
|
||||
@@ -214,10 +214,12 @@ define('views/record/panels-container', 'view', function (Dep) {
|
||||
view.disabled = false;
|
||||
view.trigger('show');
|
||||
|
||||
var fields = view.getFieldViews();
|
||||
if (fields) {
|
||||
for (var i in fields) {
|
||||
fields[i].reRender();
|
||||
if (view.getFieldViews) {
|
||||
var fields = view.getFieldViews();
|
||||
if (fields) {
|
||||
for (var i in fields) {
|
||||
fields[i].reRender();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +92,8 @@ define('views/record/panels/relationship', ['views/record/panels/bottom', 'searc
|
||||
this.defs.view = false;
|
||||
}
|
||||
|
||||
var hasCreate = false;
|
||||
|
||||
if (this.defs.create) {
|
||||
if (this.getAcl().check(this.scope, 'create') && !~this.noCreateScopeList.indexOf(this.scope)) {
|
||||
this.buttonList.push({
|
||||
@@ -104,6 +106,7 @@ define('views/record/panels/relationship', ['views/record/panels/bottom', 'searc
|
||||
link: this.link,
|
||||
}
|
||||
});
|
||||
hasCreate = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +119,7 @@ define('views/record/panels/relationship', ['views/record/panels/bottom', 'searc
|
||||
data.boolFilterList = this.defs.selectBoolFilterList;
|
||||
}
|
||||
data.massSelect = this.defs.massSelect;
|
||||
data.createButton = hasCreate;
|
||||
|
||||
this.actionList.unshift({
|
||||
label: 'Select',
|
||||
|
||||
@@ -58,6 +58,10 @@ define('views/role/record/table', 'view', function (Dep) {
|
||||
|
||||
booleanActionList: ['create'],
|
||||
|
||||
defaultLevels: {
|
||||
delete: 'no',
|
||||
},
|
||||
|
||||
colors: {
|
||||
yes: '#6BC924',
|
||||
all: '#6BC924',
|
||||
@@ -121,15 +125,37 @@ define('views/role/record/table', 'view', function (Dep) {
|
||||
if (this.lowestLevelByDefault) {
|
||||
$select.find('option').last().prop('selected', true);
|
||||
} else {
|
||||
$select.find('option').first().prop('selected', true);
|
||||
var setFirst = true;
|
||||
var action = $select.data('role-action');
|
||||
var defaultLevel = null;
|
||||
if (action) defaultLevel = this.defaultLevels[action];
|
||||
if (defaultLevel) {
|
||||
var $option = $select.find('option[value="'+defaultLevel+'"]');
|
||||
if ($option.length) {
|
||||
$option.prop('selected', true);
|
||||
setFirst = false;
|
||||
}
|
||||
}
|
||||
if (setFirst) {
|
||||
$select.find('option').first().prop('selected', true);
|
||||
}
|
||||
}
|
||||
$select.trigger('change');
|
||||
this.controlSelectColor($select);
|
||||
}.bind(this));
|
||||
} else {
|
||||
$dropdowns.attr('disabled', 'disabled');
|
||||
$dropdowns.addClass('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
this.controlSelectColor($(e.currentTarget));
|
||||
},
|
||||
'change select.scope-action': function (e) {
|
||||
this.controlSelectColor($(e.currentTarget));
|
||||
},
|
||||
'change select.field-action': function (e) {
|
||||
this.controlSelectColor($(e.currentTarget));
|
||||
},
|
||||
},
|
||||
|
||||
getTableDataList: function () {
|
||||
@@ -250,6 +276,13 @@ define('views/role/record/table', 'view', function (Dep) {
|
||||
if (this.mode == 'edit') {
|
||||
this.template = 'role/table-edit';
|
||||
}
|
||||
|
||||
this.once('remove', function () {
|
||||
$(window).off('scroll.scope-' + this.cid);
|
||||
$(window).off('resize.scope-' + this.cid);
|
||||
$(window).off('scroll.field-' + this.cid);
|
||||
$(window).off('resize.field-' + this.cid);
|
||||
}, this);
|
||||
},
|
||||
|
||||
setupData: function () {
|
||||
@@ -427,6 +460,11 @@ define('views/role/record/table', 'view', function (Dep) {
|
||||
}, this);
|
||||
|
||||
}, this);
|
||||
|
||||
this.initStickyHeader('scope');
|
||||
this.initStickyHeader('field');
|
||||
|
||||
this.setSelectColors();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -447,6 +485,8 @@ define('views/role/record/table', 'view', function (Dep) {
|
||||
$o.removeAttr('disabled');
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
this.controlSelectColor($edit);
|
||||
},
|
||||
|
||||
controlEditSelect: function (scope, value, dontChange) {
|
||||
@@ -466,6 +506,8 @@ define('views/role/record/table', 'view', function (Dep) {
|
||||
$o.removeAttr('disabled');
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
this.controlSelectColor($edit);
|
||||
},
|
||||
|
||||
controlStreamSelect: function (scope, value, dontChange) {
|
||||
@@ -485,6 +527,8 @@ define('views/role/record/table', 'view', function (Dep) {
|
||||
$o.removeAttr('disabled');
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
this.controlSelectColor($stream);
|
||||
},
|
||||
|
||||
controlDeleteSelect: function (scope, value, dontChange) {
|
||||
@@ -504,6 +548,8 @@ define('views/role/record/table', 'view', function (Dep) {
|
||||
$o.removeAttr('disabled');
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
this.controlSelectColor($delete);
|
||||
},
|
||||
|
||||
showAddFieldModal: function (scope) {
|
||||
@@ -568,5 +614,97 @@ define('views/role/record/table', 'view', function (Dep) {
|
||||
|
||||
}, this);
|
||||
},
|
||||
|
||||
initStickyHeader: function (type) {
|
||||
var $sticky = this.$el.find('.sticky-header-' + type);
|
||||
var $window = $(window);
|
||||
|
||||
var screenWidthXs = this.getThemeManager().getParam('screenWidthXs');
|
||||
|
||||
var $buttonContainer = $('.detail-button-container');
|
||||
|
||||
var $table = this.$el.find('table.'+type+'-level');
|
||||
|
||||
if (!$buttonContainer.length) return;
|
||||
|
||||
var handle = function (e) {
|
||||
if ($(window.document).width() < screenWidthXs) {
|
||||
$sticky.addClass('hidden');
|
||||
return;
|
||||
}
|
||||
var stickTopPosition = $buttonContainer.get(0).getBoundingClientRect().top + $buttonContainer.outerHeight();
|
||||
|
||||
|
||||
var topEdge = $table.position().top;
|
||||
topEdge += $buttonContainer.height();
|
||||
topEdge += $table.find('tr > th').height();
|
||||
|
||||
var bottomEdge = topEdge + $table.outerHeight(true);
|
||||
|
||||
var scrollTop = $window.scrollTop();
|
||||
|
||||
var width = $table.width();
|
||||
|
||||
if (scrollTop > topEdge && scrollTop < bottomEdge) {
|
||||
$sticky.css({
|
||||
position: 'fixed',
|
||||
marginTop: stickTopPosition + 'px',
|
||||
top: 0,
|
||||
width: width + 'px',
|
||||
marginLeft: '1px',
|
||||
});
|
||||
|
||||
$sticky.removeClass('hidden');
|
||||
} else {
|
||||
$sticky.addClass('hidden');
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
|
||||
$window.off('scroll.' + type + '-' + this.cid);
|
||||
$window.on('scroll.' + type + '-' + this.cid, handle);
|
||||
|
||||
$window.off('resize.' + type + '-' + this.cid);
|
||||
$window.on('resize.' + type + '-' + this.cid, handle);
|
||||
},
|
||||
|
||||
setSelectColors: function () {
|
||||
this.$el.find('select[data-type="access"]').each(function (i, el) {
|
||||
var $select = $(el);
|
||||
this.controlSelectColor($select);
|
||||
}.bind(this));
|
||||
|
||||
this.$el.find('select.scope-action').each(function (i, el) {
|
||||
var $select = $(el);
|
||||
this.controlSelectColor($select);
|
||||
}.bind(this));
|
||||
|
||||
this.$el.find('select.field-action').each(function (i, el) {
|
||||
var $select = $(el);
|
||||
this.controlSelectColor($select);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
controlSelectColor: function ($select) {
|
||||
var level = $select.val();
|
||||
var color = this.colors[level] || '';
|
||||
|
||||
if (level === 'not-set') color = '';
|
||||
$select.css('color', color);
|
||||
|
||||
$select.children().each(function (j, el) {
|
||||
var $o = $(el);
|
||||
var level = $o.val();
|
||||
|
||||
var color = this.colors[level] || '';
|
||||
if (level === 'not-set') color = '';
|
||||
|
||||
if ($o.attr('disabled')) {
|
||||
color = '';
|
||||
}
|
||||
|
||||
$o.css('color', color);
|
||||
}.bind(this));
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,14 +25,71 @@
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
Espo.define('views/scheduled-job/fields/scheduling', 'views/fields/base', function (Dep) {
|
||||
|
||||
define('views/scheduled-job/fields/scheduling',
|
||||
['views/fields/varchar', 'lib!client/lib/cronstrue-i18n.min.js'], function (Dep) {
|
||||
|
||||
return Dep.extend({
|
||||
|
||||
forceTrim: true,
|
||||
|
||||
setup: function () {
|
||||
Dep.prototype.setup.call(this);
|
||||
}
|
||||
|
||||
if (this.isEditMode() || this.isDetailMode()) {
|
||||
this.listenTo(this.model, 'change:' + this.name, function () {
|
||||
this.showText();
|
||||
}, this);
|
||||
}
|
||||
},
|
||||
|
||||
afterRender: function () {
|
||||
Dep.prototype.afterRender.call(this);
|
||||
|
||||
if (this.isEditMode() || this.isDetailMode()) {
|
||||
var $text = this.$text = $('<div class="small text-success"/>');
|
||||
this.$el.append($text);
|
||||
this.showText();
|
||||
}
|
||||
},
|
||||
|
||||
showText: function () {
|
||||
if (!this.$text || !this.$text.length) return;
|
||||
|
||||
var exp = this.model.get(this.name);
|
||||
|
||||
if (!exp) {
|
||||
this.$text.text('');
|
||||
return;
|
||||
}
|
||||
|
||||
if (exp == '* * * * *') {
|
||||
this.$text.text(this.translate('As often as possible', 'labels', 'ScheduledJob'));
|
||||
return;
|
||||
}
|
||||
|
||||
var locale = 'en';
|
||||
var localeList = Object.keys(cronstrue.default.locales);
|
||||
var language = this.getLanguage().name;
|
||||
|
||||
if (~localeList.indexOf(language)) {
|
||||
locale = language;
|
||||
} else if (~localeList.indexOf(language.split('_')[0])) {
|
||||
locale = language.split('_')[0];
|
||||
}
|
||||
|
||||
try {
|
||||
var text = cronstrue.toString(exp, {
|
||||
use24HourTimeFormat: !this.getDateTime().hasMeridian(),
|
||||
locale: locale,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
text = this.translate('Not valid');
|
||||
}
|
||||
|
||||
this.$text.text(text);
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -30,6 +30,8 @@ define('views/settings/fields/currency-list', 'views/fields/multi-enum', functio
|
||||
|
||||
return Dep.extend({
|
||||
|
||||
matchAnyWord: true,
|
||||
|
||||
setupOptions: function () {
|
||||
this.params.options = this.getMetadata().get(['app', 'currency', 'list']) || [];
|
||||
|
||||
|
||||
@@ -334,7 +334,7 @@ define('views/site/navbar', 'view', function (Dep) {
|
||||
$navbar.css('overflow', 'visible');
|
||||
}
|
||||
|
||||
var navbarBaseWidth = this.getThemeManager().getParam('navbarBaseWidth') || 556;
|
||||
var navbarBaseWidth = this.getThemeManager().getParam('navbarBaseWidth') || 565;
|
||||
|
||||
var tabCount = this.tabList.length;
|
||||
|
||||
@@ -462,6 +462,8 @@ define('views/site/navbar', 'view', function (Dep) {
|
||||
updateSizeForVertical();
|
||||
|
||||
this.$el.find('.notifications-badge-container').insertAfter(this.$el.find('.quick-create-container'));
|
||||
|
||||
this.adjustBodyMinHeight();
|
||||
},
|
||||
|
||||
getNavbarHeight: function () {
|
||||
@@ -479,7 +481,7 @@ define('views/site/navbar', 'view', function (Dep) {
|
||||
},
|
||||
|
||||
adjustBodyMinHeightVertical: function () {
|
||||
var minHeight = this.$tabs.height() + this.getStaticItemsHeight();
|
||||
var minHeight = this.$tabs.get(0).scrollHeight + this.getStaticItemsHeight();
|
||||
|
||||
var moreHeight = 0;
|
||||
this.$more.find('> li:visible').each(function (i, el) {
|
||||
|
||||
@@ -610,7 +610,7 @@ define('views/stream/panel', ['views/record/panels/relationship', 'lib!Textcompl
|
||||
|
||||
preview: function () {
|
||||
this.createView('dialog', 'views/modal', {
|
||||
templateContent: '<div class="complex-text">{{complexText viewObject.options.text}}</div>',
|
||||
templateContent: '<div class="complex-text">{{complexText viewObject.options.text linksInNewTab=true}}</div>',
|
||||
text: this.$textarea.val(),
|
||||
headerText: this.translate('Preview'),
|
||||
backdrop: true,
|
||||
|
||||
@@ -87,10 +87,10 @@ define('views/user/record/detail', 'views/record/detail', function (Dep) {
|
||||
name: 'generateNewPassword',
|
||||
label: 'Generate New Password',
|
||||
action: 'generateNewPassword',
|
||||
hidden: !this.model.get('emailAddress') || !this.getConfig().get('smtpServer'),
|
||||
hidden: !this.model.get('emailAddress'),
|
||||
});
|
||||
|
||||
if (!this.model.get('emailAddress') && this.getConfig().get('smtpServer')) {
|
||||
if (!this.model.get('emailAddress')) {
|
||||
this.listenTo(this.model, 'sync', function () {
|
||||
if (this.model.get('emailAddress')) {
|
||||
this.showActionItem('generateNewPassword');
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -112,3 +112,56 @@
|
||||
color: @gray-light;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
color: @gray-dark;
|
||||
border-radius: 0;
|
||||
|
||||
.far,
|
||||
.fas {
|
||||
color: @gray-soft;
|
||||
}
|
||||
|
||||
&,
|
||||
&:active,
|
||||
&.active,
|
||||
&[disabled],
|
||||
fieldset[disabled] & {
|
||||
background-color: transparent;
|
||||
.box-shadow(none);
|
||||
}
|
||||
&,
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
border-color: transparent;
|
||||
}
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: @gray-dark;
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
.far,
|
||||
.fas {
|
||||
color: @gray-dark;
|
||||
}
|
||||
}
|
||||
&.disabled,
|
||||
&[disabled],
|
||||
fieldset[disabled] & {
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: @gray-light;
|
||||
text-decoration: none;
|
||||
}
|
||||
color: @gray-light;
|
||||
.far,
|
||||
.fas {
|
||||
color: @gray-light;
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
border-bottom: 1px solid @gray-light;
|
||||
}
|
||||
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
178
frontend/less/espo/elements/modal.less
Normal file
178
frontend/less/espo/elements/modal.less
Normal file
@@ -0,0 +1,178 @@
|
||||
.modal-header.fixed-height {
|
||||
height: 40px;
|
||||
max-height: 40px;
|
||||
overflow: hidden;
|
||||
|
||||
.modal-title {
|
||||
width: ~"calc(100% - 20px)";
|
||||
> .modal-title-text {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&.overlapped {
|
||||
> .modal-title-text {
|
||||
&:before {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
right: 34px;
|
||||
background: linear-gradient(to right, transparent, @modal-header-bg);
|
||||
height: 30px;
|
||||
width: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header a {
|
||||
color: @text-color;
|
||||
}
|
||||
|
||||
.modal-container.overlaid .modal-content {
|
||||
.box-shadow(none);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
background-color: @modal-header-bg;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: @modal-title-padding @panel-padding;
|
||||
border-bottom: 0 solid @modal-header-border-color;
|
||||
min-height: (@modal-title-padding + @modal-title-line-height);
|
||||
}
|
||||
|
||||
.modal-header .close {
|
||||
font-size: 31px;
|
||||
font-weight: normal;
|
||||
margin-top: -4px;
|
||||
max-height: @font-size-h3;
|
||||
> span {
|
||||
position: relative;
|
||||
max-height: @font-size-h3;
|
||||
height: @font-size-h3;
|
||||
display: inline-block;
|
||||
}
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
margin-top: @modal-title-padding;
|
||||
padding: @panel-padding @panel-padding @panel-padding;
|
||||
}
|
||||
|
||||
.modal-footer > .main-btn-group {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.modal-footer > .additional-btn-group {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.modal-dialog > .modal-content {
|
||||
background-clip: border-box;
|
||||
border-color: @default-border-color;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.modal-footer > .main-btn-group {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.modal-footer > .additional-btn-group {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.dialog-confirm .modal-footer > .main-btn-group {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.dialog-confirm .modal-footer > .additional-btn-group {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding-top: @modal-title-padding;
|
||||
padding-bottom: @modal-title-padding;
|
||||
}
|
||||
|
||||
@media (min-width: @screen-sm-min) {
|
||||
.modal-dialog {
|
||||
margin: 0 0 0 auto;
|
||||
}
|
||||
.modal-content .note-editor > .modal > .modal-dialog {
|
||||
margin: 0 0 0 -@container-padding;
|
||||
}
|
||||
|
||||
.dialog-confirm > .modal-dialog {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: @screen-sm-min) {
|
||||
.modal-dialog {
|
||||
margin: 0 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-dialog .modal-body .list-buttons-container .sticked-bar {
|
||||
top: 94px !important;
|
||||
width: 100% !important;
|
||||
right: 0 !important;
|
||||
left: unset;
|
||||
padding-top: 0;
|
||||
padding-left: @container-padding - 1px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: (@screen-sm-min - 1px)) {
|
||||
.modal-dialog {
|
||||
margin: 0 0;
|
||||
}
|
||||
.modal-dialog .image-container {
|
||||
margin-left: -@panel-padding;
|
||||
margin-right: -@panel-padding;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-confirm > .modal-dialog > .modal-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media screen and (min-width: @screen-sm-min) {
|
||||
.modal-dialog {
|
||||
width: 740px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: @screen-md-min) {
|
||||
.modal-dialog,
|
||||
.dialog-centered {
|
||||
width: 900px;
|
||||
}
|
||||
.dialog-confirm > .modal-dialog,
|
||||
.dialog-centered > .modal-dialog {
|
||||
width: 550px;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body > .no-side-margin {
|
||||
margin-left: -@panel-padding;
|
||||
margin-right: -@panel-padding;
|
||||
}
|
||||
|
||||
.modal.in .modal.in {
|
||||
overflow: visible !important;
|
||||
overflow-y: visible !important;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user