diff --git a/application/Espo/Core/Acl.php b/application/Espo/Core/Acl.php index b704690779..f7da34df43 100644 --- a/application/Espo/Core/Acl.php +++ b/application/Espo/Core/Acl.php @@ -108,5 +108,10 @@ class Acl { return $this->getAclManager()->checkUser($this->getUser(), $entity); } + + public function getScopeForbiddenAttributeList($scope, $action = 'read', $thresholdLevel = 'no') + { + return $this->getAclManager()->getScopeForbiddenAttributeList($this->getUser(), $scope, $action, $thresholdLevel); + } } diff --git a/application/Espo/Core/Acl/Base.php b/application/Espo/Core/Acl/Base.php index a1886e2cfb..6b1801ac29 100644 --- a/application/Espo/Core/Acl/Base.php +++ b/application/Espo/Core/Acl/Base.php @@ -188,7 +188,7 @@ class Base implements Injectable } } - if ($entity->hasField('assignedUsersIds') && $entity->hasRelation('assignedUsers')) { + if ($entity->hasAttribute('assignedUsersIds') && $entity->hasRelation('assignedUsers')) { if ($entity->hasLinkMultipleId('assignedUsers', $user->id)) { return true; } @@ -201,7 +201,7 @@ class Base implements Injectable { $userTeamIdList = $user->getLinkMultipleIdList('teams'); - if (!$entity->hasRelation('teams') || !$entity->hasField('teamsIds')) { + if (!$entity->hasRelation('teams') || !$entity->hasAttribute('teamsIds')) { return false; } diff --git a/application/Espo/Core/Acl/Table.php b/application/Espo/Core/Acl/Table.php index beee4525a7..1faf521317 100644 --- a/application/Espo/Core/Acl/Table.php +++ b/application/Espo/Core/Acl/Table.php @@ -32,39 +32,51 @@ namespace Espo\Core\Acl; use \Espo\Core\Exceptions\Error; use \Espo\ORM\Entity; +use \Espo\Entities\User; + +use \Espo\Core\Utils\Config; +use \Espo\Core\Utils\Metadata; +use \Espo\Core\Utils\FieldManager; +use \Espo\Core\Utils\File\Manager as FileManager; class Table { - private $data = null; /*array( - 'table' => new \StdClass(), - 'fieldLevelTable' => array() - );*/ + private $data = null; - protected $cacheFile; + private $cacheFile; protected $actionList = ['read', 'stream', 'edit', 'delete']; protected $levelList = ['all', 'team', 'own', 'no']; - protected $fieldLevelActionList = ['read', 'edit']; + protected $fieldActionList = ['read', 'edit']; - protected $fieldLevelLevelList = ['yes', 'no']; + protected $fieldLevelList = ['yes', 'no']; - protected $fileManager; + private $fileManager; - protected $metadata; + private $metadata; - public function __construct(\Espo\Entities\User $user, $config = null, $fileManager = null, $metadata = null) + private $fieldManager; + + protected $forbiddenAttributesCache = array(); + + public function __construct(User $user, Config $config = null, FileManager $fileManager = null, Metadata $metadata = null, FieldManager $fieldManager = null) { $this->data = (object) [ 'table' => (object) [], - 'fieldLevelTable' => (object) [] + 'fieldTable' => (object) [], + 'attributeTable' => (object) [], ]; $this->user = $user; $this->metadata = $metadata; + if ($fieldManager) { + $this->fieldManager = $fieldManager; + } + if (!$this->user->isFetched()) { throw new Error('User must be fetched before ACL check.'); } @@ -88,6 +100,21 @@ class Table } } + protected function getMetadata() + { + return $this->metadata; + } + + protected function getFieldManager() + { + return $this->fieldManager; + } + + protected function getConfig() + { + return $this->config; + } + public function getMap() { return $this->data; @@ -131,7 +158,7 @@ class Table private function load() { $aclTableList = []; - $fieldLevelTableList = []; + $fieldTableList = []; $assignmentPermissionList = []; $userPermissionList = []; @@ -140,7 +167,7 @@ class Table foreach ($userRoles as $role) { $aclTableList[] = $role->get('data'); - $fieldLevelTableList[] = $role->get('fieldLevelData'); + $fieldTableList[] = $role->get('fieldData'); $assignmentPermissionList[] = $role->get('assignmentPermission'); $userPermissionList[] = $role->get('userPermission'); } @@ -150,23 +177,23 @@ class Table $teamRoles = $team->get('roles'); foreach ($teamRoles as $role) { $aclTableList[] = $role->get('data'); - $fieldLevelTableList[] = $role->get('fieldLevelData'); + $fieldTableList[] = $role->get('fieldData'); $assignmentPermissionList[] = $role->get('assignmentPermission'); $userPermissionList[] = $role->get('userPermission'); } } $aclTable = $this->mergeTableList($aclTableList); - $fieldLevelTable = $this->mergeFieldLevelTableList($fieldLevelTableList); + $fieldTable = $this->mergefieldTableList($fieldTableList); foreach ($this->getScopeList() as $scope) { if ($this->metadata->get('scopes.' . $scope . '.disabled')) { $aclTable->$scope = false; - unset($fieldLevelTable->$scope); + unset($fieldTable->$scope); } } - $this->applySolid($aclTable, $fieldLevelTable); + $this->applySolid($aclTable, $fieldTable); } else { $aclTable = (object) []; foreach ($this->getScopeList() as $scope) { @@ -181,7 +208,7 @@ class Table } } } - $fieldLevelTable = (object) []; + $fieldTable = (object) []; } foreach ($aclTable as $scope => $data) { @@ -193,7 +220,9 @@ class Table } $this->data->table = $aclTable; - $this->data->fieldLevelTable = $fieldLevelTable; + $this->data->fieldTable = $fieldTable; + + $this->fillAttributeTable(); if (!$this->user->isAdmin()) { $this->data->assignmentPermission = $this->mergeValueList($assignmentPermissionList, $this->metadata->get('app.acl.valueDefaults.assignmentPermission', 'all')); @@ -204,7 +233,77 @@ class Table } } - private function applySolid(&$table, &$fieldLevelTable) + public function getScopeForbiddenAttributeList($scope, $action = 'read', $thresholdLevel = 'no') + { + $key = $scope . '_'. $thresholdLevel; + if (isset($this->forbiddenAttributesCache[$key])) { + return $this->forbiddenAttributesCache[$key]; + } + + $attributeTable = $this->data->attributeTable; + + if (!isset($attributeTable->$scope) || !isset($attributeTable->$scope->$action)) { + $this->forbiddenAttributesCache[$key] = []; + return []; + } + + $levelList = []; + foreach ($this->fieldLevelList as $level) { + if (array_search($level, $this->fieldLevelList) >= array_search($thresholdLevel, $this->fieldLevelList)) { + $levelList[] = $level; + } + } + + $attributeList = []; + + foreach ($levelList as $level) { + if (!isset($attributeTable->$scope->$action->$level)) continue; + foreach ($attributeTable->$scope->$action->$level as $attribute) { + $attributeList[] = $attribute; + } + } + + $this->forbiddenAttributesCache[$key] = $attributeList; + + return $attributeList; + } + + protected function fillAttributeTable() + { + $fieldTable = $this->data->fieldTable; + + $attributeTable = (object) []; + + foreach (get_object_vars($fieldTable) as $scope => $scopeData) { + $attributeTable->$scope = (object) []; + + foreach ($this->fieldActionList as $action) { + $attributeTable->$scope->$action = (object) []; + foreach ($this->fieldLevelList as $level) { + $attributeTable->$scope->$action->$level = []; + } + } + + foreach (get_object_vars($scopeData) as $field => $fieldData) { + $attributeList = $this->getFieldManager()->getAttributeList($scope, $field); + + foreach ($this->fieldActionList as $action) { + if (!isset($fieldData->$action)) continue; + foreach ($this->fieldLevelList as $level) { + if ($fieldData->$action === $level) { + foreach ($attributeList as $attribute) { + $attributeTable->$scope->$action->{$level}[] = $attribute; + } + } + } + } + } + } + + $this->data->attributeTable = $attributeTable; + } + + protected function applySolid(&$table, &$fieldTable) { if (!$this->metadata) { return; @@ -245,7 +344,7 @@ class Table return $result; } - private function getScopeWithAclList() + protected function getScopeWithAclList() { $scopeList = []; $scopes = $this->metadata->get('scopes'); @@ -257,7 +356,7 @@ class Table return $scopeList; } - private function getScopeList() + protected function getScopeList() { $scopeList = []; $scopes = $this->metadata->get('scopes'); @@ -336,7 +435,7 @@ class Table return $data; } - private function mergeFieldLevelTableList(array $tableList) + private function mergefieldTableList(array $tableList) { $data = (object) []; $scopeList = $this->getScopeWithAclList(); @@ -358,14 +457,14 @@ class Table $data->$scope->$field = (object) []; } - foreach ($this->fieldLevelActionList as $i => $action) { + foreach ($this->fieldActionList as $i => $action) { if (!isset($row->$action)) continue; $level = $row->$action; if (!isset($data->$scope->$field->$action)) { $data->$scope->$field->$action = $level; } else { - if (array_search($data->$scope->$field->$action, $this->fieldLevelLevelList) > array_search($level, $this->fieldLevelLevelList)) { + if (array_search($data->$scope->$field->$action, $this->fieldLevelList) > array_search($level, $this->fieldLevelList)) { $data->$scope->$field->$action = $level; } } diff --git a/application/Espo/Core/AclManager.php b/application/Espo/Core/AclManager.php index 99a27c806d..c25ca41346 100644 --- a/application/Espo/Core/AclManager.php +++ b/application/Espo/Core/AclManager.php @@ -97,8 +97,9 @@ class AclManager $config = $this->getContainer()->get('config'); $fileManager = $this->getContainer()->get('fileManager'); $metadata = $this->getContainer()->get('metadata'); + $fieldManager = $this->getContainer()->get('fieldManager'); - $this->tableHashMap[$key] = new \Espo\Core\Acl\Table($user, $config, $fileManager, $metadata); + $this->tableHashMap[$key] = new \Espo\Core\Acl\Table($user, $config, $fileManager, $metadata, $fieldManager); } return $this->tableHashMap[$key]; @@ -218,5 +219,11 @@ class AclManager } return true; } + + public function getScopeForbiddenAttributeList(User $user, $scope, $action = 'read', $thresholdLevel = 'no') + { + if ($user->isAdmin()) return []; + return $this->getTable($user)->getScopeForbiddenAttributeList($scope, $action, $thresholdLevel); + } } diff --git a/application/Espo/Core/ORM/Entity.php b/application/Espo/Core/ORM/Entity.php index 88dd79b5a7..0ba6825a8c 100644 --- a/application/Espo/Core/ORM/Entity.php +++ b/application/Espo/Core/ORM/Entity.php @@ -34,7 +34,7 @@ class Entity extends \Espo\ORM\Entity public function loadLinkMultipleField($field, $columns = null) { - if ($this->hasRelation($field) && $this->hasField($field . 'Ids')) { + if ($this->hasRelation($field) && $this->hasAttribute($field . 'Ids')) { $defs = array(); if (!empty($columns)) { @@ -127,7 +127,7 @@ class Entity extends \Espo\ORM\Entity { $idsField = $field . 'Ids'; - if (!$this->hasField($idsField)) return null; + if (!$this->hasAttribute($idsField)) return null; if (!$this->has($idsField)) { if (!$this->isNew()) { @@ -145,7 +145,7 @@ class Entity extends \Espo\ORM\Entity { $idsField = $field . 'Ids'; - if (!$this->hasField($idsField)) return null; + if (!$this->hasAttribute($idsField)) return null; if (!$this->has($idsField)) { if (!$this->isNew()) { diff --git a/application/Espo/Core/ORM/Repositories/RDB.php b/application/Espo/Core/ORM/Repositories/RDB.php index 0c6a54e1b0..4ee39c5e05 100644 --- a/application/Espo/Core/ORM/Repositories/RDB.php +++ b/application/Espo/Core/ORM/Repositories/RDB.php @@ -153,10 +153,10 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable $this->getEntityManager()->getHookManager()->process($this->entityName, 'beforeRemove', $entity, $options); $nowString = date('Y-m-d H:i:s', time()); - if ($entity->hasField('modifiedAt')) { + if ($entity->hasAttribute('modifiedAt')) { $entity->set('modifiedAt', $nowString); } - if ($entity->hasField('modifiedById')) { + if ($entity->hasAttribute('modifiedById')) { $entity->set('modifiedById', $this->getEntityManager()->getUser()->id); } } @@ -208,13 +208,13 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable $entity->set('id', Util::generateId()); } - if ($entity->hasField('createdAt')) { + if ($entity->hasAttribute('createdAt')) { $entity->set('createdAt', $nowString); } - if ($entity->hasField('modifiedAt')) { + if ($entity->hasAttribute('modifiedAt')) { $entity->set('modifiedAt', $nowString); } - if ($entity->hasField('createdById')) { + if ($entity->hasAttribute('createdById')) { $entity->set('createdById', $this->entityManager->getUser()->id); } @@ -227,10 +227,10 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable $entity->clear('modifiedById'); } else { if (empty($options['silent'])) { - if ($entity->hasField('modifiedAt')) { + if ($entity->hasAttribute('modifiedAt')) { $entity->set('modifiedAt', $nowString); } - if ($entity->hasField('modifiedById')) { + if ($entity->hasAttribute('modifiedById')) { $entity->set('modifiedById', $this->entityManager->getUser()->id); } } @@ -253,14 +253,14 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable protected function handleEmailAddressSave(Entity $entity) { - if ($entity->hasRelation('emailAddresses') && $entity->hasField('emailAddress')) { + if ($entity->hasRelation('emailAddresses') && $entity->hasAttribute('emailAddress')) { $emailAddressRepository = $this->getEntityManager()->getRepository('EmailAddress')->storeEntityEmailAddress($entity); } } protected function handlePhoneNumberSave(Entity $entity) { - if ($entity->hasRelation('phoneNumbers') && $entity->hasField('phoneNumber')) { + if ($entity->hasRelation('phoneNumbers') && $entity->hasAttribute('phoneNumber')) { $emailAddressRepository = $this->getEntityManager()->getRepository('PhoneNumber')->storeEntityPhoneNumber($entity); } } diff --git a/application/Espo/Core/SelectManagers/Base.php b/application/Espo/Core/SelectManagers/Base.php index 9947eb7fa2..735c0b99bd 100644 --- a/application/Espo/Core/SelectManagers/Base.php +++ b/application/Espo/Core/SelectManagers/Base.php @@ -115,9 +115,9 @@ class Base } } - protected function getTextFilterFields() + protected function getTextFilterFieldList() { - return $this->metadata->get("entityDefs.{$this->entityType}.collection.textFilterFields", array('name')); + return $this->metadata->get("entityDefs.{$this->entityType}.collection.textFilterFields", ['name']); } protected function getSeed() @@ -395,28 +395,28 @@ class Base protected function hasAssignedUsersField() { - if ($this->getSeed()->hasRelation('assignedUsers') && $this->getSeed()->hasField('assignedUsersIds')) { + if ($this->getSeed()->hasRelation('assignedUsers') && $this->getSeed()->hasAttribute('assignedUsersIds')) { return true; } } protected function hasAssignedUserField() { - if ($this->getSeed()->hasField('assignedUserId')) { + if ($this->getSeed()->hasAttribute('assignedUserId')) { return true; } } protected function hasCreatedByField() { - if ($this->getSeed()->hasField('createdById')) { + if ($this->getSeed()->hasAttribute('createdById')) { return true; } } protected function hasTeamsField() { - if ($this->getSeed()->hasRelation('teams') && $this->getSeed()->hasField('teamsIds')) { + if ($this->getSeed()->hasRelation('teams') && $this->getSeed()->hasAttribute('teamsIds')) { return true; } } @@ -936,8 +936,8 @@ class Base protected function textFilter($textFilter, &$result) { - $fieldDefs = $this->getSeed()->getFields(); - $fieldList = $this->getTextFilterFields(); + $fieldDefs = $this->getSeed()->getAttributes(); + $fieldList = $this->getTextFilterFieldList(); $d = array(); foreach ($fieldList as $field) { diff --git a/application/Espo/Core/Utils/FieldManager.php b/application/Espo/Core/Utils/FieldManager.php index 543aceae28..46c37b07b3 100644 --- a/application/Espo/Core/Utils/FieldManager.php +++ b/application/Espo/Core/Utils/FieldManager.php @@ -285,4 +285,55 @@ class FieldManager return false; } + private function getAttributeListByType($scope, $name, $type) + { + $fieldType = $this->getMetadata()->get('entityDefs.' . $scope . '.fields.' . $name . '.type'); + if (!$fieldType) return []; + + $defs = $this->getMetadata()->get('fields.' . $fieldType); + if (!$defs) return []; + if (is_object($defs)) { + $defs = get_object_vars($defs); + } + + $fieldList = []; + + if (isset($defs[$type . 'Fields'])) { + $list = $defs[$type . 'Fields']; + $naming = 'suffix'; + if (isset($defs['naming'])) { + $naming = $defs['naming']; + } + if ($naming == 'prefix') { + foreach ($list as $f) { + $fieldList[] = $f . ucfirst($name); + } + } else { + foreach ($list as $f) { + $fieldList[] = $name . ucfirst($f); + } + } + } else { + if ($type == 'actual') { + $fieldList[] = $name; + } + } + + return $fieldList; + } + + public function getActualAttributeList($scope, $name) + { + return $this->getAttributeListByType($scope, $name, 'actual'); + } + + public function getNotActualAttributeList($scope, $name) + { + return $this->getAttributeListByType($scope, $name, 'notActual'); + } + + public function getAttributeList($scope, $name) + { + return array_merge($this->getActualAttributeList($scope, $name), $this->getNotActualAttributeList($scope, $name)); + } } \ No newline at end of file diff --git a/application/Espo/Modules/Crm/Services/Opportunity.php b/application/Espo/Modules/Crm/Services/Opportunity.php index 92d97adfed..01208e1425 100644 --- a/application/Espo/Modules/Crm/Services/Opportunity.php +++ b/application/Espo/Modules/Crm/Services/Opportunity.php @@ -165,7 +165,7 @@ class Opportunity extends \Espo\Services\Record foreach ($rows as $row) { $result[$row['month']] = floatval($row['amount']); } - + return $result; } diff --git a/application/Espo/ORM/Entity.php b/application/Espo/ORM/Entity.php index 517b23f12e..25d7db2df2 100644 --- a/application/Espo/ORM/Entity.php +++ b/application/Espo/ORM/Entity.php @@ -28,6 +28,7 @@ ************************************************************************/ namespace Espo\ORM; + abstract class Entity implements IEntity { public $id = null; @@ -120,7 +121,7 @@ abstract class Entity implements IEntity if ($name == 'id') { $this->id = $value; } - if ($this->hasField($name)) { + if ($this->hasAttribute($name)) { $method = '_set' . ucfirst($name); if (method_exists($this, $method)) { $this->$method($value); @@ -175,7 +176,7 @@ abstract class Entity implements IEntity $this->reset(); } - foreach ($this->fields as $field => $fieldDefs) { + foreach ($this->getAttributes() as $field => $fieldDefs) { if (array_key_exists($field, $arr)) { if ($field == 'id') { $this->id = $arr[$field]; @@ -264,6 +265,11 @@ abstract class Entity implements IEntity return isset($this->fields[$fieldName]); } + public function hasAttribute($name) + { + return isset($this->fields[$name]); + } + public function hasRelation($relationName) { return isset($this->relations[$relationName]); @@ -292,6 +298,11 @@ abstract class Entity implements IEntity return $this->fields; } + public function getAttributes() + { + return $this->fields; + } + public function getRelations() { return $this->relations; @@ -307,6 +318,11 @@ abstract class Entity implements IEntity return $this->has($fieldName) && ($this->get($fieldName) != $this->getFetched($fieldName)); } + public function isAttributeChanged($fieldName) + { + return $this->has($fieldName) && ($this->get($fieldName) != $this->getFetched($fieldName)); + } + public function setFetched($fieldName, $value) { $this->fetchedValuesContainer[$fieldName] = $value; diff --git a/application/Espo/Resources/metadata/entityDefs/Role.json b/application/Espo/Resources/metadata/entityDefs/Role.json index 8e19a8a9c8..6b3a318b66 100644 --- a/application/Espo/Resources/metadata/entityDefs/Role.json +++ b/application/Espo/Resources/metadata/entityDefs/Role.json @@ -22,7 +22,7 @@ "data": { "type": "jsonObject" }, - "fieldLevelData": { + "fieldData": { "type": "jsonObject" } }, diff --git a/application/Espo/Services/Attachment.php b/application/Espo/Services/Attachment.php index 685bc2dbb0..d327583b73 100644 --- a/application/Espo/Services/Attachment.php +++ b/application/Espo/Services/Attachment.php @@ -31,8 +31,7 @@ namespace Espo\Services; class Attachment extends Record { - - protected $notFilteringFields = array('contents'); + protected $notFilteringAttributeList = ['contents']; public function createEntity($data) { diff --git a/application/Espo/Services/AuthToken.php b/application/Espo/Services/AuthToken.php index 19d3fbe1cc..22b9a83262 100644 --- a/application/Espo/Services/AuthToken.php +++ b/application/Espo/Services/AuthToken.php @@ -35,6 +35,6 @@ use \Espo\Core\Exceptions\NotFound; class AuthToken extends Record { - protected $internalFields = array('hash', 'token'); + protected $internalAttributeList = ['hash', 'token']; } diff --git a/application/Espo/Services/EmailAccount.php b/application/Espo/Services/EmailAccount.php index d29ee57488..ba82a72510 100644 --- a/application/Espo/Services/EmailAccount.php +++ b/application/Espo/Services/EmailAccount.php @@ -37,9 +37,9 @@ use \Zend\Mail\Storage; class EmailAccount extends Record { - protected $internalFields = array('password'); + protected $internalAttributeList = ['password']; - protected $readOnlyFields = array('fetchData'); + protected $readOnlyAttributeList= ['fetchData']; const PORTION_LIMIT = 10; diff --git a/application/Espo/Services/InboundEmail.php b/application/Espo/Services/InboundEmail.php index 924b2b3148..f79e3ad541 100644 --- a/application/Espo/Services/InboundEmail.php +++ b/application/Espo/Services/InboundEmail.php @@ -36,7 +36,7 @@ use \Espo\Core\Exceptions\Forbidden; class InboundEmail extends \Espo\Services\Record { - protected $internalFields = array('password'); + protected $internalAttributeList = ['password']; private $campaignService = null; diff --git a/application/Espo/Services/Record.php b/application/Espo/Services/Record.php index 4a9d178017..c97ae0501c 100644 --- a/application/Espo/Services/Record.php +++ b/application/Espo/Services/Record.php @@ -62,19 +62,19 @@ class Record extends \Espo\Core\Services\Base private $streamService; - protected $notFilteringFields = array(); // TODO maybe remove it + protected $notFilteringAttributeList =[]; // TODO maybe remove it - protected $internalFields = array(); + protected $internalAttributeList = []; - protected $readOnlyFields = array(); + protected $readOnlyAttributeList = []; - protected $linkSelectParams = array(); + protected $linkSelectParams = []; - protected $mergeLinkList = array(); + protected $mergeLinkList = []; - protected $exportSkipFieldList = array(); + protected $exportSkipAttributeList = []; - protected $exportAdditionalFieldList = array(); + protected $exportAdditionalAttributeList = []; const FOLLOWERS_LIMIT = 4; @@ -244,7 +244,7 @@ class Record extends \Espo\Core\Services\Base if (!empty($defs['noJoin']) && !empty($defs['entity'])) { $nameField = $link . 'Name'; $idField = $link . 'Id'; - if ($entity->hasField($nameField) && $entity->hasField($idField)) { + if ($entity->hasAttribute($nameField) && $entity->hasAttribute($idField)) { $id = $entity->get($idField); } @@ -308,8 +308,8 @@ class Record extends \Espo\Core\Services\Base protected function isValid($entity) { - $fieldDefs = $entity->getFields(); - if ($entity->hasField('name') && !empty($fieldDefs['name']['required'])) { + $fieldDefs = $entity->getAttributes(); + if ($entity->hasAttribute('name') && !empty($fieldDefs['name']['required'])) { if (!$entity->get('name')) { return false; } @@ -331,7 +331,7 @@ class Record extends \Espo\Core\Services\Base public function isPermittedAssignedUser(Entity $entity) { - if (!$entity->hasField('assignedUserId')) { + if (!$entity->hasAttribute('assignedUserId')) { return true; } @@ -381,7 +381,7 @@ class Record extends \Espo\Core\Services\Base return true; } - if (!$entity->hasField('teamsIds')) { + if (!$entity->hasAttribute('teamsIds')) { return true; } $teamIds = $entity->get('teamsIds'); @@ -424,12 +424,12 @@ class Record extends \Espo\Core\Services\Base return strip_tags($string, '