Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend

This commit is contained in:
Taras Machyshyn
2019-10-03 17:11:09 +03:00
67 changed files with 1295 additions and 249 deletions
+6 -2
View File
@@ -33,9 +33,13 @@ class I18n extends \Espo\Core\Controllers\Base
{
public function actionRead($params, $data, $request)
{
if ($request->get('default')) {
$default = $request->get('default') === 'true';
return $this->getServiceFactory()->create('Language')->getDataForFrontend($default);
/*if ($request->get('default')) {
return $this->getContainer()->get('defaultLanguage')->getAll();
}
return $this->getContainer()->get('language')->getAll();
return $this->getContainer()->get('language')->getAll();*/
}
}
+1 -5
View File
@@ -39,11 +39,7 @@ class Layout extends \Espo\Core\Controllers\Base
{
public function actionRead($params, $data)
{
$data = $this->getContainer()->get('layout')->get($params['scope'], $params['name']);
if (empty($data)) {
throw new NotFound("Layout " . $params['scope'] . ":" . $params['name'] . ' is not found.');
}
return $data;
return $this->getServiceFactory()->create('Layout')->getForFrontend($params['scope'], $params['name']);
}
public function actionUpdate($params, $data, $request)
+1 -1
View File
@@ -36,7 +36,7 @@ class Metadata extends \Espo\Core\Controllers\Base
public function actionRead($params, $data)
{
return $this->getMetadata()->getAllForFrontend();
return $this->getServiceFactory()->create('Metadata')->getDataForFrontend();
}
public function getActionGet($params, $data, $request)
@@ -97,7 +97,21 @@ class SumRelatedType extends \Espo\Core\Formula\Functions\Base
$selectParams['select'] = [[$foreignLink . '.id', 'foreignId'], 'SUM:' . $field];
$foreignSelectManager->addJoin($foreignLink, $selectParams);
if ($entity->getRelationType($link) === 'hasChildren') {
$foreignSelectManager->addJoin([
$entity->getEntityType(),
$foreignLink,
[
$foreignLink . '.id:' => $foreignLink . 'Id',
'deleted' => false,
$foreignLink . '.id!=' => null,
]
], $selectParams);
$selectParams['whereClause'][] = [$foreignLink . 'Type' => $entity->getEntityType()];
} else {
$foreignSelectManager->addJoin($foreignLink, $selectParams);
}
$selectParams['groupBy'] = [$foreignLink . '.id'];
@@ -0,0 +1,54 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2019 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\PasswordGroup;
use \Espo\ORM\Entity;
use \Espo\Core\Exceptions\Error;
class GenerateType extends \Espo\Core\Formula\Functions\Base
{
protected function init()
{
$this->addDependency('config');
}
public function process(\StdClass $item)
{
$config = $this->getInjection('config');
$length = $config->get('passwordGenerateLength', 10);
$letterCount = $config->get('passwordGenerateLetterCount', 4);
$numberCount = $config->get('passwordGenerateNumberCount', 2);
$password = \Espo\Core\Utils\Util::generatePassword($length, $letterCount, $numberCount, true);
return $password;
}
}
@@ -0,0 +1,60 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2019 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\PasswordGroup;
use \Espo\ORM\Entity;
use \Espo\Core\Exceptions\Error;
class HashType extends \Espo\Core\Formula\Functions\Base
{
protected function init()
{
$this->addDependency('config');
}
public function process(\StdClass $item)
{
$args = $item->value ?? [];
if (!is_array($args)) throw new Error();
if (count($args) < 1)
throw new Error("Formula: password\\hash: no argument.");
$password = $this->evaluate($args[0]);
if (!is_string($password))
throw new Error("Formula: password\\hash: bad argument.");
$passwordHash = new \Espo\Core\Utils\PasswordHash($this->getInjection('config'));
$hash = $passwordHash->hash($password);
return $hash;
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2019 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\Loaders;
class Hasher extends Base
{
public function load()
{
return new \Espo\Core\Utils\Hasher(
$this->getContainer()->get('config')
);
}
}
@@ -3,13 +3,13 @@
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2019 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Copyright (C] 2014-2019 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.
* (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
@@ -23,37 +23,39 @@
* 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,
* 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.
************************************************************************/
return array(
'Subscription' => array(
'fields' => array(
'id' => array(
return [
'Subscription' => [
'fields' => [
'id' => [
'type' => 'id',
'dbType' => 'int',
'len' => '11',
'autoincrement' => true,
),
'entityId' => array(
],
'entityId' => [
'type' => 'varchar',
'len' => '24',
'index' => 'entity',
),
'entityType' => array(
],
'entityType' => [
'type' => 'varchar',
'len' => '100',
'index' => 'entity',
),
'userId' => array(
],
'userId' => [
'type' => 'varchar',
'len' => '24',
'index' => true,
),
),
),
);
],
],
'indexes' => [
'userEntity' => [
'columns' => ['userId', 'entityId', 'entityType'],
],
],
],
];
+49
View File
@@ -0,0 +1,49 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2019 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\Utils;
class Hasher
{
protected $config;
protected $secretKeyParam = 'hashSecretKey';
public function __construct(\Espo\Core\Utils\Config $config)
{
$this->config = $config;
}
public function hash(string $string) : string
{
$secretKey = $this->config->get($this->secretKeyParam) ?? '';
return md5(hash_hmac('sha256', $string, $secretKey, true));
}
}
+8
View File
@@ -568,6 +568,14 @@ class Util
return bin2hex(random_bytes(16));
}
public static function generateSecretKey()
{
if (!function_exists('random_bytes')) {
return self::generateId();
}
return bin2hex(random_bytes(16));
}
public static function generateKey()
{
return md5(uniqid(rand(), true));
@@ -96,6 +96,7 @@ return [
'passwordSalt',
'cryptKey',
'apiSecretKeys',
'hashSecretKey',
'restrictedMode',
'userLimit',
'portalUserLimit',
+3 -3
View File
@@ -35,11 +35,11 @@ class Stream extends \Espo\Core\Hooks\Base
{
protected $streamService = null;
protected $auditedFieldsCache = array();
protected $auditedFieldsCache = [];
protected $hasStreamCache = array();
protected $hasStreamCache = [];
protected $isLinkObservableInStreamCache = array();
protected $isLinkObservableInStreamCache = [];
protected $statusFields = null;
@@ -47,10 +47,19 @@ class SubscribeAgain extends \Espo\Core\EntryPoints\Base
public function run()
{
if (empty($_GET['id'])) {
$id = $_GET['id'] ?? null;
$emailAddress = $_GET['emailAddress'] ?? null;
$hash = $_GET['hash'] ?? null;
if ($emailAddress && $hash) {
$this->processWithHash($emailAddress, $hash);
return;
}
if (!$id) {
throw new BadRequest();
}
$queueItemId = $_GET['id'];
$queueItemId = $id;
$queueItem = $this->getEntityManager()->getEntity('EmailQueueItem', $queueItemId);
@@ -76,6 +85,10 @@ class SubscribeAgain extends \Espo\Core\EntryPoints\Base
if ($targetType && $targetId) {
$target = $this->getEntityManager()->getEntity($targetType, $targetId);
if (!$target) {
throw new NotFound();
}
if ($massEmail->get('optOutEntirely')) {
$emailAddress = $target->get('emailAddress');
if ($emailAddress) {
@@ -92,7 +105,7 @@ class SubscribeAgain extends \Espo\Core\EntryPoints\Base
'Account' => 'accounts',
'Contact' => 'contacts',
'Lead' => 'leads',
'User' => 'users'
'User' => 'users',
];
if (!empty($m[$target->getEntityType()])) {
$link = $m[$target->getEntityType()];
@@ -114,22 +127,9 @@ class SubscribeAgain extends \Espo\Core\EntryPoints\Base
}
}
$data = [
'actionData' => [
'queueItemId' => $queueItemId,
],
'view' => $this->getMetadata()->get(['clientDefs', 'Campaign', 'subscribeView']),
'template' => $this->getMetadata()->get(['clientDefs', 'Campaign', 'subscribeTemplate']),
];
$this->getHookManager()->process($target->getEntityType(), 'afterCancelOptOut', $target, [], []);
$runScript = "
Espo.require('crm:controllers/unsubscribe', function (Controller) {
var controller = new Controller(app.baseController.params, app.getControllerInjection());
controller.masterView = app.masterView;
controller.doAction('subscribeAgain', ".json_encode($data).");
});
";
$this->getClientManager()->display($runScript);
$this->display(['queueItemId' => $queueItemId]);
}
}
}
@@ -147,5 +147,56 @@ class SubscribeAgain extends \Espo\Core\EntryPoints\Base
}
}
}
protected function display(array $actionData)
{
$data = [
'actionData' => $actionData,
'view' => $this->getMetadata()->get(['clientDefs', 'Campaign', 'subscribeView']),
'template' => $this->getMetadata()->get(['clientDefs', 'Campaign', 'subscribeTemplate']),
];
$runScript = "
Espo.require('crm:controllers/unsubscribe', function (Controller) {
var controller = new Controller(app.baseController.params, app.getControllerInjection());
controller.masterView = app.masterView;
controller.doAction('subscribeAgain', ".json_encode($data).");
});
";
$this->getClientManager()->display($runScript);
}
protected function processWithHash(string $emailAddress, string $hash)
{
$secretKey = $this->getConfig()->get('hashSecretKey');
$hash2 = $this->getContainer()->get('hasher')->hash($emailAddress);
if ($hash2 !== $hash) {
throw new NotFound();
}
$repository = $this->getEntityManager()->getRepository('EmailAddress');
$ea = $repository->getByAddress($emailAddress);
if ($ea) {
$entityList = $repository->getEntityListByAddressId($ea->id);
if ($ea->get('optOut')) {
$ea->set('optOut', false);
$this->getEntityManager()->saveEntity($ea);
foreach ($entityList as $entity) {
$this->getHookManager()->process($entity->getEntityType(), 'afterCancelOptOut', $entity, [], []);
}
}
$this->display([
'emailAddress' => $emailAddress,
'hash' => $hash,
]);
} else {
throw new NotFound();
}
}
}
@@ -47,10 +47,19 @@ class Unsubscribe extends \Espo\Core\EntryPoints\Base
public function run()
{
if (empty($_GET['id'])) {
$id = $_GET['id'] ?? null;
$emailAddress = $_GET['emailAddress'] ?? null;
$hash = $_GET['hash'] ?? null;
if ($emailAddress && $hash) {
$this->processWithHash($emailAddress, $hash);
return;
}
if (!$id) {
throw new BadRequest();
}
$queueItemId = $_GET['id'];
$queueItemId = $id;
$queueItem = $this->getEntityManager()->getEntity('EmailQueueItem', $queueItemId);
@@ -76,6 +85,10 @@ class Unsubscribe extends \Espo\Core\EntryPoints\Base
if ($targetType && $targetId) {
$target = $this->getEntityManager()->getEntity($targetType, $targetId);
if (!$target) {
throw new NotFound();
}
if ($massEmail->get('optOutEntirely')) {
$emailAddress = $target->get('emailAddress');
if ($emailAddress) {
@@ -92,7 +105,7 @@ class Unsubscribe extends \Espo\Core\EntryPoints\Base
'Account' => 'accounts',
'Contact' => 'contacts',
'Lead' => 'leads',
'User' => 'users'
'User' => 'users',
];
if (!empty($m[$target->getEntityType()])) {
$link = $m[$target->getEntityType()];
@@ -114,22 +127,9 @@ class Unsubscribe extends \Espo\Core\EntryPoints\Base
}
}
$data = [
'actionData' => [
'queueItemId' => $queueItemId,
],
'view' => $this->getMetadata()->get(['clientDefs', 'Campaign', 'unsubscribeView']),
'template' => $this->getMetadata()->get(['clientDefs', 'Campaign', 'unsubscribeTemplate']),
];
$this->getHookManager()->process($target->getEntityType(), 'afterOptOut', $target, [], []);
$runScript = "
Espo.require('crm:controllers/unsubscribe', function (Controller) {
var controller = new Controller(app.baseController.params, app.getControllerInjection());
controller.masterView = app.masterView;
controller.doAction('unsubscribe', ".json_encode($data).");
});
";
$this->getClientManager()->display($runScript);
$this->display(['queueItemId' => $queueItemId]);
}
}
}
@@ -140,4 +140,56 @@ class Unsubscribe extends \Espo\Core\EntryPoints\Base
$campaignService->logOptedOut($campaignId, $queueItemId, $target, $queueItem->get('emailAddress'), null, $queueItem->get('isTest'));
}
}
protected function display(array $actionData)
{
$data = [
'actionData' => $actionData,
'view' => $this->getMetadata()->get(['clientDefs', 'Campaign', 'unsubscribeView']),
'template' => $this->getMetadata()->get(['clientDefs', 'Campaign', 'unsubscribeTemplate']),
];
$runScript = "
Espo.require('crm:controllers/unsubscribe', function (Controller) {
var controller = new Controller(app.baseController.params, app.getControllerInjection());
controller.masterView = app.masterView;
controller.doAction('unsubscribe', ".json_encode($data).");
});
";
$this->getClientManager()->display($runScript);
}
protected function processWithHash(string $emailAddress, string $hash)
{
$secretKey = $this->getConfig()->get('hashSecretKey');
$hash2 = $this->getContainer()->get('hasher')->hash($emailAddress);
if ($hash2 !== $hash) {
throw new NotFound();
}
$repository = $this->getEntityManager()->getRepository('EmailAddress');
$ea = $repository->getByAddress($emailAddress);
if ($ea) {
$entityList = $repository->getEntityListByAddressId($ea->id);
if (!$ea->get('optOut')) {
$ea->set('optOut', true);
$this->getEntityManager()->saveEntity($ea);
foreach ($entityList as $entity) {
$this->getHookManager()->process($entity->getEntityType(), 'afterOptOut', $entity, [], []);
}
}
$this->display([
'emailAddress' => $emailAddress,
'hash' => $hash,
]);
} else {
throw new NotFound();
}
}
}
@@ -7,34 +7,6 @@
"EmailQueueItem": false
}
},
"default": {
"scopeFieldLevel": {
"KnowledgeBaseArticle": {
"portals": false,
"order": false,
"status": false,
"assignedUser": false
},
"Case": {
"status": {
"read": "yes",
"edit": "no"
},
"account": {
"read": "yes",
"edit": "no"
},
"contacts": {
"read": "yes",
"edit": "no"
},
"contact": {
"read": "yes",
"edit": "no"
}
}
}
},
"strictDefault": {
"scopeFieldLevel": {
"KnowledgeBaseArticle": {
@@ -43,22 +15,27 @@
"status": false,
"assignedUser": false
},
"Call": {
"users": {
"read": "yes",
"edit": "no"
},
"leads": false
},
"Meeting": {
"users": {
"read": "yes",
"edit": "no"
},
"leads": false
},
"KnowledgeBaseArticle": {
"assignedUser": false
},
"Case": {
"status": {
"read": "yes",
"edit": "no"
},
"account": {
"read": "yes",
"edit": "no"
},
"contacts": {
"read": "yes",
"edit": "no"
},
"contact": {
"read": "yes",
"edit": "no"
}
}
}
+10 -5
View File
@@ -74,18 +74,23 @@ class Portal extends \Espo\Core\ORM\Repositories\RDB
}
}
protected function afterSave(Entity $entity, array $options = array())
protected function afterSave(Entity $entity, array $options = [])
{
parent::afterSave($entity, $options);
if ($entity->has('isDefault')) {
if ($entity->get('isDefault')) {
$this->getConfig()->set('defaultPortalId', $entity->id);
$this->getConfig()->save();
$defaultPortalId = $this->getConfig()->get('defaultPortalId');
if ($defaultPortalId !== $entity->id) {
$this->getConfig()->set('defaultPortalId', $entity->id);
$this->getConfig()->save();
}
} else {
if ($entity->isAttributeChanged('isDefault')) {
$this->getConfig()->set('defaultPortalId', null);
$this->getConfig()->save();
if ($entity->getFetched('isDefault')) {
$this->getConfig()->set('defaultPortalId', null);
$this->getConfig()->save();
}
}
}
}
@@ -0,0 +1,28 @@
[
{
"label":"",
"rows":[
[{"name":"name"}, {"name": "status"}],
[{"name":"queue"}, {"name":"number"}]
]
},
{
"label":"",
"rows":[
[{"name":"executeTime"}, {"name": "createdAt"}],
[{"name":"startedAt"}, {"name": "modifiedAt"}],
[{"name":"executedAt"}, false],
[{"name":"attempts"}, false],
[{"name":"failedAttempts"}, false]
]
},
{
"label":"",
"rows":[
[{"name":"scheduledJob"}, {"name":"targetType"}],
[{"name":"serviceName"}, {"name":"targetId"}],
[{"name":"methodName"}, {"name":"job"}],
[{"name": "data", "fullWidth": true}]
]
}
]
@@ -2,8 +2,7 @@
{
"label": "In-app Notifications",
"rows": [
[{"name": "assignmentNotificationsEntityList"}, false],
[{"name": "notificationSoundsDisabled"}, false]
[{"name": "assignmentNotificationsEntityList"}, false]
]
},
{
@@ -105,6 +105,9 @@
"read": "own",
"edit": "no"
},
"Team": {
"read": "team"
},
"Import": false,
"Webhook": false
},
@@ -100,8 +100,11 @@
"type": false,
"contact": false,
"accounts": false,
"account": false,
"portalRoles": false,
"portals": false,
"roles": false,
"defaultTeam": false,
"isActive": false
}
}
@@ -121,23 +124,6 @@
"teams": false
},
"scopeFieldLevel": {
"Call": {
"users": {
"read": "yes",
"edit": "no"
},
"leads": false
},
"Meeting": {
"users": {
"read": "yes",
"edit": "no"
},
"leads": false
},
"KnowledgeBaseArticle": {
"assignedUser": false
},
"User": {
"gender": false
}
@@ -16,6 +16,9 @@
"internal": true,
"nonAdminReadOnly": true
},
"authLogRecordId": {
"internal": true
},
"authMethod": {
"onlyAdmin": true
},
@@ -104,7 +104,7 @@
"collection": {
"orderBy": "number",
"order": "desc",
"textFilterFields": ["name", "methodName", "serviceName", "scheduledJob"]
"textFilterFields": ["id", "name", "methodName", "serviceName", "scheduledJob"]
},
"indexes": {
"executeTime": {
@@ -155,6 +155,9 @@
"parentAndSuperParent": {
"type": "index",
"columns": ["parentId", "parentType", "superParentId", "superParentType"]
},
"createdByNumber": {
"columns": ["createdById", "number"]
}
}
}
@@ -85,8 +85,7 @@
},
"dashboardLayout": {
"type": "jsonArray",
"view": "views/settings/fields/dashboard-layout",
"required": true
"view": "views/settings/fields/dashboard-layout"
},
"dashletsOptions": {
"type": "jsonObject",
@@ -0,0 +1,7 @@
{
"entity": true,
"layouts": false,
"tab": false,
"acl": false,
"customizable": false
}
@@ -1 +1,7 @@
{"entity":true,"layouts":false,"tab":false,"acl":false,"customizable":false}
{
"entity": true,
"layouts": false,
"tab": false,
"acl": false,
"customizable": false
}
+4 -5
View File
@@ -52,8 +52,10 @@ class Email extends \Espo\Core\SelectManagers\Base
break;
} else {
if (isset($item['attribute'])) {
$skipIndex = true;
break;
if (!in_array($item['attribute'], ['teams', 'users', 'status'])) {
$skipIndex = true;
break;
}
}
}
}
@@ -61,9 +63,6 @@ class Email extends \Espo\Core\SelectManagers\Base
if ($folderId === 'important' || $folderId === 'drafts') {
$skipIndex = true;
}
if (!$skipIndex && $this->hasLinkJoined('teams', $result)) {
$skipIndex = true;
}
if (!$skipIndex) {
$result['useIndexList'] = ['dateSent'];
}
+5 -1
View File
@@ -43,6 +43,10 @@ class Team extends \Espo\Core\SelectManagers\Base
protected function accessOnlyTeam(&$result)
{
$this->boolFilterOnlyMy($result);
$this->setDistinct(true, $result);
$this->addLeftJoin(['users', 'usersOnlyMyAccess'], $result);
$result['whereClause'][] = [
'usersOnlyMyAccessMiddle.userId' => $this->getUser()->id
];
}
}
+56 -13
View File
@@ -75,6 +75,7 @@ class App extends \Espo\Core\Services\Base
$settingsService = $this->getServiceFactory()->create('Settings');
$user = $this->getUser();
if (!$user->has('teamsIds')) {
$user->loadLinkMultipleField('teams');
}
@@ -83,13 +84,6 @@ class App extends \Espo\Core\Services\Base
$user->loadLinkMultipleField('accounts');
}
$userData = $user->getValueMap();
$emailAddressData = $this->getEmailAddressData();
$userData->emailAddressList = $emailAddressData->emailAddressList;
$userData->userEmailAddressList = $emailAddressData->userEmailAddressList;
$settings = $this->getServiceFactory()->create('Settings')->getConfigData();
if ($user->get('dashboardTemplateId')) {
@@ -100,14 +94,11 @@ class App extends \Espo\Core\Services\Base
}
}
unset($userData->authTokenId);
unset($userData->password);
$language = \Espo\Core\Utils\Language::detectLanguage($this->getConfig(), $this->getPreferences());
return [
'user' => $userData,
'acl' => $this->getAcl()->getMap(),
'user' => $this->getUserDataForFrontend(),
'acl' => $this->getAclDataForFrontend(),
'preferences' => $preferencesData,
'token' => $this->getUser()->get('token'),
'settings' => $settings,
@@ -116,11 +107,63 @@ class App extends \Espo\Core\Services\Base
'maxUploadSize' => $this->getMaxUploadSize() / 1024.0 / 1024.0,
'templateEntityTypeList' => $this->getTemplateEntityTypeList(),
'isRestrictedMode' => $this->getConfig()->get('restrictedMode'),
'passwordChangeForNonAdminDisabled' => $this->getConfig()->get('authenticationMethod', 'Espo') !== 'Espo'
'passwordChangeForNonAdminDisabled' => $this->getConfig()->get('authenticationMethod', 'Espo') !== 'Espo',
'timeZoneList' => $this->getMetadata()->get(['entityDefs', 'Settings', 'fields', 'timeZone', 'options'], []),
]
];
}
protected function getUserDataForFrontend()
{
$user = $this->getUser();
$emailAddressData = $this->getEmailAddressData();
$data = $user->getValueMap();
$data->emailAddressList = $emailAddressData->emailAddressList;
$data->userEmailAddressList = $emailAddressData->userEmailAddressList;
unset($data->authTokenId);
unset($data->password);
$forbiddenAttributeList = $this->getAcl()->getScopeForbiddenAttributeList('User');
$isPortal = $user->isPortal();
foreach ($forbiddenAttributeList as $attribute) {
if ($attribute === 'type') continue;
if ($isPortal) {
if (in_array($attribute, ['contactId', 'contactName', 'accountId', 'accountsIds'])) continue;
} else {
if (in_array($attribute, ['teamsIds', 'defaultTeamId', 'defaultTeamName'])) continue;
}
unset($data->$attribute);
}
return $data;
}
protected function getAclDataForFrontend()
{
$data = $this->getAcl()->getMap();
if (!$this->getUser()->isAdmin()) {
$data = unserialize(serialize($data));
$scopeList = array_keys($this->getMetadata()->get(['scopes'], []));
foreach ($scopeList as $scope) {
if (!$this->getAcl()->check($scope)) {
unset($data->table->$scope);
unset($data->fieldTable->$scope);
unset($data->fieldTableQuickAccess->$scope);
}
}
}
return $data;
}
protected function getEmailAddressData()
{
$user = $this->getUser();
+140
View File
@@ -0,0 +1,140 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2019 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\Services;
class Language extends \Espo\Core\Services\Base
{
protected function init()
{
$this->addDependency('container');
$this->addDependency('metadata');
$this->addDependency('acl');
}
protected function getMetadata()
{
return $this->getInjection('metadata');
}
protected function getAcl()
{
return $this->getInjection('acl');
}
protected function getDefaultLanguage()
{
return $this->getInjection('container')->get('defaultLanguage');
}
protected function getLanguage()
{
return $this->getInjection('container')->get('language');
}
public function getDataForFrontend(bool $default = false)
{
if ($default) {
$languageObj = $this->getDefaultLanguage();
} else {
$languageObj = $this->getLanguage();
}
$data = $languageObj->getAll();
if ($this->getUser()->isSystem()) {
unset($data['Global']['scopeNames']);
unset($data['Global']['scopeNamesPlural']);
unset($data['Global']['dashlets']);
unset($data['Global']['links']);
unset($data['Global']['fields']);
unset($data['Global']['options']);
foreach ($data as $k => $item) {
if (in_array($k, ['Global', 'User', 'Campaign'])) continue;
unset($data[$k]);
}
unset($data['User']['fields']);
unset($data['User']['links']);
unset($data['User']['options']);
unset($data['User']['filters']);
unset($data['User']['presetFilters']);
unset($data['User']['boolFilters']);
unset($data['User']['tooltips']);
unset($data['Campaign']['fields']);
unset($data['Campaign']['links']);
unset($data['Campaign']['options']);
unset($data['Campaign']['tooltips']);
unset($data['Campaign']['presetFilters']);
} else {
$scopeList = array_keys($this->getMetadata()->get(['scopes'], []));
foreach ($scopeList as $scope) {
if (!$this->getAcl()->check($scope)) {
unset($data[$scope]);
unset($data['Global']['scopeNames'][$scope]);
unset($data['Global']['scopeNamesPlural'][$scope]);
} else {
foreach ($this->getAcl()->getScopeForbiddenFieldList($scope) as $field) {
if (isset($data[$scope]['fields'])) unset($data[$scope]['fields'][$field]);
if (isset($data[$scope]['options'])) unset($data[$scope]['options'][$field]);
if (isset($data[$scope]['links'])) unset($data[$scope]['links'][$field]);
}
}
}
if (!$this->getUser()->isAdmin()) {
unset($data['Admin']);
unset($data['LayoutManager']);
unset($data['EntityManager']);
unset($data['FieldManager']);
unset($data['Settings']);
unset($data['ApiUser']);
unset($data['DynamicLogic']);
$data['Settings'] = [
'options' => [
'weekStart' => $languageObj->get(['Settings', 'options', 'weekStart']),
],
];
$data['Admin'] = [
'messages' => [
'userHasNoEmailAddress' => $languageObj->translate('userHasNoEmailAddress', 'messages', 'Admin'),
],
];
}
$data['User']['fields']['password'] = $languageObj->translate('password', 'fields', 'User');
$data['User']['fields']['passwordConfirm'] = $languageObj->translate('passwordConfirm', 'fields', 'User');
}
return $data;
}
}
+79
View File
@@ -0,0 +1,79 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2019 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\Services;
class Layout extends \Espo\Core\Services\Base
{
protected function init()
{
$this->addDependency('acl');
$this->addDependency('layout');
$this->addDependency('metadata');
}
protected function getAcl()
{
return $this->getInjection('acl');
}
protected function getMetadata()
{
return $this->getInjection('metadata');
}
public function getForFrontend(string $scope, string $name)
{
$dataString = $this->getInjection('layout')->get($scope, $name);
if (!$dataString) {
throw new NotFound("Layout {$scope}:{$scope} is not found.");
}
if (!$this->getUser()->isAdmin()) {
if ($name === 'relationships') {
$data = json_decode($dataString);
if (is_array($data)) {
foreach ($data as $i => $link) {
$foreignEntityType = $this->getMetadata()->get(['entityDefs', $scope, 'links', $link, 'entity']);
if ($foreignEntityType) {
if (!$this->getAcl()->check($foreignEntityType)) {
unset($data[$i]);
}
}
}
$data = array_values($data);
$dataString = json_encode($data);
}
}
}
return $dataString;
}
}
+149
View File
@@ -0,0 +1,149 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2019 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\Services;
class Metadata extends \Espo\Core\Services\Base
{
protected function init()
{
$this->addDependency('metadata');
$this->addDependency('acl');
}
protected function getMetadata()
{
return $this->getInjection('metadata');
}
protected function getAcl()
{
return $this->getInjection('acl');
}
protected function getDefaultLanguage()
{
return $this->getInjection('container')->get('defaultLanguage');
}
protected function getLanguage()
{
return $this->getInjection('container')->get('language');
}
public function getDataForFrontend()
{
$data = $this->getMetadata()->getAllForFrontend();
if (!$this->getUser()->isAdmin()) {
$scopeList = array_keys($this->getMetadata()->get(['scopes'], []));
foreach ($scopeList as $scope) {
if (!$this->getAcl()->check($scope)) {
unset($data->entityDefs->$scope);
unset($data->clientDefs->$scope);
unset($data->entityAcl->$scope);
unset($data->scopes->$scope);
}
}
$entityTypeList = array_keys(get_object_vars($data->entityDefs));
foreach ($entityTypeList as $entityType) {
$linksDefs = $this->getMetadata()->get(['entityDefs', $entityType, 'links'], []);
$fobiddenFieldList = $this->getAcl()->getScopeForbiddenFieldList($entityType);
foreach ($linksDefs as $link => $defs) {
$type = $defs['type'] ?? null;
$hasField = !!$this->getMetadata()->get(['entityDefs', $entityType, 'fields', $link]);
if ($type === 'belongsToParent') {
if ($hasField) {
$parentEntityList = $this->getMetadata()->get(['entityDefs', $entityType, 'fields', $link, 'entityList']);
if (is_array($parentEntityList)) {
foreach ($parentEntityList as $i => $e) {
if (!$this->getAcl()->check($e)) {
unset($parentEntityList[$i]);
}
}
$parentEntityList = array_values($parentEntityList);
$data->entityDefs->$entityType->fields->$link->entityList = $parentEntityList;
}
}
continue;
}
$foreignEntityType = $defs['entity'] ?? null;
if ($this->getAcl()->check($foreignEntityType)) continue;
if ($this->getUser()->isPortal()) {
if ($foreignEntityType === 'Account' || $foreignEntityType === 'Contact') {
continue;
}
}
if ($hasField) {
if (!in_array($link, $fobiddenFieldList)) {
continue;
}
unset($data->entityDefs->$entityType->fields->$link);
}
unset($data->entityDefs->$entityType->links->$link);
if (
isset($data->clientDefs)
&&
isset($data->clientDefs->$entityType)
&&
isset($data->clientDefs->$entityType->relationshipPanels)
) {
unset($data->clientDefs->$entityType->relationshipPanels->$link);
}
}
}
unset($data->entityDefs->Settings);
$dashletList = array_keys($this->getMetadata()->get(['dashlets'], []));
foreach ($dashletList as $item) {
$aclScope = $this->getMetadata()->get(['dashlets', $item, 'aclScope']);
if ($aclScope && !$this->getAcl()->check($aclScope)) {
unset($data->dashlets->$item);
}
}
unset($data->authenticationMethods);
unset($data->formula);
}
return $data;
}
}
+17 -1
View File
@@ -35,6 +35,13 @@ class Portal extends Record
{
protected $getEntityBeforeUpdate = true;
protected function init()
{
parent::init();
$this->addDependency('fileManager');
$this->addDependency('dataManager');
}
public function loadAdditionalFields(Entity $entity)
{
parent::loadAdditionalFields($entity);
@@ -50,11 +57,20 @@ class Portal extends Record
protected function afterUpdateEntity(Entity $entity, $data)
{
$this->loadUrlField($entity);
if (property_exists($data, 'portalRolesIds')) {
$this->clearRolesCache();
}
}
protected function loadUrlField(Entity $entity)
{
$this->getRepository()->loadUrlField($entity);
}
}
protected function clearRolesCache()
{
$this->getInjection('fileManager')->removeInDir('data/cache/application/acl-portal');
$this->getInjection('dataManager')->updateCacheTimestamp();
}
}
+2 -1
View File
@@ -37,6 +37,7 @@ class PortalRole extends Record
{
parent::init();
$this->addDependency('fileManager');
$this->addDependency('dataManager');
}
protected $forceSelectAllAttributes = true;
@@ -56,6 +57,6 @@ class PortalRole extends Record
protected function clearRolesCache()
{
$this->getInjection('fileManager')->removeInDir('data/cache/application/acl-portal');
$this->getInjection('dataManager')->updateCacheTimestamp();
}
}
+3 -2
View File
@@ -558,7 +558,6 @@ class Record extends \Espo\Core\Services\Base
$mandatoryValidationList = $this->getMetadata()->get(['fields', $fieldType, 'mandatoryValidationList'], []);
$fieldValidatorManager = $this->getInjection('container')->get('fieldValidatorManager');
foreach ($validationList as $type) {
$value = $this->getFieldManagerUtil()->getEntityTypeFieldParam($this->entityType, $field, $type);
if (is_null($value)) {
@@ -570,7 +569,7 @@ class Record extends \Espo\Core\Services\Base
$skipPropertyName = 'validate' . ucfirst($type) . 'SkipFieldList';
if (property_exists($this, $skipPropertyName)) {
$skipList = $this->$skipPropertyName;
if (!in_array($type, $skipList)) {
if (in_array($type, $skipList)) {
continue;
}
}
@@ -1857,6 +1856,7 @@ class Record extends \Espo\Core\Services\Base
public function follow($id, $userId = null)
{
$entity = $this->getRepository()->get($id);
if (!$entity) throw new NotFoundSilent();
if (!$this->getAcl()->check($entity, 'stream')) {
throw new Forbidden();
@@ -1872,6 +1872,7 @@ class Record extends \Espo\Core\Services\Base
public function unfollow($id, $userId = null)
{
$entity = $this->getRepository()->get($id);
if (!$entity) throw new NotFoundSilent();
if (empty($userId)) {
$userId = $this->getUser()->id;
+2
View File
@@ -37,6 +37,7 @@ class Role extends Record
{
parent::init();
$this->addDependency('fileManager');
$this->addDependency('dataManager');
}
protected $forceSelectAllAttributes = true;
@@ -56,5 +57,6 @@ class Role extends Record
protected function clearRolesCache()
{
$this->getInjection('fileManager')->removeInDir('data/cache/application/acl');
$this->getInjection('dataManager')->updateCacheTimestamp();
}
}
+35 -1
View File
@@ -111,10 +111,44 @@ class Settings extends \Espo\Core\Services\Base
}
}
if (!$this->getUser()->isAdmin() && !$this->getUser()->isSystem()) {
$entityTypeListParamList = [
'tabList',
'quickCreateList',
'globalSearchEntityList',
'assignmentEmailNotificationsEntityList',
'assignmentNotificationsEntityList',
'calendarEntityList',
'disabledCountQueryEntityList',
'streamEmailNotificationsEntityList',
'activitiesEntityList',
'historyEntityList',
'streamEmailNotificationsTypeList',
'emailKeepParentTeamsEntityList',
];
$scopeList = array_keys($this->getMetadata()->get(['entityDefs'], []));
foreach ($scopeList as $scope) {
if (!$this->getMetadata()->get(['scopes', $scope, 'acl'])) continue;
if (!$this->getAcl()->check($scope)) {
foreach ($entityTypeListParamList as $param) {
$list = $data->$param ?? [];
foreach ($list as $i => $item) {
if ($item === $scope) {
unset($list[$i]);
}
}
$list = array_values($list);
$data->$param = $list;
}
}
}
}
if (
($this->getConfig()->get('smtpServer') || $this->getConfig()->get('internalSmtpServer'))
&&
!$this->getConfig()->geT('passwordRecoveryDisabled')
!$this->getConfig()->get('passwordRecoveryDisabled')
) {
$data->passwordRecoveryEnabled = true;
}
+47 -10
View File
@@ -61,7 +61,7 @@ class Stream extends \Espo\Core\Services\Base
protected $emailsWithContentEntityList = ['Case'];
protected $auditedFieldsCache = array();
protected $auditedFieldsCache = [];
private $notificationService = null;
@@ -106,7 +106,7 @@ class Stream extends \Espo\Core\Services\Base
protected function getStatusStyles()
{
if (empty($this->statusStyles)) {
$this->statusStyles = $this->getMetadata()->get('entityDefs.Note.statusStyles', array());
$this->statusStyles = $this->getMetadata()->get('entityDefs.Note.statusStyles', []);
}
return $this->statusStyles;
}
@@ -115,7 +115,7 @@ class Stream extends \Espo\Core\Services\Base
{
if (is_null($this->statusFields)) {
$this->statusFields = array();
$scopes = $this->getMetadata()->get('scopes', array());
$scopes = $this->getMetadata()->get('scopes', []);
foreach ($scopes as $scope => $data) {
if (empty($data['statusField'])) continue;
$this->statusFields[$scope] = $data['statusField'];
@@ -143,7 +143,8 @@ class Stream extends \Espo\Core\Services\Base
foreach ($userIdList as $i => $userId) {
$user = $this->getEntityManager()->getEntity('User', $userId);
if (!$user){
if (!$user) {
unset($userIdList[$i]);
continue;
}
if (!$this->getAclManager()->check($user, $entity, 'stream')) {
@@ -165,10 +166,10 @@ class Stream extends \Espo\Core\Services\Base
$this->followEntityMass($entity, $userIdList);
$noteList = $this->getEntityManager()->getRepository('Note')->where(array(
$noteList = $this->getEntityManager()->getRepository('Note')->where([
'parentType' => $entityType,
'parentId' => $entityId
))->order('number', 'ASC')->find();
])->order('number', 'ASC')->find();
foreach ($noteList as $note) {
$this->getNotificationService()->notifyAboutNote($userIdList, $note);
@@ -197,9 +198,9 @@ class Stream extends \Espo\Core\Services\Base
return false;
}
public function followEntityMass(Entity $entity, array $sourceUserIdList)
public function followEntityMass(Entity $entity, array $sourceUserIdList, bool $skipAclCheck = false)
{
if (!$this->getMetadata()->get('scopes.' . $entity->getEntityName() . '.stream')) {
if (!$this->getMetadata()->get(['scopes', $entity->getEntityType(), 'stream'])) {
return false;
}
@@ -213,6 +214,27 @@ class Stream extends \Espo\Core\Services\Base
$userIdList = array_unique($userIdList);
if (!$skipAclCheck) {
foreach ($userIdList as $i => $userId) {
$user = $this->getEntityManager()->getRepository('User')
->select(['id', 'type', 'isActive'])
->where([
'id' => $userId,
'isActive' => true,
])->findOne();
if (!$user) {
unset($userIdList[$i]);
continue;
}
if (!$this->getAclManager()->check($user, $entity, 'stream')) {
unset($userIdList[$i]);
}
}
$userIdList = array_values($userIdList);
}
if (empty($userIdList)) {
return;
}
@@ -364,6 +386,7 @@ class Stream extends \Espo\Core\Services\Base
'orderBy' => 'number',
'order' => 'DESC',
'limit' => $sqLimit,
'useIndexList' => ['createdByNumber'],
];
if ($user->isPortal()) {
@@ -461,6 +484,7 @@ class Stream extends \Espo\Core\Services\Base
'orderBy' => 'number',
'order' => 'DESC',
'limit' => $sqLimit,
'useIndexList' => ['createdByNumber'],
];
if ($user->isPortal()) {
@@ -1638,17 +1662,30 @@ class Stream extends \Espo\Core\Services\Base
{
$ignoreScopeList = [];
$scopes = $this->getMetadata()->get('scopes', []);
$aclManager = $this->getAclManager();
if ($user->isPortal() && !$this->getUser()->isPortal()) {
$aclManager = new \Espo\Core\Portal\AclManager($this->getInjection('container'));
$portals = $user->get('portals');
if (count($portals)) {
$aclManager->setPortal($portals[0]);
}
}
foreach ($scopes as $scope => $item) {
if (empty($item['entity'])) continue;
if (empty($item['object'])) continue;
if (
!$this->getAclManager()->checkScope($user, $scope, 'read')
!$aclManager->checkScope($user, $scope, 'read')
||
!$this->getAclManager()->checkScope($user, $scope, 'stream')
!$aclManager->checkScope($user, $scope, 'stream')
) {
$ignoreScopeList[] = $scope;
}
}
return $ignoreScopeList;
}
+44 -2
View File
@@ -751,10 +751,28 @@ class User extends Record
{
parent::afterUpdateEntity($entity, $data);
if (property_exists($data, 'rolesIds') || property_exists($data, 'teamsIds') || property_exists($data, 'type')) {
if (
property_exists($data, 'rolesIds') ||
property_exists($data, 'teamsIds') ||
property_exists($data, 'type') ||
property_exists($data, 'portalRolesIds') ||
property_exists($data, 'portalsIds')
) {
$this->clearRoleCache($entity->id);
}
if (
property_exists($data, 'portalRolesIds')
||
property_exists($data, 'portalsIds')
||
property_exists($data, 'contactId')
||
property_exists($data, 'accountsIds')
) {
$this->clearPortalRolesCache();
}
if ($entity->isPortal() && $entity->get('contactId')) {
if (property_exists($data, 'firstName') || property_exists($data, 'lastName') || property_exists($data, 'salutationName')) {
$contact = $this->getEntityManager()->getEntity('Contact', $entity->get('contactId'));
@@ -775,6 +793,12 @@ class User extends Record
protected function clearRoleCache($id)
{
$this->getFileManager()->removeFile('data/cache/application/acl/' . $id . '.php');
$this->getContainer()->get('dataManager')->updateCacheTimestamp();
}
protected function clearPortalRolesCache()
{
$this->getInjection('fileManager')->removeInDir('data/cache/application/acl-portal');
}
public function massUpdate(array $params, $data)
@@ -792,11 +816,29 @@ class User extends Record
{
parent::afterMassUpdate($idList, $data);
if (array_key_exists('rolesIds', $data) || array_key_exists('teamsIds', $data) || array_key_exists('type', $data)) {
if (
property_exists($data, 'rolesIds') ||
property_exists($data, 'teamsIds') ||
property_exists($data, 'type') ||
property_exists($data, 'portalRolesIds') ||
property_exists($data, 'portalsIds')
) {
foreach ($idList as $id) {
$this->clearRoleCache($id);
}
}
if (
property_exists($data, 'portalRolesIds')
||
property_exists($data, 'portalsIds')
||
property_exists($data, 'contactId')
||
property_exists($data, 'accountsIds')
) {
$this->clearPortalRolesCache();
}
}
public function loadAdditionalFields(Entity $entity)
+44 -42
View File
@@ -1,5 +1,5 @@
/*!
* Bootstrap Colorpicker v2.5.1
* Bootstrap Colorpicker v2.5.2
* https://itsjavi.com/bootstrap-colorpicker/
*
* Originally written by (c) 2012 Stefan Petre
@@ -38,14 +38,9 @@
val, predefinedColors, fallbackColor, fallbackFormat, hexNumberSignPrefix) {
this.fallbackValue = fallbackColor ?
(
fallbackColor && (typeof fallbackColor.h !== 'undefined') ?
fallbackColor :
this.value = {
h: 0,
s: 0,
b: 0,
a: 1
}
(typeof fallbackColor === 'string') ?
this.parse(fallbackColor) :
fallbackColor
) :
null;
@@ -480,6 +475,9 @@
* @returns {Object} Object containing h,s,b,a,format properties or FALSE if failed to parse
*/
parse: function(strVal) {
if (typeof strVal !== 'string') {
return this.fallbackValue;
}
if (arguments.length === 0) {
return false;
}
@@ -793,6 +791,8 @@
this.updateData(this.color);
}
this.disabled = false;
// Setup picker
var $picker = this.picker = $(this.options.template);
if (this.options.customClass) {
@@ -861,7 +861,7 @@
'keyup.colorpicker': $.proxy(this.keyup, this)
});
this.input.on({
'change.colorpicker': $.proxy(this.change, this)
'input.colorpicker': $.proxy(this.change, this)
});
if (this.component === false) {
this.element.on({
@@ -1105,34 +1105,31 @@
return (this.input !== false);
},
isDisabled: function() {
if (this.hasInput()) {
return (this.input.prop('disabled') === true);
}
return false;
return this.disabled;
},
disable: function() {
if (this.hasInput()) {
this.input.prop('disabled', true);
this.element.trigger({
type: 'disable',
color: this.color,
value: this.getValue()
});
return true;
}
return false;
this.disabled = true;
this.element.trigger({
type: 'disable',
color: this.color,
value: this.getValue()
});
return true;
},
enable: function() {
if (this.hasInput()) {
this.input.prop('disabled', false);
this.element.trigger({
type: 'enable',
color: this.color,
value: this.getValue()
});
return true;
}
return false;
this.disabled = false;
this.element.trigger({
type: 'enable',
color: this.color,
value: this.getValue()
});
return true;
},
currentSlider: null,
mousePointer: {
@@ -1251,7 +1248,24 @@
return false;
},
change: function(e) {
this.keyup(e);
this.color = this.createColor(this.input.val());
// Change format dynamically
// Only occurs if user choose the dynamic format by
// setting option format to false
if (this.color.origFormat && this.options.format === false) {
this.format = this.color.origFormat;
}
if (this.getValue(false) !== false) {
this.updateData();
this.updateComponent();
this.updatePicker();
}
this.element.trigger({
type: 'changeColor',
color: this.color,
value: this.input.val()
});
},
keyup: function(e) {
if ((e.keyCode === 38)) {
@@ -1264,20 +1278,8 @@
this.color.value.a = Math.round((this.color.value.a - 0.01) * 100) / 100;
}
this.update(true);
} else {
this.color = this.createColor(this.input.val());
// Change format dynamically
// Only occurs if user choose the dynamic format by
// setting option format to false
if (this.color.origFormat && this.options.format === false) {
this.format = this.color.origFormat;
}
if (this.getValue(false) !== false) {
this.updateData();
this.updateComponent();
this.updatePicker();
}
}
this.element.trigger({
type: 'changeColor',
color: this.color,
@@ -6,7 +6,7 @@
{{translate 'subscribedAgain' category='messages' scope='Campaign'}}
</p>
<p>
<a class="btn btn-default btn-sm" href="?entryPoint=unsubscribe&id={{actionData.queueItemId}}">{{translate 'Unsubscribe again' scope='Campaign'}}</a>
<a class="btn btn-default btn-sm" href="{{revertUrl}}">{{translate 'Unsubscribe again' scope='Campaign'}}</a>
</p>
</div>
</div>
@@ -6,7 +6,7 @@
{{translate 'unsubscribed' category='messages' scope='Campaign'}}
</p>
<p>
<a class="btn btn-default btn-sm" href="?entryPoint=subscribeAgain&id={{actionData.queueItemId}}">{{translate 'Subscribe again' scope='Campaign'}}</a>
<a class="btn btn-default btn-sm" href="{{revertUrl}}">{{translate 'Subscribe again' scope='Campaign'}}</a>
</p>
</div>
</div>
@@ -149,6 +149,13 @@ Espo.define('crm:views/calendar/calendar', ['view', 'lib!full-calendar'], functi
this.enabledScopeList = [];
}
this.enabledScopeList.forEach(function (item) {
var color = this.getMetadata().get(['clientDefs', item, 'color']);
if (color) {
this.colors[item] = color;
}
}, this);
if (this.header) {
this.createView('modeButtons', 'crm:views/calendar/mode-buttons', {
el: this.getSelector() + ' .mode-buttons',
@@ -170,6 +170,13 @@ Espo.define('crm:views/calendar/timeline', ['view', 'lib!vis'], function (Dep, V
this.enabledScopeList = [];
}
this.enabledScopeList.forEach(function (item) {
var color = this.getMetadata().get(['clientDefs', item, 'color']);
if (color) {
this.colors[item] = color;
}
}, this);
if (this.options.calendarType) {
this.calendarType = this.options.calendarType;
} else {
@@ -26,15 +26,23 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
Espo.define('crm:views/campaign/subscribe-again', 'view', function (Dep) {
define('crm:views/campaign/subscribe-again', 'view', function (Dep) {
return Dep.extend({
template: 'crm:campaign/subscribe-again',
data: function () {
var revertUrl;
var actionData = this.options.actionData;
if (actionData.hash && actionData.emailAddress)
revertUrl = '?entryPoint=unsubscribe&emailAddress=' + actionData.emailAddress + '&hash=' + actionData.hash;
else
revertUrl = '?entryPoint=unsubscribe&id=' + actionData.queueItemId;
var data = {
actionData: this.options.actionData
revertUrl: revertUrl,
};
return data;
}
@@ -26,15 +26,23 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
Espo.define('crm:views/campaign/unsubscribe', 'view', function (Dep) {
define('crm:views/campaign/unsubscribe', 'view', function (Dep) {
return Dep.extend({
template: 'crm:campaign/unsubscribe',
data: function () {
var revertUrl;
var actionData = this.options.actionData;
if (actionData.hash && actionData.emailAddress)
revertUrl = '?entryPoint=subscribeAgain&emailAddress=' + actionData.emailAddress + '&hash=' + actionData.hash;
else
revertUrl = '?entryPoint=subscribeAgain&id=' + actionData.queueItemId;
var data = {
actionData: this.options.actionData
revertUrl: revertUrl,
};
return data;
}
@@ -26,7 +26,7 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
Espo.define('crm:views/mass-email/fields/smtp-account', 'views/fields/enum', function (Dep) {
define('crm:views/mass-email/fields/smtp-account', 'views/fields/enum', function (Dep) {
return Dep.extend({
@@ -36,6 +36,15 @@ Espo.define('crm:views/mass-email/fields/smtp-account', 'views/fields/enum', fun
return [this.name, 'inboundEmailId'];
},
data: function () {
var data = Dep.prototype.data.call(this);
data.valueIsSet = true;
data.isNotEmpty = true;
return data;
},
setupOptions: function () {
Dep.prototype.setupOptions.call(this);
@@ -118,7 +127,7 @@ Espo.define('crm:views/mass-email/fields/smtp-account', 'views/fields/enum', fun
}
return data;
}
},
});
});
@@ -1,4 +1,8 @@
{{#if isEmpty}}
{{translate 'None'}}
{{/if}}
<div class="button-container clearfix">
{{#ifNotEqual dashboardLayout.length 1}}
<div class="btn-group pull-right dashboard-tabs">
+1 -1
View File
@@ -57,7 +57,7 @@ define('acl-portal', ['acl'], function (Dep) {
return true;
}
if (data === null) {
return true;
return false;
}
action = action || null;
+2 -2
View File
@@ -62,13 +62,13 @@ define('acl', [], function () {
return true;
}
if (data === null) {
return true;
return false;
}
action = action || null;
if (action === null) {
return true
return true;
}
if (!(action in data)) {
return false;
+1 -1
View File
@@ -225,7 +225,7 @@ define(
this.fieldManager.defs = this.metadata.get('fields');
this.fieldManager.metadata = this.metadata;
this.settings.defs = this.metadata.get('entityDefs.Settings');
this.settings.defs = this.metadata.get('entityDefs.Settings') || {};
this.user.defs = this.metadata.get('entityDefs.User');
this.preferences.defs = this.metadata.get('entityDefs.Preferences');
this.viewHelper.layoutManager.userId = this.user.id;
+4 -4
View File
@@ -178,14 +178,14 @@ define('model', [], function () {
},
getFieldType: function (field) {
if (('defs' in this) && ('fields' in this.defs) && (field in this.defs.fields)) {
if (this.defs && this.defs.fields && (field in this.defs.fields)) {
return this.defs.fields[field].type || null;
}
return null;
},
getFieldParam: function (field, param) {
if (('defs' in this) && ('fields' in this.defs) && (field in this.defs.fields)) {
if (this.defs && this.defs.fields && (field in this.defs.fields)) {
if (param in this.defs.fields[field]) {
return this.defs.fields[field][param];
}
@@ -194,14 +194,14 @@ define('model', [], function () {
},
getLinkType: function (link) {
if (('defs' in this) && ('links' in this.defs) && (link in this.defs.links)) {
if (this.defs && this.defs.links && (link in this.defs.links)) {
return this.defs.links[link].type || null;
}
return null;
},
getLinkParam: function (link, param) {
if (('defs' in this) && ('links' in this.defs) && (link in this.defs.links)) {
if (this.defs && this.defs.links && (link in this.defs.links)) {
if (param in this.defs.links[link]) {
return this.defs.links[link][param];
}
+11 -1
View File
@@ -77,6 +77,10 @@ define('ui', [], function () {
this.id = 'dialog-' + Math.floor((Math.random() * 100000));
if (typeof this.backdrop === 'undefined') {
this.backdrop = 'static';
}
this.contents = '';
if (this.header) {
var headerClassName = '';
@@ -340,13 +344,19 @@ define('ui', [], function () {
Dialog: Dialog,
confirm: function (message, o, callback, context) {
o = o || {};
var confirmText = o.confirmText;
var cancelText = o.cancelText;
var confirmStyle = o.confirmStyle || 'danger';
var backdrop = o.backdrop;
if (typeof backdrop === 'undefined') {
backdrop = false;
}
return new Promise(function (resolve) {
var dialog = new Dialog({
backdrop: ('backdrop' in o) ? o.backdrop : false,
backdrop: backdrop,
header: false,
className: 'dialog-confirm',
body: '<span class="confirm-message">' + message + '</a>',
+2 -3
View File
@@ -26,12 +26,12 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
Espo.define('views/admin/job/fields/name', 'views/fields/varchar', function (Dep) {
define('views/admin/job/fields/name', 'views/fields/varchar', function (Dep) {
return Dep.extend({
getValueForDisplay: function () {
if (this.mode == 'list' || this.mode == 'detail') {
if (this.mode == 'list' || this.mode == 'detail' || this.mode === 'listLink') {
if (!this.model.get('name')) {
return this.model.get('serviceName') + ': ' + this.model.get('methodName');
} else {
@@ -42,4 +42,3 @@ Espo.define('views/admin/job/fields/name', 'views/fields/varchar', function (Dep
});
});
+12
View File
@@ -246,6 +246,11 @@ define('views/dashboard', ['view', 'lib!gridstack'], function (Dep, Gridstack) {
this.currentTabLayout.forEach(function (o) {
if (!o.id || !o.name) return;
if (!this.getMetadata().get(['dashlets', o.name])) {
console.error("Dashlet " + o.name + " doesn't exist or not available.");
return;
}
this.createDashletView(o.id, o.name);
}, this);
@@ -323,6 +328,9 @@ define('views/dashboard', ['view', 'lib!gridstack'], function (Dep, Gridstack) {
this.currentTabLayout.forEach(function (o) {
var $item = this.prepareGridstackItem(o.id, o.name);
if (!this.getMetadata().get(['dashlets', o.name])) {
return;
}
grid.addWidget($item, o.x, o.y, o.width, o.height);
}, this);
@@ -330,6 +338,10 @@ define('views/dashboard', ['view', 'lib!gridstack'], function (Dep, Gridstack) {
this.currentTabLayout.forEach(function (o) {
if (!o.id || !o.name) return;
if (!this.getMetadata().get(['dashlets', o.name])) {
console.error("Dashlet " + o.name + " doesn't exist or not available.");
return;
}
this.createDashletView(o.id, o.name);
}, this);
+2 -1
View File
@@ -81,7 +81,8 @@ define('views/dashlet', 'view', function (Dep) {
bodyView.trigger('resize');
}, this);
var viewName = this.getMetadata().get('dashlets.' + this.name + '.view') || 'views/dashlets/' + Espo.Utils.camelCaseToHyphen(this.name);
var viewName = this.getMetadata().get(['dashlets', this.name, 'view']) ||
'views/dashlets/' + Espo.Utils.camelCaseToHyphen(this.name);
this.createView('body', viewName, {
el: this.options.el + ' .dashlet-body',
+7 -1
View File
@@ -119,6 +119,9 @@ define('views/fields/array', ['views/fields/base', 'lib!Selectize'], function (D
this.setupTranslation();
}
this.displayAsLabel = this.params.displayAsLabel || this.displayAsLabel;
this.displayAsList = this.params.displayAsList || this.displayAsList;
if (this.params.isSorted && this.translatedOptions) {
this.params.options = Espo.Utils.clone(this.params.options);
this.params.options = this.params.options.sort(function (v1, v2) {
@@ -390,7 +393,10 @@ define('views/fields/array', ['views/fields/base', 'lib!Selectize'], function (D
}, this)
if (this.params.displayAsLabel) {
if (this.displayAsList) {
if (!list.length) return '';
return '<div>' + list.join('</div><div>') + '</div>';
} else if (this.displayAsLabel) {
return list.join(' ');
} else {
return list.join(', ')
+10 -2
View File
@@ -26,7 +26,7 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
Espo.define('views/fields/colorpicker', ['views/fields/varchar', 'lib!Colorpicker'], function (Dep, Colorpicker) {
define('views/fields/colorpicker', ['views/fields/varchar', 'lib!Colorpicker'], function (Dep, Colorpicker) {
return Dep.extend({
@@ -46,10 +46,18 @@ Espo.define('views/fields/colorpicker', ['views/fields/varchar', 'lib!Colorpicke
afterRender: function () {
Dep.prototype.afterRender.call(this);
if (this.mode == 'edit') {
var isModal = !!this.$el.closest('.modal').length;
this.$element.parent().colorpicker({
format: 'hex'
format: 'hex',
container: isModal ? this.$el : false,
});
if (isModal) {
this.$el.find('.colorpicker').css('position', 'relative').addClass('pull-right');
}
}
if (this.mode === 'edit') {
this.$element.on('change', function () {
+1 -1
View File
@@ -80,7 +80,7 @@ define('views/modal', 'view', function (Dep) {
this.options = this.options || {};
this.backdrop = this.options.backdrop;
this.backdrop = this.options.backdrop || this.backdrop;
this.setSelector(this.containerSelector);
+1 -1
View File
@@ -49,7 +49,7 @@ define('views/notification/badge', 'view', function (Dep) {
setup: function () {
this.soundPath = this.getBasePath() + (this.getConfig().get('notificationSound') || this.soundPath);
this.notificationSoundsDisabled = this.getConfig().get('notificationSoundsDisabled');
this.notificationSoundsDisabled = true;
this.useWebSocket = this.getConfig().get('useWebSocket');
@@ -26,12 +26,12 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
Espo.define('views/preferences/fields/time-zone', 'views/fields/enum', function (Dep) {
define('views/preferences/fields/time-zone', 'views/fields/enum', function (Dep) {
return Dep.extend({
setupOptions: function () {
this.params.options = Espo.Utils.clone(this.getConfig().getFieldParam('timeZone', 'options') || []);
this.params.options = Espo.Utils.clone(this.getHelper().getAppParam('timeZoneList')) || [];
this.params.options.unshift('');
this.translatedOptions = this.translatedOptions || {};
@@ -39,5 +39,4 @@ Espo.define('views/preferences/fields/time-zone', 'views/fields/enum', function
},
});
});
@@ -134,6 +134,11 @@ define('views/preferences/record/edit', 'views/record/edit', function (Dep) {
this.hideField('assignmentEmailNotificationsIgnoreEntityTypeList');
} else {
hideNotificationPanel = false;
this.controlAssignmentEmailNotificationsVisibility();
this.listenTo(this.model, 'change:receiveAssignmentEmailNotifications', function () {
this.controlAssignmentEmailNotificationsVisibility();
}, this);
}
if ((this.getConfig().get('assignmentEmailNotificationsEntityList') || []).length === 0) {
@@ -233,6 +238,14 @@ define('views/preferences/record/edit', 'views/record/edit', function (Dep) {
}
},
controlAssignmentEmailNotificationsVisibility: function () {
if (this.model.get('receiveAssignmentEmailNotifications')) {
this.showField('assignmentEmailNotificationsIgnoreEntityTypeList');
} else {
this.hideField('assignmentEmailNotificationsIgnoreEntityTypeList');
}
},
actionReset: function () {
this.confirm(this.translate('resetPreferencesConfirmation', 'messages'), function () {
$.ajax({
@@ -66,7 +66,8 @@ Espo.define('views/settings/fields/dashboard-layout', ['views/fields/base', 'lib
data: function () {
return {
dashboardLayout: this.dashboardLayout,
currentTab: this.currentTab
currentTab: this.currentTab,
isEmpty: this.isEmpty(),
};
},
@@ -364,10 +365,37 @@ Espo.define('views/settings/fields/dashboard-layout', ['views/fields/base', 'lib
return options[optionName];
},
isEmpty: function () {
var isEmpty = true
if (this.dashboardLayout && this.dashboardLayout.length) {
this.dashboardLayout.forEach(function (item) {
if (item.layout && item.layout.length) {
isEmpty = false;
}
}, this);
}
return isEmpty;
},
validateRequired: function () {
if (!this.isRequired()) return;
if (this.isEmpty()) {
var msg = this.translate('fieldIsRequired', 'messages').replace('{field}', this.getLabelText());
this.showValidationMessage(msg);
return true;
}
},
fetch: function () {
var data = {};
if (!this.dashboardLayout || !this.dashboardLayout.length) {
data[this.name] = null;
data['dashletsOptions'] = {};
return data;
} else {
data[this.name] = Espo.Utils.cloneDeep(this.dashboardLayout);
}
+3
View File
@@ -206,6 +206,8 @@ define('views/site/navbar', 'view', function (Dep) {
var scopes = this.getMetadata().get('scopes') || {};
this.tabList = tabList.filter(function (scope) {
if (scope === '_delimiter_' || scope === 'Home') return true;
if (!scopes[scope]) return false;
if ((scopes[scope] || {}).disabled) return;
if ((scopes[scope] || {}).acl) {
return this.getAcl().check(scope);
@@ -214,6 +216,7 @@ define('views/site/navbar', 'view', function (Dep) {
}, this);
this.quickCreateList = this.getQuickCreateList().filter(function (scope) {
if (!scopes[scope]) return false;
if ((scopes[scope] || {}).disabled) return;
if ((scopes[scope] || {}).acl) {
return this.getAcl().check(scope, 'create');
+4 -1
View File
@@ -2213,6 +2213,10 @@ h4.panel-title span.fas.color-icon {
}
.record .middle {
> .panel.hidden + .panel {
border-top-width: @panel-border-width;
}
margin-bottom: @line-height-computed;
}
@@ -2256,7 +2260,6 @@ h4.panel-title span.fas.color-icon {
.bottom {
.panel.hidden + .panel.sticked {
margin-top: 0;
border-top-width: @panel-border-width;
}
}
+4 -3
View File
@@ -254,13 +254,14 @@ class Installer
$siteUrl = $this->getSystemHelper()->getBaseUrl();
$databaseDefaults = $this->app->getContainer()->get('config')->get('database');
$data = array(
$data = [
'database' => array_merge($databaseDefaults, $database),
'language' => $language,
'siteUrl' => $siteUrl,
'passwordSalt' => $this->getPasswordHash()->generateSalt(),
'cryptKey' => $this->getContainer()->get('crypt')->generateKey()
);
'cryptKey' => $this->getContainer()->get('crypt')->generateKey(),
'hashSecretKey' => \Espo\Core\Utils\Util::generateSecretKey(),
];
$owner = $this->getFileManager()->getPermissionUtils()->getDefaultOwner(true);
$group = $this->getFileManager()->getPermissionUtils()->getDefaultGroup(true);
@@ -296,4 +296,28 @@ class FormulaTest extends \tests\integration\Core\BaseTestCase
$result = $fm->run($script, $contact);
$this->assertEquals('1', $result);
}
public function testPasswordGenerate()
{
$fm = $this->getContainer()->get('formulaManager');
$script = "password\\generate()";
$result = $fm->run($script);
$this->assertTrue(is_string($result));
}
public function testPasswordHash()
{
$fm = $this->getContainer()->get('formulaManager');
$script1 = "password\\hash('1')";
$result1 = $fm->run($script1);
$script2 = "password\\hash('2')";
$result2 = $fm->run($script2);
$this->assertTrue(is_string($result1));
$this->assertTrue($result1 !== $result);
}
}