diff --git a/application/Espo/Controllers/I18n.php b/application/Espo/Controllers/I18n.php index cde4381417..ea8f5a1f52 100644 --- a/application/Espo/Controllers/I18n.php +++ b/application/Espo/Controllers/I18n.php @@ -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();*/ } } diff --git a/application/Espo/Controllers/Layout.php b/application/Espo/Controllers/Layout.php index bcbed006b5..71c114c57e 100644 --- a/application/Espo/Controllers/Layout.php +++ b/application/Espo/Controllers/Layout.php @@ -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) diff --git a/application/Espo/Controllers/Metadata.php b/application/Espo/Controllers/Metadata.php index 2d7980b4a7..e7455ab7f4 100644 --- a/application/Espo/Controllers/Metadata.php +++ b/application/Espo/Controllers/Metadata.php @@ -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) diff --git a/application/Espo/Core/Formula/Functions/EntityGroup/SumRelatedType.php b/application/Espo/Core/Formula/Functions/EntityGroup/SumRelatedType.php index b0ef65a2cf..c136914da1 100644 --- a/application/Espo/Core/Formula/Functions/EntityGroup/SumRelatedType.php +++ b/application/Espo/Core/Formula/Functions/EntityGroup/SumRelatedType.php @@ -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']; diff --git a/application/Espo/Core/Formula/Functions/PasswordGroup/GenerateType.php b/application/Espo/Core/Formula/Functions/PasswordGroup/GenerateType.php new file mode 100644 index 0000000000..a5b9c211d5 --- /dev/null +++ b/application/Espo/Core/Formula/Functions/PasswordGroup/GenerateType.php @@ -0,0 +1,54 @@ +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; + } +} diff --git a/application/Espo/Core/Formula/Functions/PasswordGroup/HashType.php b/application/Espo/Core/Formula/Functions/PasswordGroup/HashType.php new file mode 100644 index 0000000000..463092b5b8 --- /dev/null +++ b/application/Espo/Core/Formula/Functions/PasswordGroup/HashType.php @@ -0,0 +1,60 @@ +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; + } +} diff --git a/application/Espo/Core/Loaders/Hasher.php b/application/Espo/Core/Loaders/Hasher.php new file mode 100644 index 0000000000..2b43ccb830 --- /dev/null +++ b/application/Espo/Core/Loaders/Hasher.php @@ -0,0 +1,40 @@ +getContainer()->get('config') + ); + } +} diff --git a/application/Espo/Core/Utils/Database/Schema/tables/subscription.php b/application/Espo/Core/Utils/Database/Schema/tables/subscription.php index 2f5cccba60..cb394332df 100644 --- a/application/Espo/Core/Utils/Database/Schema/tables/subscription.php +++ b/application/Espo/Core/Utils/Database/Schema/tables/subscription.php @@ -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'], + ], + ], + ], +]; diff --git a/application/Espo/Core/Utils/Hasher.php b/application/Espo/Core/Utils/Hasher.php new file mode 100644 index 0000000000..6222367fe5 --- /dev/null +++ b/application/Espo/Core/Utils/Hasher.php @@ -0,0 +1,49 @@ +config = $config; + } + + public function hash(string $string) : string + { + $secretKey = $this->config->get($this->secretKeyParam) ?? ''; + + return md5(hash_hmac('sha256', $string, $secretKey, true)); + } +} diff --git a/application/Espo/Core/Utils/Util.php b/application/Espo/Core/Utils/Util.php index 12b3d0aac5..d533ce99e3 100644 --- a/application/Espo/Core/Utils/Util.php +++ b/application/Espo/Core/Utils/Util.php @@ -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)); diff --git a/application/Espo/Core/defaults/systemConfig.php b/application/Espo/Core/defaults/systemConfig.php index e708cfd4a5..008c7b5ed6 100644 --- a/application/Espo/Core/defaults/systemConfig.php +++ b/application/Espo/Core/defaults/systemConfig.php @@ -96,6 +96,7 @@ return [ 'passwordSalt', 'cryptKey', 'apiSecretKeys', + 'hashSecretKey', 'restrictedMode', 'userLimit', 'portalUserLimit', diff --git a/application/Espo/Hooks/Common/Stream.php b/application/Espo/Hooks/Common/Stream.php index 73e45c809c..885f2a4681 100644 --- a/application/Espo/Hooks/Common/Stream.php +++ b/application/Espo/Hooks/Common/Stream.php @@ -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; diff --git a/application/Espo/Modules/Crm/EntryPoints/SubscribeAgain.php b/application/Espo/Modules/Crm/EntryPoints/SubscribeAgain.php index de9e897a82..1f6e703ce4 100644 --- a/application/Espo/Modules/Crm/EntryPoints/SubscribeAgain.php +++ b/application/Espo/Modules/Crm/EntryPoints/SubscribeAgain.php @@ -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(); + } + } +} diff --git a/application/Espo/Modules/Crm/EntryPoints/Unsubscribe.php b/application/Espo/Modules/Crm/EntryPoints/Unsubscribe.php index 2c874010ce..508a15973f 100644 --- a/application/Espo/Modules/Crm/EntryPoints/Unsubscribe.php +++ b/application/Espo/Modules/Crm/EntryPoints/Unsubscribe.php @@ -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(); + } + } } diff --git a/application/Espo/Modules/Crm/Resources/metadata/app/aclPortal.json b/application/Espo/Modules/Crm/Resources/metadata/app/aclPortal.json index 3c4385bebe..318408439b 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/app/aclPortal.json +++ b/application/Espo/Modules/Crm/Resources/metadata/app/aclPortal.json @@ -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" } } } diff --git a/application/Espo/Repositories/Portal.php b/application/Espo/Repositories/Portal.php index ac3f0c0fce..c593f78f75 100644 --- a/application/Espo/Repositories/Portal.php +++ b/application/Espo/Repositories/Portal.php @@ -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(); + } } } } diff --git a/application/Espo/Resources/layouts/Job/detail.json b/application/Espo/Resources/layouts/Job/detail.json new file mode 100644 index 0000000000..667eea760a --- /dev/null +++ b/application/Espo/Resources/layouts/Job/detail.json @@ -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}] + ] + } +] diff --git a/application/Espo/Resources/layouts/Settings/notifications.json b/application/Espo/Resources/layouts/Settings/notifications.json index 78db9cb94b..b029926759 100644 --- a/application/Espo/Resources/layouts/Settings/notifications.json +++ b/application/Espo/Resources/layouts/Settings/notifications.json @@ -2,8 +2,7 @@ { "label": "In-app Notifications", "rows": [ - [{"name": "assignmentNotificationsEntityList"}, false], - [{"name": "notificationSoundsDisabled"}, false] + [{"name": "assignmentNotificationsEntityList"}, false] ] }, { diff --git a/application/Espo/Resources/metadata/app/acl.json b/application/Espo/Resources/metadata/app/acl.json index 711ab8ae6c..82956a4d8d 100644 --- a/application/Espo/Resources/metadata/app/acl.json +++ b/application/Espo/Resources/metadata/app/acl.json @@ -105,6 +105,9 @@ "read": "own", "edit": "no" }, + "Team": { + "read": "team" + }, "Import": false, "Webhook": false }, diff --git a/application/Espo/Resources/metadata/app/aclPortal.json b/application/Espo/Resources/metadata/app/aclPortal.json index 35ca97c2d2..af9daf057b 100644 --- a/application/Espo/Resources/metadata/app/aclPortal.json +++ b/application/Espo/Resources/metadata/app/aclPortal.json @@ -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 } diff --git a/application/Espo/Resources/metadata/entityAcl/User.json b/application/Espo/Resources/metadata/entityAcl/User.json index da78c602cd..f2c5f7f843 100644 --- a/application/Espo/Resources/metadata/entityAcl/User.json +++ b/application/Espo/Resources/metadata/entityAcl/User.json @@ -16,6 +16,9 @@ "internal": true, "nonAdminReadOnly": true }, + "authLogRecordId": { + "internal": true + }, "authMethod": { "onlyAdmin": true }, diff --git a/application/Espo/Resources/metadata/entityDefs/Job.json b/application/Espo/Resources/metadata/entityDefs/Job.json index 69bd930683..71d45b0bf2 100644 --- a/application/Espo/Resources/metadata/entityDefs/Job.json +++ b/application/Espo/Resources/metadata/entityDefs/Job.json @@ -104,7 +104,7 @@ "collection": { "orderBy": "number", "order": "desc", - "textFilterFields": ["name", "methodName", "serviceName", "scheduledJob"] + "textFilterFields": ["id", "name", "methodName", "serviceName", "scheduledJob"] }, "indexes": { "executeTime": { diff --git a/application/Espo/Resources/metadata/entityDefs/Note.json b/application/Espo/Resources/metadata/entityDefs/Note.json index b5f6968613..1e46923360 100644 --- a/application/Espo/Resources/metadata/entityDefs/Note.json +++ b/application/Espo/Resources/metadata/entityDefs/Note.json @@ -155,6 +155,9 @@ "parentAndSuperParent": { "type": "index", "columns": ["parentId", "parentType", "superParentId", "superParentType"] + }, + "createdByNumber": { + "columns": ["createdById", "number"] } } } diff --git a/application/Espo/Resources/metadata/entityDefs/Portal.json b/application/Espo/Resources/metadata/entityDefs/Portal.json index c0aeac3e5c..02632096d2 100644 --- a/application/Espo/Resources/metadata/entityDefs/Portal.json +++ b/application/Espo/Resources/metadata/entityDefs/Portal.json @@ -85,8 +85,7 @@ }, "dashboardLayout": { "type": "jsonArray", - "view": "views/settings/fields/dashboard-layout", - "required": true + "view": "views/settings/fields/dashboard-layout" }, "dashletsOptions": { "type": "jsonObject", diff --git a/application/Espo/Resources/metadata/scopes/PortalRole.json b/application/Espo/Resources/metadata/scopes/PortalRole.json new file mode 100644 index 0000000000..a5280171da --- /dev/null +++ b/application/Espo/Resources/metadata/scopes/PortalRole.json @@ -0,0 +1,7 @@ +{ + "entity": true, + "layouts": false, + "tab": false, + "acl": false, + "customizable": false +} diff --git a/application/Espo/Resources/metadata/scopes/Role.json b/application/Espo/Resources/metadata/scopes/Role.json index d22f18db45..a5280171da 100644 --- a/application/Espo/Resources/metadata/scopes/Role.json +++ b/application/Espo/Resources/metadata/scopes/Role.json @@ -1 +1,7 @@ -{"entity":true,"layouts":false,"tab":false,"acl":false,"customizable":false} +{ + "entity": true, + "layouts": false, + "tab": false, + "acl": false, + "customizable": false +} diff --git a/application/Espo/SelectManagers/Email.php b/application/Espo/SelectManagers/Email.php index 317c03cd80..52a25a1cf9 100644 --- a/application/Espo/SelectManagers/Email.php +++ b/application/Espo/SelectManagers/Email.php @@ -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']; } diff --git a/application/Espo/SelectManagers/Team.php b/application/Espo/SelectManagers/Team.php index b0119849d5..a2c4d1f175 100644 --- a/application/Espo/SelectManagers/Team.php +++ b/application/Espo/SelectManagers/Team.php @@ -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 + ]; } } diff --git a/application/Espo/Services/App.php b/application/Espo/Services/App.php index 5fc2261903..dce9b9f28b 100644 --- a/application/Espo/Services/App.php +++ b/application/Espo/Services/App.php @@ -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(); diff --git a/application/Espo/Services/Language.php b/application/Espo/Services/Language.php new file mode 100644 index 0000000000..7c140ec0a5 --- /dev/null +++ b/application/Espo/Services/Language.php @@ -0,0 +1,140 @@ +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; + } +} diff --git a/application/Espo/Services/Layout.php b/application/Espo/Services/Layout.php new file mode 100644 index 0000000000..e5b7b3e17e --- /dev/null +++ b/application/Espo/Services/Layout.php @@ -0,0 +1,79 @@ +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; + } +} diff --git a/application/Espo/Services/Metadata.php b/application/Espo/Services/Metadata.php new file mode 100644 index 0000000000..054719dce5 --- /dev/null +++ b/application/Espo/Services/Metadata.php @@ -0,0 +1,149 @@ +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; + } +} diff --git a/application/Espo/Services/Portal.php b/application/Espo/Services/Portal.php index 951be43a6c..5cfc163477 100644 --- a/application/Espo/Services/Portal.php +++ b/application/Espo/Services/Portal.php @@ -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(); + } +} diff --git a/application/Espo/Services/PortalRole.php b/application/Espo/Services/PortalRole.php index efe52233be..60724eb8b6 100644 --- a/application/Espo/Services/PortalRole.php +++ b/application/Espo/Services/PortalRole.php @@ -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(); } } - diff --git a/application/Espo/Services/Record.php b/application/Espo/Services/Record.php index a502882d49..4bc0647755 100644 --- a/application/Espo/Services/Record.php +++ b/application/Espo/Services/Record.php @@ -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; diff --git a/application/Espo/Services/Role.php b/application/Espo/Services/Role.php index d5ff531b98..3f60150ef9 100644 --- a/application/Espo/Services/Role.php +++ b/application/Espo/Services/Role.php @@ -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(); } } diff --git a/application/Espo/Services/Settings.php b/application/Espo/Services/Settings.php index 4a372eaf38..6c51736588 100644 --- a/application/Espo/Services/Settings.php +++ b/application/Espo/Services/Settings.php @@ -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; } diff --git a/application/Espo/Services/Stream.php b/application/Espo/Services/Stream.php index f7665710f1..d05924de00 100644 --- a/application/Espo/Services/Stream.php +++ b/application/Espo/Services/Stream.php @@ -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; } diff --git a/application/Espo/Services/User.php b/application/Espo/Services/User.php index 52bc000fc2..f73a6f88ef 100644 --- a/application/Espo/Services/User.php +++ b/application/Espo/Services/User.php @@ -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) diff --git a/client/lib/bootstrap-colorpicker.js b/client/lib/bootstrap-colorpicker.js index b2cdf560cf..ac22ed526c 100644 --- a/client/lib/bootstrap-colorpicker.js +++ b/client/lib/bootstrap-colorpicker.js @@ -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, diff --git a/client/modules/crm/res/templates/campaign/subscribe-again.tpl b/client/modules/crm/res/templates/campaign/subscribe-again.tpl index ebf2424928..1af52780b8 100644 --- a/client/modules/crm/res/templates/campaign/subscribe-again.tpl +++ b/client/modules/crm/res/templates/campaign/subscribe-again.tpl @@ -6,7 +6,7 @@ {{translate 'subscribedAgain' category='messages' scope='Campaign'}}

