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