From 8bafcded509ececb12ac06befee3a04a0c25bf72 Mon Sep 17 00:00:00 2001 From: yuri Date: Sat, 28 Sep 2019 11:20:42 +0300 Subject: [PATCH 01/44] portal role scope --- .../Espo/Resources/metadata/scopes/PortalRole.json | 7 +++++++ application/Espo/Resources/metadata/scopes/Role.json | 8 +++++++- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 application/Espo/Resources/metadata/scopes/PortalRole.json 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 +} From 68615ba8bbc7b985d2fbd547ff61a846c8066ef0 Mon Sep 17 00:00:00 2001 From: yuri Date: Sat, 28 Sep 2019 11:33:20 +0300 Subject: [PATCH 02/44] language hide labels by acl --- application/Espo/Controllers/I18n.php | 8 +- application/Espo/Services/Language.php | 133 +++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 application/Espo/Services/Language.php 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/Services/Language.php b/application/Espo/Services/Language.php new file mode 100644 index 0000000000..0b43d4be8a --- /dev/null +++ b/application/Espo/Services/Language.php @@ -0,0 +1,133 @@ +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'])) 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']); + } 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; + } +} From 4b25456acdf07d961ab2b36cff94763fc79b7a46 Mon Sep 17 00:00:00 2001 From: yuri Date: Sat, 28 Sep 2019 11:55:31 +0300 Subject: [PATCH 03/44] fix teams select manager --- application/Espo/SelectManagers/Team.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 + ]; } } From 1e4e8b3fcfd36f923be21b28de03f9c68b0c4253 Mon Sep 17 00:00:00 2001 From: yuri Date: Sat, 28 Sep 2019 12:07:08 +0300 Subject: [PATCH 04/44] follow access --- application/Espo/Hooks/Common/Stream.php | 6 ++--- application/Espo/Services/Record.php | 2 ++ application/Espo/Services/Stream.php | 32 ++++++++++++++++++++---- 3 files changed, 32 insertions(+), 8 deletions(-) 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/Services/Record.php b/application/Espo/Services/Record.php index a502882d49..8cc82d5fc3 100644 --- a/application/Espo/Services/Record.php +++ b/application/Espo/Services/Record.php @@ -1857,6 +1857,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 +1873,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/Stream.php b/application/Espo/Services/Stream.php index f7665710f1..a89d9cac06 100644 --- a/application/Espo/Services/Stream.php +++ b/application/Espo/Services/Stream.php @@ -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; } From 882eeffdd5f4283559a1933b45964f1b6dcadd3f Mon Sep 17 00:00:00 2001 From: yuri Date: Sat, 28 Sep 2019 12:16:17 +0300 Subject: [PATCH 05/44] modal backdrop fix --- client/src/ui.js | 12 +++++++++++- client/src/views/modal.js | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) 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/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); From 6e1d905db9ad78106fb3b9b4deb0bb51255dae5c Mon Sep 17 00:00:00 2001 From: yuri Date: Sat, 28 Sep 2019 12:19:19 +0300 Subject: [PATCH 06/44] team role by default --- application/Espo/Resources/metadata/app/acl.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/application/Espo/Resources/metadata/app/acl.json b/application/Espo/Resources/metadata/app/acl.json index 711ab8ae6c..b7a5d5ea20 100644 --- a/application/Espo/Resources/metadata/app/acl.json +++ b/application/Espo/Resources/metadata/app/acl.json @@ -105,6 +105,10 @@ "read": "own", "edit": "no" }, + "Team": { + "read": "team", + "edit": "no" + }, "Import": false, "Webhook": false }, From f2c769a0628d33188132cb8cd644e187323c610d Mon Sep 17 00:00:00 2001 From: yuri Date: Sat, 28 Sep 2019 12:23:59 +0300 Subject: [PATCH 07/44] fix acl --- application/Espo/Resources/metadata/app/acl.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/application/Espo/Resources/metadata/app/acl.json b/application/Espo/Resources/metadata/app/acl.json index b7a5d5ea20..82956a4d8d 100644 --- a/application/Espo/Resources/metadata/app/acl.json +++ b/application/Espo/Resources/metadata/app/acl.json @@ -106,8 +106,7 @@ "edit": "no" }, "Team": { - "read": "team", - "edit": "no" + "read": "team" }, "Import": false, "Webhook": false From 3478aa772c30646f5b6b46ffeac302412b077837 Mon Sep 17 00:00:00 2001 From: yuri Date: Sat, 28 Sep 2019 13:04:48 +0300 Subject: [PATCH 08/44] metadata hide by acl --- application/Espo/Controllers/Metadata.php | 2 +- application/Espo/Services/App.php | 3 +- application/Espo/Services/Metadata.php | 92 +++++++++++++++++++ client/src/app.js | 2 +- client/src/model.js | 8 +- .../src/views/preferences/fields/time-zone.js | 5 +- client/src/views/site/navbar.js | 3 + 7 files changed, 105 insertions(+), 10 deletions(-) create mode 100644 application/Espo/Services/Metadata.php 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/Services/App.php b/application/Espo/Services/App.php index 5fc2261903..e495e0a365 100644 --- a/application/Espo/Services/App.php +++ b/application/Espo/Services/App.php @@ -116,7 +116,8 @@ 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'], []), ] ]; } diff --git a/application/Espo/Services/Metadata.php b/application/Espo/Services/Metadata.php new file mode 100644 index 0000000000..935765c9af --- /dev/null +++ b/application/Espo/Services/Metadata.php @@ -0,0 +1,92 @@ +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); + } + } + + 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/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/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/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'); From 6cfd5db8acf86489769a55f12ddf829e2f2adbdc Mon Sep 17 00:00:00 2001 From: yuri Date: Sat, 28 Sep 2019 13:46:32 +0300 Subject: [PATCH 09/44] fix portal --- application/Espo/Repositories/Portal.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) 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(); + } } } } From 1f334c2690ddd72e475820a117627d681ba1d84a Mon Sep 17 00:00:00 2001 From: yuri Date: Sat, 28 Sep 2019 13:46:46 +0300 Subject: [PATCH 10/44] clear frontend cache on role change --- application/Espo/Services/Portal.php | 18 +++++++++- application/Espo/Services/PortalRole.php | 3 +- application/Espo/Services/Role.php | 2 ++ application/Espo/Services/User.php | 46 ++++++++++++++++++++++-- 4 files changed, 65 insertions(+), 4 deletions(-) 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/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/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) From e6d79e0847a4d11426e97759e59be2cd0a65f5b1 Mon Sep 17 00:00:00 2001 From: yuri Date: Sat, 28 Sep 2019 14:14:00 +0300 Subject: [PATCH 11/44] dashboard layout fix --- .../Resources/metadata/entityDefs/Portal.json | 3 +- .../fields/dashboard-layout/detail.tpl | 4 +++ .../views/settings/fields/dashboard-layout.js | 30 ++++++++++++++++++- 3 files changed, 34 insertions(+), 3 deletions(-) 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/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/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); } From b39d2b34e28c347c39ef3ce75246f8f5c9a9ccf4 Mon Sep 17 00:00:00 2001 From: yuri Date: Sat, 28 Sep 2019 14:52:15 +0300 Subject: [PATCH 12/44] acl portal additions --- .../Crm/Resources/metadata/app/aclPortal.json | 60 +++++++------------ .../Resources/metadata/app/aclPortal.json | 18 +----- 2 files changed, 21 insertions(+), 57 deletions(-) diff --git a/application/Espo/Modules/Crm/Resources/metadata/app/aclPortal.json b/application/Espo/Modules/Crm/Resources/metadata/app/aclPortal.json index 3c4385bebe..f3641a7fae 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,23 +15,31 @@ "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" - } + "account": false, + "contacts": false, + "contact": false } } } diff --git a/application/Espo/Resources/metadata/app/aclPortal.json b/application/Espo/Resources/metadata/app/aclPortal.json index 35ca97c2d2..26fa848481 100644 --- a/application/Espo/Resources/metadata/app/aclPortal.json +++ b/application/Espo/Resources/metadata/app/aclPortal.json @@ -100,6 +100,7 @@ "type": false, "contact": false, "accounts": false, + "account": false, "portalRoles": false, "portals": false, "isActive": false @@ -121,23 +122,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 } From ea15452db47da8b7373c6c37c222cc0ed9dcf27d Mon Sep 17 00:00:00 2001 From: yuri Date: Sat, 28 Sep 2019 14:52:39 +0300 Subject: [PATCH 13/44] restrict user data and acl --- .../Resources/metadata/entityAcl/User.json | 3 + application/Espo/Services/App.php | 58 +++++++++++++++---- 2 files changed, 49 insertions(+), 12 deletions(-) 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/Services/App.php b/application/Espo/Services/App.php index e495e0a365..f284fc738f 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, @@ -122,6 +113,49 @@ class App extends \Espo\Core\Services\Base ]; } + 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'); + + foreach ($forbiddenAttributeList as $attribute) { + 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(); From 1c1b8b39854a1d3fee3d1ac0a51087e30e3a8159 Mon Sep 17 00:00:00 2001 From: yuri Date: Sat, 28 Sep 2019 15:13:47 +0300 Subject: [PATCH 14/44] restrict settings lists --- application/Espo/Services/Settings.php | 36 +++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) 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; } From 84434fa14ac80b39bd7ee4e50b2da9ce2c0da319 Mon Sep 17 00:00:00 2001 From: yuri Date: Mon, 30 Sep 2019 12:38:36 +0300 Subject: [PATCH 15/44] acl frontend fix --- client/src/acl-portal.js | 2 +- client/src/acl.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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; From 7e2bd603828eb405a5ce360593cef3c4c7c1c486 Mon Sep 17 00:00:00 2001 From: yuri Date: Mon, 30 Sep 2019 12:39:25 +0300 Subject: [PATCH 16/44] filter relationships layout by acl --- application/Espo/Controllers/Layout.php | 6 +- application/Espo/Services/Layout.php | 79 +++++++++++++++++++++++++ application/Espo/Services/Metadata.php | 51 ++++++++++++++++ 3 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 application/Espo/Services/Layout.php 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/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 index 935765c9af..48917a2de9 100644 --- a/application/Espo/Services/Metadata.php +++ b/application/Espo/Services/Metadata.php @@ -72,6 +72,57 @@ class Metadata extends \Espo\Core\Services\Base } } + $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 ($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'], [])); From 62c865bdc3ed5b1fda354503e5c9e3564d9592c8 Mon Sep 17 00:00:00 2001 From: yuri Date: Mon, 30 Sep 2019 13:28:25 +0300 Subject: [PATCH 17/44] subscription index --- .../Database/Schema/tables/subscription.php | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) 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'], + ], + ], + ], +]; From 3057f749c48dabe587607ff9f4a21c5234681a8c Mon Sep 17 00:00:00 2001 From: yuri Date: Mon, 30 Sep 2019 14:16:04 +0300 Subject: [PATCH 18/44] fix language filter --- application/Espo/Services/Language.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/application/Espo/Services/Language.php b/application/Espo/Services/Language.php index 0b43d4be8a..a416119550 100644 --- a/application/Espo/Services/Language.php +++ b/application/Espo/Services/Language.php @@ -123,9 +123,10 @@ class Language extends \Espo\Core\Services\Base 'userHasNoEmailAddress' => $languageObj->translate('userHasNoEmailAddress', 'messages', 'Admin'), ], ]; - $data['User']['fields']['password'] = $languageObj->translate('password', 'fields', 'User'); - $data['User']['fields']['passwordConfirm'] = $languageObj->translate('passwordConfirm', 'fields', 'User'); } + + $data['User']['fields']['password'] = $languageObj->translate('password', 'fields', 'User'); + $data['User']['fields']['passwordConfirm'] = $languageObj->translate('passwordConfirm', 'fields', 'User'); } return $data; From c572d0110f95acbc301698c40d1e56554a5672bb Mon Sep 17 00:00:00 2001 From: yuri Date: Mon, 30 Sep 2019 15:36:26 +0300 Subject: [PATCH 19/44] dashboard fix --- client/src/views/dashboard.js | 12 ++++++++++++ client/src/views/dashlet.js | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) 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', From ca05495e9ee8875a8e823357b9743e8ac7224417 Mon Sep 17 00:00:00 2001 From: yuri Date: Mon, 30 Sep 2019 15:44:39 +0300 Subject: [PATCH 20/44] fix metadata filter --- application/Espo/Services/Metadata.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/application/Espo/Services/Metadata.php b/application/Espo/Services/Metadata.php index 48917a2de9..054719dce5 100644 --- a/application/Espo/Services/Metadata.php +++ b/application/Espo/Services/Metadata.php @@ -102,6 +102,12 @@ class Metadata extends \Espo\Core\Services\Base $foreignEntityType = $defs['entity'] ?? null; if ($this->getAcl()->check($foreignEntityType)) continue; + if ($this->getUser()->isPortal()) { + if ($foreignEntityType === 'Account' || $foreignEntityType === 'Contact') { + continue; + } + } + if ($hasField) { if (!in_array($link, $fobiddenFieldList)) { continue; From d28638136ac91543e3c0cf02f54c7174e3ea91ca Mon Sep 17 00:00:00 2001 From: yuri Date: Mon, 30 Sep 2019 16:00:40 +0300 Subject: [PATCH 21/44] fix --- application/Espo/Resources/metadata/app/aclPortal.json | 2 ++ application/Espo/Services/App.php | 1 + 2 files changed, 3 insertions(+) diff --git a/application/Espo/Resources/metadata/app/aclPortal.json b/application/Espo/Resources/metadata/app/aclPortal.json index 26fa848481..af9daf057b 100644 --- a/application/Espo/Resources/metadata/app/aclPortal.json +++ b/application/Espo/Resources/metadata/app/aclPortal.json @@ -103,6 +103,8 @@ "account": false, "portalRoles": false, "portals": false, + "roles": false, + "defaultTeam": false, "isActive": false } } diff --git a/application/Espo/Services/App.php b/application/Espo/Services/App.php index f284fc738f..c333634551 100644 --- a/application/Espo/Services/App.php +++ b/application/Espo/Services/App.php @@ -130,6 +130,7 @@ class App extends \Espo\Core\Services\Base $forbiddenAttributeList = $this->getAcl()->getScopeForbiddenAttributeList('User'); foreach ($forbiddenAttributeList as $attribute) { + if ($attribute === 'type') continue; unset($data->$attribute); } From 1b94573502b2fd24b7013463f68ac3d45992ce4b Mon Sep 17 00:00:00 2001 From: yuri Date: Mon, 30 Sep 2019 16:10:01 +0300 Subject: [PATCH 22/44] fix acl portal --- application/Espo/Services/App.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/application/Espo/Services/App.php b/application/Espo/Services/App.php index c333634551..aa3a88a7f4 100644 --- a/application/Espo/Services/App.php +++ b/application/Espo/Services/App.php @@ -129,8 +129,13 @@ class App extends \Espo\Core\Services\Base $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; + } unset($data->$attribute); } From 5fe98cbbbccd37d845c41b17587c316491a203c3 Mon Sep 17 00:00:00 2001 From: yuri Date: Mon, 30 Sep 2019 16:19:22 +0300 Subject: [PATCH 23/44] fix validation --- application/Espo/Services/Record.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/application/Espo/Services/Record.php b/application/Espo/Services/Record.php index 8cc82d5fc3..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; } } From cf65e40b35512a8a5bf32eeedb82bf0c0887a262 Mon Sep 17 00:00:00 2001 From: yuri Date: Mon, 30 Sep 2019 16:20:54 +0300 Subject: [PATCH 24/44] fix case portal acl --- .../Espo/Modules/Crm/Resources/metadata/app/aclPortal.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/application/Espo/Modules/Crm/Resources/metadata/app/aclPortal.json b/application/Espo/Modules/Crm/Resources/metadata/app/aclPortal.json index f3641a7fae..318408439b 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/app/aclPortal.json +++ b/application/Espo/Modules/Crm/Resources/metadata/app/aclPortal.json @@ -36,10 +36,7 @@ "status": { "read": "yes", "edit": "no" - }, - "account": false, - "contacts": false, - "contact": false + } } } } From bf5fe492b36ad1ad11cad7b9674eaa87b66fc999 Mon Sep 17 00:00:00 2001 From: yuri Date: Tue, 1 Oct 2019 10:10:08 +0300 Subject: [PATCH 25/44] user data fix --- application/Espo/Services/App.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/application/Espo/Services/App.php b/application/Espo/Services/App.php index aa3a88a7f4..dce9b9f28b 100644 --- a/application/Espo/Services/App.php +++ b/application/Espo/Services/App.php @@ -135,6 +135,8 @@ class App extends \Espo\Core\Services\Base 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); } From a3f7f4e2d28543292a902dbd275f914e95893193 Mon Sep 17 00:00:00 2001 From: yuri Date: Tue, 1 Oct 2019 11:19:01 +0300 Subject: [PATCH 26/44] fix smtp account field --- .../crm/src/views/mass-email/fields/smtp-account.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) 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; - } + }, }); }); From d01748a24c0a2ab536ac309e7ae3c1eb69c6704c Mon Sep 17 00:00:00 2001 From: yuri Date: Tue, 1 Oct 2019 14:21:38 +0300 Subject: [PATCH 27/44] hasher --- application/Espo/Core/Loaders/Hasher.php | 40 +++++++++++++++ application/Espo/Core/Utils/Hasher.php | 49 +++++++++++++++++++ application/Espo/Core/Utils/Util.php | 8 +++ .../Espo/Core/defaults/systemConfig.php | 1 + install/core/Installer.php | 7 +-- 5 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 application/Espo/Core/Loaders/Hasher.php create mode 100644 application/Espo/Core/Utils/Hasher.php 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/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/install/core/Installer.php b/install/core/Installer.php index daa2c2a513..0db8061f09 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); From 91506a8ca6e2208df4c5f9de9db760a57f6e4777 Mon Sep 17 00:00:00 2001 From: yuri Date: Tue, 1 Oct 2019 14:21:51 +0300 Subject: [PATCH 28/44] unsubscribe without mass email --- .../Crm/EntryPoints/SubscribeAgain.php | 87 +++++++++++++++---- .../Modules/Crm/EntryPoints/Unsubscribe.php | 86 ++++++++++++++---- .../templates/campaign/subscribe-again.tpl | 2 +- .../res/templates/campaign/unsubscribe.tpl | 2 +- .../crm/src/views/campaign/subscribe-again.js | 12 ++- .../crm/src/views/campaign/unsubscribe.js | 12 ++- 6 files changed, 158 insertions(+), 43 deletions(-) diff --git a/application/Espo/Modules/Crm/EntryPoints/SubscribeAgain.php b/application/Espo/Modules/Crm/EntryPoints/SubscribeAgain.php index de9e897a82..6b22bd56ab 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,54 @@ 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); + + $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..d500c558d1 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,54 @@ 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); + + $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/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/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; } From a38fd487f4e1f18fd32fb3267757d5a091bac2c5 Mon Sep 17 00:00:00 2001 From: yuri Date: Tue, 1 Oct 2019 15:43:22 +0300 Subject: [PATCH 29/44] lang fix --- application/Espo/Services/Language.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/application/Espo/Services/Language.php b/application/Espo/Services/Language.php index a416119550..7c140ec0a5 100644 --- a/application/Espo/Services/Language.php +++ b/application/Espo/Services/Language.php @@ -77,7 +77,7 @@ class Language extends \Espo\Core\Services\Base unset($data['Global']['options']); foreach ($data as $k => $item) { - if (in_array($k, ['Global', 'User'])) continue; + if (in_array($k, ['Global', 'User', 'Campaign'])) continue; unset($data[$k]); } unset($data['User']['fields']); @@ -87,6 +87,12 @@ class Language extends \Espo\Core\Services\Base 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'], [])); From 3fe4bc34ef67a5259566dc780cb497b58122526e Mon Sep 17 00:00:00 2001 From: yuri Date: Tue, 1 Oct 2019 15:51:02 +0300 Subject: [PATCH 30/44] fix --- .../Espo/Modules/Crm/EntryPoints/SubscribeAgain.php | 10 ++++++---- .../Espo/Modules/Crm/EntryPoints/Unsubscribe.php | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/application/Espo/Modules/Crm/EntryPoints/SubscribeAgain.php b/application/Espo/Modules/Crm/EntryPoints/SubscribeAgain.php index 6b22bd56ab..1f6e703ce4 100644 --- a/application/Espo/Modules/Crm/EntryPoints/SubscribeAgain.php +++ b/application/Espo/Modules/Crm/EntryPoints/SubscribeAgain.php @@ -182,11 +182,13 @@ class SubscribeAgain extends \Espo\Core\EntryPoints\Base if ($ea) { $entityList = $repository->getEntityListByAddressId($ea->id); - $ea->set('optOut', false); - $this->getEntityManager()->saveEntity($ea); + if ($ea->get('optOut')) { + $ea->set('optOut', false); + $this->getEntityManager()->saveEntity($ea); - foreach ($entityList as $entity) { - $this->getHookManager()->process($entity->getEntityType(), 'afterCancelOptOut', $entity, [], []); + foreach ($entityList as $entity) { + $this->getHookManager()->process($entity->getEntityType(), 'afterCancelOptOut', $entity, [], []); + } } $this->display([ diff --git a/application/Espo/Modules/Crm/EntryPoints/Unsubscribe.php b/application/Espo/Modules/Crm/EntryPoints/Unsubscribe.php index d500c558d1..508a15973f 100644 --- a/application/Espo/Modules/Crm/EntryPoints/Unsubscribe.php +++ b/application/Espo/Modules/Crm/EntryPoints/Unsubscribe.php @@ -175,11 +175,13 @@ class Unsubscribe extends \Espo\Core\EntryPoints\Base if ($ea) { $entityList = $repository->getEntityListByAddressId($ea->id); - $ea->set('optOut', true); - $this->getEntityManager()->saveEntity($ea); + if (!$ea->get('optOut')) { + $ea->set('optOut', true); + $this->getEntityManager()->saveEntity($ea); - foreach ($entityList as $entity) { - $this->getHookManager()->process($entity->getEntityType(), 'afterOptOut', $entity, [], []); + foreach ($entityList as $entity) { + $this->getHookManager()->process($entity->getEntityType(), 'afterOptOut', $entity, [], []); + } } $this->display([ From 9d485624a88f447b72e6516862e333378393985c Mon Sep 17 00:00:00 2001 From: yuri Date: Tue, 1 Oct 2019 16:00:21 +0300 Subject: [PATCH 31/44] fix --- install/core/Installer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/core/Installer.php b/install/core/Installer.php index 0db8061f09..621f722fd0 100644 --- a/install/core/Installer.php +++ b/install/core/Installer.php @@ -260,7 +260,7 @@ class Installer 'siteUrl' => $siteUrl, 'passwordSalt' => $this->getPasswordHash()->generateSalt(), 'cryptKey' => $this->getContainer()->get('crypt')->generateKey(), - 'hashSecretKey' => \Espo\Core\Utils\Util::generateSecretKey(); + 'hashSecretKey' => \Espo\Core\Utils\Util::generateSecretKey(), ]; $owner = $this->getFileManager()->getPermissionUtils()->getDefaultOwner(true); From 20ede11bdd8b6e825a5207a70b13924a9f739809 Mon Sep 17 00:00:00 2001 From: yuri Date: Tue, 1 Oct 2019 16:40:03 +0300 Subject: [PATCH 32/44] css fix --- frontend/less/espo/custom.less | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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; } } From 73b00190f73ef45a75a52699e16a74cbad0904e5 Mon Sep 17 00:00:00 2001 From: yuri Date: Tue, 1 Oct 2019 17:06:32 +0300 Subject: [PATCH 33/44] colorpicker fix --- client/lib/bootstrap-colorpicker.js | 86 +++++++++++++------------- client/src/views/fields/colorpicker.js | 12 +++- 2 files changed, 54 insertions(+), 44 deletions(-) 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/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 () { From 57843f5f86d00f46db1a8674ccd289cfd5688307 Mon Sep 17 00:00:00 2001 From: yuri Date: Tue, 1 Oct 2019 17:13:12 +0300 Subject: [PATCH 34/44] calendar apply color --- client/modules/crm/src/views/calendar/calendar.js | 7 +++++++ client/modules/crm/src/views/calendar/timeline.js | 7 +++++++ 2 files changed, 14 insertions(+) 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 { From 3b880de16d695d6732bc5c34d1236f63e9672f64 Mon Sep 17 00:00:00 2001 From: yuri Date: Wed, 2 Oct 2019 10:43:42 +0300 Subject: [PATCH 35/44] preferences fix --- client/src/views/preferences/record/edit.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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({ From 00230f50d6a0d6f4113e894d61fa59c97a5c6d69 Mon Sep 17 00:00:00 2001 From: yuri Date: Wed, 2 Oct 2019 10:46:19 +0300 Subject: [PATCH 36/44] remove notifications sound disabled --- application/Espo/Resources/layouts/Settings/notifications.json | 3 +-- client/src/views/notification/badge.js | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) 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/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'); From c752e8fc91d43bbff4cf4b899814ca66264ecba0 Mon Sep 17 00:00:00 2001 From: yuri Date: Wed, 2 Oct 2019 11:44:28 +0300 Subject: [PATCH 37/44] job front end fixes --- .../Espo/Resources/layouts/Job/detail.json | 28 +++++++++++++++++++ .../Resources/metadata/entityDefs/Job.json | 2 +- client/src/views/admin/job/fields/name.js | 5 ++-- 3 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 application/Espo/Resources/layouts/Job/detail.json 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/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/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 }); }); - From 2f93dfd653a7b41bed5b1b6e3b45eb8819ef71c9 Mon Sep 17 00:00:00 2001 From: yuri Date: Wed, 2 Oct 2019 12:18:27 +0300 Subject: [PATCH 38/44] array displayAsList --- client/src/views/fields/array.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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(', ') From 43fed19d0776965e17a58c5d325c1fb4c49a8e7d Mon Sep 17 00:00:00 2001 From: yuri Date: Wed, 2 Oct 2019 14:58:20 +0300 Subject: [PATCH 39/44] formula password functions --- .../Functions/PasswordGroup/GenerateType.php | 54 +++++++++++++++++ .../Functions/PasswordGroup/HashType.php | 60 +++++++++++++++++++ .../Espo/Core/Formula/FormulaTest.php | 24 ++++++++ 3 files changed, 138 insertions(+) create mode 100644 application/Espo/Core/Formula/Functions/PasswordGroup/GenerateType.php create mode 100644 application/Espo/Core/Formula/Functions/PasswordGroup/HashType.php 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/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); + } } From 9b2ac56563129958fb5ef6cabb9a36806345f865 Mon Sep 17 00:00:00 2001 From: yuri Date: Thu, 3 Oct 2019 10:27:29 +0300 Subject: [PATCH 40/44] email use index fix --- application/Espo/SelectManagers/Email.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) 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']; } From a5433ec82a199620bcdfc29bf89d83ab45024c26 Mon Sep 17 00:00:00 2001 From: yuri Date: Thu, 3 Oct 2019 13:09:54 +0300 Subject: [PATCH 41/44] stream use index --- application/Espo/Resources/metadata/entityDefs/Note.json | 3 +++ application/Espo/Services/Stream.php | 2 ++ 2 files changed, 5 insertions(+) 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/Services/Stream.php b/application/Espo/Services/Stream.php index a89d9cac06..3df5ed1cff 100644 --- a/application/Espo/Services/Stream.php +++ b/application/Espo/Services/Stream.php @@ -386,6 +386,7 @@ class Stream extends \Espo\Core\Services\Base 'orderBy' => 'number', 'order' => 'DESC', 'limit' => $sqLimit, + 'useIndexList' => ['createdByNumber'], ]; if ($user->isPortal()) { @@ -483,6 +484,7 @@ class Stream extends \Espo\Core\Services\Base 'orderBy' => 'number', 'order' => 'DESC', 'limit' => $sqLimit, + 'useIndexList' => ['createdByNumber'], ]; if ($user->isPortal()) { From 850f6ad41e0d40c4dc892e3abfabd9cbd57ff51b Mon Sep 17 00:00:00 2001 From: yuri Date: Thu, 3 Oct 2019 13:12:51 +0300 Subject: [PATCH 42/44] cs fix --- application/Espo/Services/Stream.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/application/Espo/Services/Stream.php b/application/Espo/Services/Stream.php index 3df5ed1cff..496220cefe 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']; From 415eff0486ab46634efa7558c63b1aced830bdda Mon Sep 17 00:00:00 2001 From: yuri Date: Thu, 3 Oct 2019 13:41:53 +0300 Subject: [PATCH 43/44] fix stream portal user --- application/Espo/Services/Stream.php | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/application/Espo/Services/Stream.php b/application/Espo/Services/Stream.php index 496220cefe..d05924de00 100644 --- a/application/Espo/Services/Stream.php +++ b/application/Espo/Services/Stream.php @@ -1662,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; } From 98e69436d9766da67293f40877ee88c3b8f29f43 Mon Sep 17 00:00:00 2001 From: yuri Date: Thu, 3 Oct 2019 13:42:17 +0300 Subject: [PATCH 44/44] sum related support children --- .../Functions/EntityGroup/SumRelatedType.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) 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'];