- {{translate 'Unsubscribe again' scope='Campaign'}} + {{translate 'Unsubscribe again' scope='Campaign'}}

diff --git a/client/modules/crm/res/templates/campaign/unsubscribe.tpl b/client/modules/crm/res/templates/campaign/unsubscribe.tpl index 062d477ab3..dfd5d45a2e 100644 --- a/client/modules/crm/res/templates/campaign/unsubscribe.tpl +++ b/client/modules/crm/res/templates/campaign/unsubscribe.tpl @@ -6,7 +6,7 @@ {{translate 'unsubscribed' category='messages' scope='Campaign'}}

- {{translate 'Subscribe again' scope='Campaign'}} + {{translate 'Subscribe again' scope='Campaign'}}

diff --git a/client/modules/crm/src/views/calendar/calendar.js b/client/modules/crm/src/views/calendar/calendar.js index 8ca427f4b5..87a6ac721d 100644 --- a/client/modules/crm/src/views/calendar/calendar.js +++ b/client/modules/crm/src/views/calendar/calendar.js @@ -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', diff --git a/client/modules/crm/src/views/calendar/timeline.js b/client/modules/crm/src/views/calendar/timeline.js index bd7946c3b7..49b72fa583 100644 --- a/client/modules/crm/src/views/calendar/timeline.js +++ b/client/modules/crm/src/views/calendar/timeline.js @@ -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 { diff --git a/client/modules/crm/src/views/campaign/subscribe-again.js b/client/modules/crm/src/views/campaign/subscribe-again.js index 8cce5fcc27..f1d5bad20d 100644 --- a/client/modules/crm/src/views/campaign/subscribe-again.js +++ b/client/modules/crm/src/views/campaign/subscribe-again.js @@ -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; } diff --git a/client/modules/crm/src/views/campaign/unsubscribe.js b/client/modules/crm/src/views/campaign/unsubscribe.js index 4d5a974e83..7e073585e3 100644 --- a/client/modules/crm/src/views/campaign/unsubscribe.js +++ b/client/modules/crm/src/views/campaign/unsubscribe.js @@ -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; } diff --git a/client/modules/crm/src/views/mass-email/fields/smtp-account.js b/client/modules/crm/src/views/mass-email/fields/smtp-account.js index 4562942fb9..878f1c5e0c 100644 --- a/client/modules/crm/src/views/mass-email/fields/smtp-account.js +++ b/client/modules/crm/src/views/mass-email/fields/smtp-account.js @@ -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; - } + }, }); }); diff --git a/client/res/templates/settings/fields/dashboard-layout/detail.tpl b/client/res/templates/settings/fields/dashboard-layout/detail.tpl index 308bd56e45..4534610894 100644 --- a/client/res/templates/settings/fields/dashboard-layout/detail.tpl +++ b/client/res/templates/settings/fields/dashboard-layout/detail.tpl @@ -1,4 +1,8 @@ +{{#if isEmpty}} +{{translate 'None'}} +{{/if}} +
{{#ifNotEqual dashboardLayout.length 1}}
diff --git a/client/src/acl-portal.js b/client/src/acl-portal.js index 37a3f25105..e960a5200b 100644 --- a/client/src/acl-portal.js +++ b/client/src/acl-portal.js @@ -57,7 +57,7 @@ define('acl-portal', ['acl'], function (Dep) { return true; } if (data === null) { - return true; + return false; } action = action || null; diff --git a/client/src/acl.js b/client/src/acl.js index 93c2f8a6e2..c7a039f549 100644 --- a/client/src/acl.js +++ b/client/src/acl.js @@ -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; diff --git a/client/src/app.js b/client/src/app.js index 773cb47d7f..bcccbcb40f 100644 --- a/client/src/app.js +++ b/client/src/app.js @@ -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; diff --git a/client/src/model.js b/client/src/model.js index 06f9d245de..690f63c019 100644 --- a/client/src/model.js +++ b/client/src/model.js @@ -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]; } diff --git a/client/src/ui.js b/client/src/ui.js index 5f021669a4..0fc2fcb343 100644 --- a/client/src/ui.js +++ b/client/src/ui.js @@ -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: '' + message + '', diff --git a/client/src/views/admin/job/fields/name.js b/client/src/views/admin/job/fields/name.js index c3b073806e..54aca96003 100644 --- a/client/src/views/admin/job/fields/name.js +++ b/client/src/views/admin/job/fields/name.js @@ -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 }); }); - diff --git a/client/src/views/dashboard.js b/client/src/views/dashboard.js index 08d9edf898..b11aaca03d 100644 --- a/client/src/views/dashboard.js +++ b/client/src/views/dashboard.js @@ -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); diff --git a/client/src/views/dashlet.js b/client/src/views/dashlet.js index a9d90aa775..759579f212 100644 --- a/client/src/views/dashlet.js +++ b/client/src/views/dashlet.js @@ -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', diff --git a/client/src/views/fields/array.js b/client/src/views/fields/array.js index 75812f0799..ef4931c5fe 100644 --- a/client/src/views/fields/array.js +++ b/client/src/views/fields/array.js @@ -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 '
' + list.join('
') + '
'; + } else if (this.displayAsLabel) { return list.join(' '); } else { return list.join(', ') diff --git a/client/src/views/fields/colorpicker.js b/client/src/views/fields/colorpicker.js index 1e16b61554..04ecf541d0 100644 --- a/client/src/views/fields/colorpicker.js +++ b/client/src/views/fields/colorpicker.js @@ -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 () { diff --git a/client/src/views/modal.js b/client/src/views/modal.js index 52a3a968ba..7109fb5a9f 100644 --- a/client/src/views/modal.js +++ b/client/src/views/modal.js @@ -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); diff --git a/client/src/views/notification/badge.js b/client/src/views/notification/badge.js index f5f294c9cb..31e3cf085f 100644 --- a/client/src/views/notification/badge.js +++ b/client/src/views/notification/badge.js @@ -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'); diff --git a/client/src/views/preferences/fields/time-zone.js b/client/src/views/preferences/fields/time-zone.js index 3368c2d21a..e298856a12 100644 --- a/client/src/views/preferences/fields/time-zone.js +++ b/client/src/views/preferences/fields/time-zone.js @@ -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 }, }); - }); diff --git a/client/src/views/preferences/record/edit.js b/client/src/views/preferences/record/edit.js index f26471995f..eb7efdb1e5 100644 --- a/client/src/views/preferences/record/edit.js +++ b/client/src/views/preferences/record/edit.js @@ -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({ diff --git a/client/src/views/settings/fields/dashboard-layout.js b/client/src/views/settings/fields/dashboard-layout.js index fa18b61efc..03fe12bced 100644 --- a/client/src/views/settings/fields/dashboard-layout.js +++ b/client/src/views/settings/fields/dashboard-layout.js @@ -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); } diff --git a/client/src/views/site/navbar.js b/client/src/views/site/navbar.js index b0824c6d82..1c1141729e 100644 --- a/client/src/views/site/navbar.js +++ b/client/src/views/site/navbar.js @@ -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'); diff --git a/frontend/less/espo/custom.less b/frontend/less/espo/custom.less index 6cf1c12091..369bd1229e 100644 --- a/frontend/less/espo/custom.less +++ b/frontend/less/espo/custom.less @@ -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; } } diff --git a/install/core/Installer.php b/install/core/Installer.php index daa2c2a513..621f722fd0 100644 --- a/install/core/Installer.php +++ b/install/core/Installer.php @@ -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); diff --git a/tests/integration/Espo/Core/Formula/FormulaTest.php b/tests/integration/Espo/Core/Formula/FormulaTest.php index ef6fbc5220..3b59e07577 100644 --- a/tests/integration/Espo/Core/Formula/FormulaTest.php +++ b/tests/integration/Espo/Core/Formula/FormulaTest.php @@ -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); + } }