mirror of
https://github.com/espocrm/espocrm.git
synced 2026-06-28 15:06:06 +00:00
1936 lines
53 KiB
PHP
1936 lines
53 KiB
PHP
<?php
|
|
/************************************************************************
|
|
* This file is part of EspoCRM.
|
|
*
|
|
* EspoCRM - Open Source CRM application.
|
|
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
|
* Website: https://www.espocrm.com
|
|
*
|
|
* EspoCRM is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* EspoCRM is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
|
*
|
|
* The interactive user interfaces in modified source and object code versions
|
|
* of this program must display Appropriate Legal Notices, as required under
|
|
* Section 5 of the GNU General Public License version 3.
|
|
*
|
|
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
|
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
|
************************************************************************/
|
|
|
|
namespace Espo\Core\Record;
|
|
|
|
use Espo\Core\Exceptions\{
|
|
Error,
|
|
BadRequest,
|
|
NotFound,
|
|
Forbidden,
|
|
NotFoundSilent,
|
|
ForbiddenSilent,
|
|
ConflictSilent,
|
|
};
|
|
|
|
use Espo\ORM\{
|
|
Entity,
|
|
Repository\Repository,
|
|
Collection,
|
|
EntityManager,
|
|
};
|
|
|
|
use Espo\Entities\User;
|
|
|
|
use Espo\Services\Stream as StreamService;
|
|
|
|
use Espo\Core\{
|
|
Acl,
|
|
Acl\Table as AclTable,
|
|
Select\SearchParams,
|
|
Select\SelectBuilderFactory,
|
|
Record\Crud,
|
|
Record\Collection as RecordCollection,
|
|
FieldValidation\Params as FieldValidationParams,
|
|
FieldProcessing\ReadLoadProcessor,
|
|
FieldProcessing\ListLoadProcessor,
|
|
FieldProcessing\LoaderParams as FieldLoaderParams,
|
|
};
|
|
|
|
use Espo\Core\Di;
|
|
|
|
use StdClass;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* The layer between a controller and ORM repository. For CRUD and other operations with records.
|
|
* Access control is processed here.
|
|
*/
|
|
class Service implements Crud,
|
|
|
|
Di\ConfigAware,
|
|
Di\ServiceFactoryAware,
|
|
Di\EntityManagerAware,
|
|
Di\UserAware,
|
|
Di\MetadataAware,
|
|
Di\AclAware,
|
|
Di\InjectableFactoryAware,
|
|
Di\FieldUtilAware,
|
|
Di\FieldValidationManagerAware,
|
|
Di\RecordServiceContainerAware,
|
|
Di\SelectBuilderFactoryAware
|
|
{
|
|
use Di\ConfigSetter;
|
|
use Di\ServiceFactorySetter;
|
|
use Di\EntityManagerSetter;
|
|
use Di\UserSetter;
|
|
use Di\MetadataSetter;
|
|
use Di\AclSetter;
|
|
use Di\InjectableFactorySetter;
|
|
use Di\FieldUtilSetter;
|
|
use Di\FieldValidationManagerSetter;
|
|
use Di\RecordServiceContainerSetter;
|
|
use Di\SelectBuilderFactorySetter;
|
|
|
|
protected $getEntityBeforeUpdate = false;
|
|
|
|
protected $entityType = null;
|
|
|
|
private $streamService = null;
|
|
|
|
protected $notFilteringAttributeList = []; // TODO maybe remove it
|
|
|
|
protected $forbiddenAttributeList = [];
|
|
|
|
protected $internalAttributeList = [];
|
|
|
|
protected $onlyAdminAttributeList = [];
|
|
|
|
protected $readOnlyAttributeList = [];
|
|
|
|
protected $nonAdminReadOnlyAttributeList = [];
|
|
|
|
protected $forbiddenLinkList = [];
|
|
|
|
protected $internalLinkList = [];
|
|
|
|
protected $readOnlyLinkList = [];
|
|
|
|
protected $nonAdminReadOnlyLinkList = [];
|
|
|
|
protected $onlyAdminLinkList = [];
|
|
|
|
protected $linkParams = [];
|
|
|
|
protected $linkMandatorySelectAttributeList = [];
|
|
|
|
protected $noEditAccessRequiredLinkList = [];
|
|
|
|
protected $noEditAccessRequiredForLink = false;
|
|
|
|
protected $checkForDuplicatesInUpdate = false;
|
|
|
|
protected $actionHistoryDisabled = false;
|
|
|
|
protected $duplicatingLinkList = [];
|
|
|
|
protected $listCountQueryDisabled = false;
|
|
|
|
protected $maxSelectTextAttributeLength = null;
|
|
|
|
protected $maxSelectTextAttributeLengthDisabled = false;
|
|
|
|
protected $selectAttributeList = null;
|
|
|
|
protected $mandatorySelectAttributeList = [];
|
|
|
|
protected $forceSelectAllAttributes = false;
|
|
|
|
protected $validateSkipFieldList = [];
|
|
|
|
/**
|
|
* @todo Move to metadata.
|
|
*/
|
|
protected $validateRequiredSkipFieldList = [];
|
|
|
|
protected $findDuplicatesSelectAttributeList = ['id', 'name'];
|
|
|
|
protected $duplicateIgnoreFieldList = [];
|
|
|
|
protected $duplicateIgnoreAttributeList = [];
|
|
|
|
/**
|
|
* @var Acl
|
|
*/
|
|
protected $acl = null;
|
|
|
|
/**
|
|
* @var User
|
|
*/
|
|
protected $user = null;
|
|
|
|
/**
|
|
* @var EntityManager
|
|
*/
|
|
protected $entityManager;
|
|
|
|
/**
|
|
* @var SelectBuilderFactory
|
|
*/
|
|
protected $selectBuilderFactory;
|
|
|
|
private $listLoadProcessor;
|
|
|
|
protected const MAX_SELECT_TEXT_ATTRIBUTE_LENGTH = 5000;
|
|
|
|
protected const FIND_DUPLICATES_LIMIT = 10;
|
|
|
|
public function __construct()
|
|
{
|
|
|
|
}
|
|
|
|
public function setEntityType(string $entityType): void
|
|
{
|
|
if ($this->entityType && $this->entityType !== $entityType) {
|
|
throw new RuntimeException("entityType is already set.");
|
|
}
|
|
|
|
if ($this->entityType) {
|
|
return;
|
|
}
|
|
|
|
$this->entityType = $entityType;
|
|
}
|
|
|
|
protected function getRepository(): Repository
|
|
{
|
|
return $this->entityManager->getRepository($this->entityType);
|
|
}
|
|
|
|
public function processActionHistoryRecord(string $action, Entity $entity): void
|
|
{
|
|
if ($this->actionHistoryDisabled) {
|
|
return;
|
|
}
|
|
|
|
if ($this->config->get('actionHistoryDisabled')) {
|
|
return;
|
|
}
|
|
|
|
$historyRecord = $this->entityManager->getEntity('ActionHistoryRecord');
|
|
|
|
$historyRecord->set('action', $action);
|
|
$historyRecord->set('userId', $this->user->id);
|
|
$historyRecord->set('authTokenId', $this->user->get('authTokenId'));
|
|
$historyRecord->set('ipAddress', $this->user->get('ipAddress'));
|
|
$historyRecord->set('authLogRecordId', $this->user->get('authLogRecordId'));
|
|
|
|
if ($entity) {
|
|
$historyRecord->set([
|
|
'targetType' => $entity->getEntityType(),
|
|
'targetId' => $entity->id
|
|
]);
|
|
}
|
|
|
|
$this->entityManager->saveEntity($historyRecord);
|
|
}
|
|
|
|
/**
|
|
* Read a record by ID. Access control check is performed.
|
|
*
|
|
* @throws Error
|
|
* @throws NotFoundSilent If no read access.
|
|
*/
|
|
public function read(string $id): Entity
|
|
{
|
|
if (!$this->acl->check($this->entityType, AclTable::ACTION_READ)) {
|
|
throw new ForbiddenSilent();
|
|
}
|
|
|
|
if (empty($id)) {
|
|
throw new Error("No ID passed.");
|
|
}
|
|
|
|
$entity = $this->getEntity($id);
|
|
|
|
if (!$entity) {
|
|
throw new NotFoundSilent("Record {$id} does not exist.");
|
|
}
|
|
|
|
$this->processActionHistoryRecord('read', $entity);
|
|
|
|
return $entity;
|
|
}
|
|
|
|
/**
|
|
* Get an entity by ID. Access control check is performed.
|
|
* If ID is not specified then it will return an empty entity.
|
|
*
|
|
* @throws ForbiddenSilent If no read access.
|
|
*/
|
|
public function getEntity(?string $id = null): ?Entity
|
|
{
|
|
if ($id === null) {
|
|
return $this->getRepository()->getNew();
|
|
}
|
|
|
|
$entity = $this->getRepository()->getById($id);
|
|
|
|
if (!$entity && $this->user->isAdmin()) {
|
|
$entity = $this->getEntityEvenDeleted($id);
|
|
}
|
|
|
|
if (!$entity) {
|
|
return null;
|
|
}
|
|
|
|
$this->loadAdditionalFields($entity);
|
|
|
|
if (!$this->acl->check($entity, AclTable::ACTION_READ)) {
|
|
throw new ForbiddenSilent("No 'read' access.");
|
|
}
|
|
|
|
$this->prepareEntityForOutput($entity);
|
|
|
|
return $entity;
|
|
}
|
|
|
|
protected function getStreamService(): StreamService
|
|
{
|
|
if (empty($this->streamService)) {
|
|
$this->streamService = $this->serviceFactory->create('Stream');
|
|
}
|
|
|
|
return $this->streamService;
|
|
}
|
|
|
|
private function createReadLoadProcessor(): ReadLoadProcessor
|
|
{
|
|
return $this->injectableFactory->create(ReadLoadProcessor::class);
|
|
}
|
|
|
|
private function getListLoadProcessor(): ListLoadProcessor
|
|
{
|
|
if (!$this->listLoadProcessor) {
|
|
$this->listLoadProcessor = $this->injectableFactory->create(ListLoadProcessor::class);
|
|
}
|
|
|
|
return $this->listLoadProcessor;
|
|
}
|
|
|
|
public function loadAdditionalFields(Entity $entity)
|
|
{
|
|
$loadProcessor = $this->createReadLoadProcessor();
|
|
|
|
$loadProcessor->process($entity);
|
|
}
|
|
|
|
private function loadListAdditionalFields(Entity $entity, ?SearchParams $searchParams): void
|
|
{
|
|
$params = new FieldLoaderParams();
|
|
|
|
if ($searchParams && $searchParams->getSelect()) {
|
|
$params = $params->withSelect($searchParams->getSelect());
|
|
}
|
|
|
|
$loadProcessor = $this->getListLoadProcessor();
|
|
|
|
$loadProcessor->process($entity, $params);
|
|
}
|
|
|
|
public function loadAdditionalFieldsForExport(Entity $entity)
|
|
{
|
|
}
|
|
|
|
/**
|
|
* @return void
|
|
* @throws BadRequest
|
|
*/
|
|
public function processValidation(Entity $entity, $data)
|
|
{
|
|
$params = FieldValidationParams
|
|
::fromNothing()
|
|
->withSkipFieldList($this->validateSkipFieldList)
|
|
->withTypeSkipFieldList('required', $this->validateRequiredSkipFieldList);
|
|
|
|
$this->fieldValidationManager->process($entity, $data, $params);
|
|
}
|
|
|
|
/**
|
|
* @return void
|
|
* @throws Forbidden
|
|
*/
|
|
public function processAssignmentCheck(Entity $entity)
|
|
{
|
|
if (!$this->checkAssignment($entity)) {
|
|
throw new Forbidden("Assignment failure: assigned user or team not allowed.");
|
|
}
|
|
}
|
|
|
|
public function checkAssignment(Entity $entity): bool
|
|
{
|
|
if (!$this->isPermittedAssignedUser($entity)) {
|
|
return false;
|
|
}
|
|
|
|
if (!$this->isPermittedTeams($entity)) {
|
|
return false;
|
|
}
|
|
|
|
if ($entity->hasLinkMultipleField('assignedUsers')) {
|
|
if (!$this->isPermittedAssignedUsers($entity)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public function isPermittedAssignedUsers(Entity $entity): bool
|
|
{
|
|
if (!$entity->hasLinkMultipleField('assignedUsers')) {
|
|
return true;
|
|
}
|
|
|
|
if ($this->user->isPortal()) {
|
|
if (count($entity->getLinkMultipleIdList('assignedUsers')) === 0) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
$assignmentPermission = $this->acl->get('assignmentPermission');
|
|
|
|
if (
|
|
$assignmentPermission === true ||
|
|
$assignmentPermission === AclTable::LEVEL_YES ||
|
|
!in_array($assignmentPermission, [AclTable::LEVEL_TEAM, AclTable::LEVEL_NO])
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
$toProcess = false;
|
|
|
|
if (!$entity->isNew()) {
|
|
$userIdList = $entity->getLinkMultipleIdList('assignedUsers');
|
|
|
|
if ($entity->isAttributeChanged('assignedUsersIds')) {
|
|
$toProcess = true;
|
|
}
|
|
}
|
|
else {
|
|
$toProcess = true;
|
|
}
|
|
|
|
$userIdList = $entity->getLinkMultipleIdList('assignedUsers');
|
|
|
|
if ($toProcess) {
|
|
if (empty($userIdList)) {
|
|
if ($assignmentPermission == AclTable::LEVEL_NO && !$this->user->isApi()) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
$fetchedAssignedUserIdList = $entity->getFetched('assignedUsersIds');
|
|
|
|
if ($assignmentPermission === AclTable::LEVEL_NO) {
|
|
foreach ($userIdList as $userId) {
|
|
if (!$entity->isNew() && in_array($userId, $fetchedAssignedUserIdList)) {
|
|
continue;
|
|
}
|
|
|
|
if ($this->user->id != $userId) {
|
|
return false;
|
|
}
|
|
}
|
|
} else if ($assignmentPermission === AclTable::LEVEL_TEAM) {
|
|
$teamIdList = $this->user->getLinkMultipleIdList('teams');
|
|
|
|
foreach ($userIdList as $userId) {
|
|
if (!$entity->isNew() && in_array($userId, $fetchedAssignedUserIdList)) {
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
!$this->entityManager
|
|
->getRepository('User')
|
|
->checkBelongsToAnyOfTeams($userId, $teamIdList)
|
|
) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public function isPermittedAssignedUser(Entity $entity): bool
|
|
{
|
|
if (!$entity->hasAttribute('assignedUserId')) {
|
|
return true;
|
|
}
|
|
|
|
$assignedUserId = $entity->get('assignedUserId');
|
|
|
|
if ($this->user->isPortal()) {
|
|
if (!$entity->isAttributeChanged('assignedUserId') && empty($assignedUserId)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
$assignmentPermission = $this->acl->get('assignmentPermission');
|
|
|
|
if (
|
|
$assignmentPermission === true ||
|
|
$assignmentPermission === AclTable::LEVEL_YES ||
|
|
!in_array($assignmentPermission, [AclTable::LEVEL_TEAM, AclTable::LEVEL_NO])
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
$toProcess = false;
|
|
|
|
if (!$entity->isNew()) {
|
|
if ($entity->isAttributeChanged('assignedUserId')) {
|
|
$toProcess = true;
|
|
}
|
|
} else {
|
|
$toProcess = true;
|
|
}
|
|
|
|
if ($toProcess) {
|
|
if (empty($assignedUserId)) {
|
|
if ($assignmentPermission === AclTable::LEVEL_NO && !$this->user->isApi()) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
if ($assignmentPermission === AclTable::LEVEL_NO) {
|
|
if ($this->user->id !== $assignedUserId) {
|
|
return false;
|
|
}
|
|
}
|
|
else if ($assignmentPermission === AclTable::LEVEL_TEAM) {
|
|
$teamIdList = $this->user->get('teamsIds');
|
|
|
|
if (
|
|
!$this->entityManager
|
|
->getRepository('User')
|
|
->checkBelongsToAnyOfTeams($assignedUserId, $teamIdList)
|
|
) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public function isPermittedTeams(Entity $entity): bool
|
|
{
|
|
$assignmentPermission = $this->acl->get('assignmentPermission');
|
|
|
|
if (
|
|
empty($assignmentPermission) ||
|
|
$assignmentPermission === true ||
|
|
!in_array($assignmentPermission, [AclTable::LEVEL_TEAM, AclTable::LEVEL_NO])
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
if (!$entity->hasLinkMultipleField('teams')) {
|
|
return true;
|
|
}
|
|
|
|
$teamIdList = $entity->getLinkMultipleIdList('teams');
|
|
|
|
if (empty($teamIdList)) {
|
|
if ($assignmentPermission === 'team') {
|
|
if ($entity->hasLinkMultipleField('assignedUsers')) {
|
|
$assignedUserIdList = $entity->getLinkMultipleIdList('assignedUsers');
|
|
|
|
if (empty($assignedUserIdList)) {
|
|
return false;
|
|
}
|
|
}
|
|
else if ($entity->hasAttribute('assignedUserId')) {
|
|
if (!$entity->get('assignedUserId')) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
$newIdList = [];
|
|
|
|
if (!$entity->isNew()) {
|
|
$existingIdList = [];
|
|
|
|
$teamCollection = $this->entityManager
|
|
->getRepository($entity->getEntityType())
|
|
->getRelation($entity, 'teams')
|
|
->select('id')
|
|
->find();
|
|
|
|
foreach ($teamCollection as $team) {
|
|
$existingIdList[] = $team->id;
|
|
}
|
|
|
|
foreach ($teamIdList as $id) {
|
|
if (!in_array($id, $existingIdList)) {
|
|
$newIdList[] = $id;
|
|
}
|
|
}
|
|
} else {
|
|
$newIdList = $teamIdList;
|
|
}
|
|
|
|
if (empty($newIdList)) {
|
|
return true;
|
|
}
|
|
|
|
$userTeamIdList = $this->user->getLinkMultipleIdList('teams');
|
|
|
|
foreach ($newIdList as $id) {
|
|
if (!in_array($id, $userTeamIdList)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
protected function filterInputAttribute($attribute, $value)
|
|
{
|
|
if (in_array($attribute, $this->notFilteringAttributeList)) {
|
|
return $value;
|
|
}
|
|
|
|
$methodName = 'filterInputAttribute' . ucfirst($attribute);
|
|
|
|
if (method_exists($this, $methodName)) {
|
|
$value = $this->$methodName($value);
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
protected function filterInput($data)
|
|
{
|
|
foreach ($this->readOnlyAttributeList as $attribute) {
|
|
unset($data->$attribute);
|
|
}
|
|
|
|
foreach ($this->forbiddenAttributeList as $attribute) {
|
|
unset($data->$attribute);
|
|
}
|
|
|
|
foreach ($data as $key => $value) {
|
|
$data->$key = $this->filterInputAttribute($key, $data->$key);
|
|
}
|
|
|
|
if (!$this->user->isAdmin()) {
|
|
foreach ($this->onlyAdminAttributeList as $attribute) {
|
|
unset($data->$attribute);
|
|
}
|
|
}
|
|
|
|
$forbiddenAttributeList = $this->acl
|
|
->getScopeForbiddenAttributeList($this->entityType, AclTable::ACTION_EDIT);
|
|
|
|
foreach ($forbiddenAttributeList as $attribute) {
|
|
unset($data->$attribute);
|
|
}
|
|
|
|
if (!$this->user->isAdmin()) {
|
|
foreach ($this->nonAdminReadOnlyAttributeList as $attribute) {
|
|
unset($data->$attribute);
|
|
}
|
|
}
|
|
}
|
|
|
|
public function filterCreateInput(StdClass $data): void
|
|
{
|
|
unset($data->deleted);
|
|
unset($data->id);
|
|
unset($data->modifiedById);
|
|
unset($data->modifiedByName);
|
|
unset($data->modifiedAt);
|
|
unset($data->createdById);
|
|
unset($data->createdByName);
|
|
unset($data->createdAt);
|
|
|
|
$this->filterInput($data);
|
|
|
|
$this->handleInput($data);
|
|
$this->handleCreateInput($data);
|
|
}
|
|
|
|
public function filterUpdateInput(StdClass $data): void
|
|
{
|
|
unset($data->deleted);
|
|
unset($data->id);
|
|
unset($data->modifiedById);
|
|
unset($data->modifiedByName);
|
|
unset($data->modifiedAt);
|
|
unset($data->createdById);
|
|
unset($data->createdByName);
|
|
unset($data->createdAt);
|
|
|
|
$this->filterInput($data);
|
|
|
|
$this->handleInput($data);
|
|
}
|
|
|
|
/**
|
|
* @deprecated
|
|
*/
|
|
protected function handleCreateInput($data)
|
|
{
|
|
}
|
|
|
|
/**
|
|
* @deprecated
|
|
*/
|
|
protected function handleInput($data)
|
|
{
|
|
}
|
|
|
|
protected function processDuplicateCheck(Entity $entity, $data)
|
|
{
|
|
if (
|
|
!empty($data->_skipDuplicateCheck) ||
|
|
!empty($data->skipDuplicateCheck) ||
|
|
!empty($data->forceDuplicate)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
$duplicateList = $this->findDuplicates($entity, $data);
|
|
|
|
if (empty($duplicateList)) {
|
|
return;
|
|
}
|
|
|
|
$list = [];
|
|
|
|
foreach ($duplicateList as $e) {
|
|
$list[] = $e->getValueMap();
|
|
}
|
|
|
|
throw ConflictSilent::createWithBody('duplicate', json_encode($list));
|
|
}
|
|
|
|
public function populateDefaults(Entity $entity, StdClass $data): void
|
|
{
|
|
if (!$this->user->isPortal()) {
|
|
$forbiddenFieldList = null;
|
|
|
|
if ($entity->hasAttribute('assignedUserId')) {
|
|
$forbiddenFieldList = $this->acl
|
|
->getScopeForbiddenFieldList($this->entityType, AclTable::ACTION_EDIT);
|
|
|
|
if (in_array('assignedUser', $forbiddenFieldList)) {
|
|
$entity->set('assignedUserId', $this->user->id);
|
|
$entity->set('assignedUserName', $this->user->get('name'));
|
|
}
|
|
}
|
|
|
|
if ($entity->hasLinkMultipleField('teams')) {
|
|
if (is_null($forbiddenFieldList)) {
|
|
$forbiddenFieldList = $this->acl
|
|
->getScopeForbiddenFieldList($this->entityType, AclTable::ACTION_EDIT);
|
|
}
|
|
|
|
if (
|
|
in_array('teams', $forbiddenFieldList) &&
|
|
$this->user->get('defaultTeamId')
|
|
) {
|
|
|
|
$defaultTeamId = $this->user->get('defaultTeamId');
|
|
|
|
$entity->addLinkMultipleId('teams', $defaultTeamId);
|
|
|
|
$teamsNames = $entity->get('teamsNames');
|
|
|
|
if (!$teamsNames || !is_object($teamsNames)) {
|
|
$teamsNames = (object) [];
|
|
}
|
|
|
|
$teamsNames->$defaultTeamId = $this->user->get('defaultTeamName');
|
|
|
|
$entity->set('teamsNames', $teamsNames);
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($this->fieldUtil->getEntityTypeFieldList($this->entityType) as $field) {
|
|
$type = $this->fieldUtil->getEntityTypeFieldParam($this->entityType, $field, 'type');
|
|
|
|
if ($type === 'currency') {
|
|
if ($entity->get($field) && !$entity->get($field . 'Currency')) {
|
|
$entity->set($field . 'Currency', $this->config->get('defaultCurrency'));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a record.
|
|
*
|
|
* @throws ForbiddenSilent If no create access.
|
|
*/
|
|
public function create(StdClass $data): Entity
|
|
{
|
|
if (!$this->acl->check($this->entityType, AclTable::ACTION_CREATE)) {
|
|
throw new ForbiddenSilent();
|
|
}
|
|
|
|
$entity = $this->getRepository()->get();
|
|
|
|
$this->filterCreateInput($data);
|
|
|
|
$entity->set($data);
|
|
|
|
$this->populateDefaults($entity, $data);
|
|
|
|
if (!$this->acl->check($entity, AclTable::ACTION_CREATE)) {
|
|
throw new ForbiddenSilent("No create access.");
|
|
}
|
|
|
|
$this->processValidation($entity, $data);
|
|
$this->processAssignmentCheck($entity);
|
|
$this->processDuplicateCheck($entity, $data);
|
|
$this->beforeCreateEntity($entity, $data);
|
|
|
|
$this->entityManager->saveEntity($entity);
|
|
|
|
$this->afterCreateEntity($entity, $data);
|
|
$this->afterCreateProcessDuplicating($entity, $data);
|
|
$this->loadAdditionalFields($entity);
|
|
$this->prepareEntityForOutput($entity);
|
|
$this->processActionHistoryRecord('create', $entity);
|
|
|
|
return $entity;
|
|
}
|
|
|
|
/**
|
|
* Update a record.
|
|
*
|
|
* @throws BadRequest
|
|
* @throws NotFound If record not found.
|
|
* @throws Forbidden If no access.
|
|
*/
|
|
public function update(string $id, StdClass $data): Entity
|
|
{
|
|
if (!$this->acl->check($this->entityType, AclTable::ACTION_EDIT)) {
|
|
throw new ForbiddenSilent();
|
|
}
|
|
|
|
if (empty($id)) {
|
|
throw new BadRequest("ID is empty.");
|
|
}
|
|
|
|
$this->filterUpdateInput($data);
|
|
|
|
if ($this->getEntityBeforeUpdate) {
|
|
$entity = $this->getEntity($id);
|
|
} else {
|
|
$entity = $this->getRepository()->get($id);
|
|
}
|
|
|
|
if (!$entity) {
|
|
throw new NotFound("Record {$id} not found.");
|
|
}
|
|
|
|
if (!$this->acl->check($entity, AclTable::ACTION_EDIT)) {
|
|
throw new ForbiddenSilent("No edit access.");
|
|
}
|
|
|
|
$entity->set($data);
|
|
|
|
$this->processValidation($entity, $data);
|
|
|
|
$this->processAssignmentCheck($entity);
|
|
|
|
$this->beforeUpdateEntity($entity, $data);
|
|
|
|
if ($this->checkForDuplicatesInUpdate) {
|
|
$this->processDuplicateCheck($entity, $data);
|
|
}
|
|
|
|
$this->entityManager->saveEntity($entity);
|
|
|
|
$this->afterUpdateEntity($entity, $data);
|
|
|
|
$this->prepareEntityForOutput($entity);
|
|
|
|
$this->processActionHistoryRecord('update', $entity);
|
|
|
|
return $entity;
|
|
}
|
|
|
|
/**
|
|
* Delete a record.
|
|
*
|
|
* @throws Forbidden
|
|
* @throws BadRequest
|
|
* @throws NotFound
|
|
*/
|
|
public function delete(string $id): void
|
|
{
|
|
if (!$this->acl->check($this->entityType, AclTable::ACTION_DELETE)) {
|
|
throw new ForbiddenSilent();
|
|
}
|
|
|
|
if (empty($id)) {
|
|
throw new BadRequest("ID is empty.");
|
|
}
|
|
|
|
$entity = $this->getRepository()->get($id);
|
|
|
|
if (!$entity) {
|
|
throw new NotFound("Record {$id} not found.");
|
|
}
|
|
|
|
if (!$this->acl->check($entity, AclTable::ACTION_DELETE)) {
|
|
throw new ForbiddenSilent("No delete access.");
|
|
}
|
|
|
|
$this->beforeDeleteEntity($entity);
|
|
|
|
$this->getRepository()->remove($entity);
|
|
|
|
$this->afterDeleteEntity($entity);
|
|
|
|
$this->processActionHistoryRecord('delete', $entity);
|
|
}
|
|
|
|
/**
|
|
* Find records.
|
|
*
|
|
* @throws Forbidden
|
|
*/
|
|
public function find(SearchParams $searchParams): RecordCollection
|
|
{
|
|
if (!$this->acl->check($this->entityType, AclTable::ACTION_READ)) {
|
|
throw new ForbiddenSilent();
|
|
}
|
|
|
|
$disableCount =
|
|
$this->listCountQueryDisabled ||
|
|
$this->metadata->get(['entityDefs', $this->entityType, 'collection', 'countDisabled']);
|
|
|
|
$maxSize = $searchParams->getMaxSize();
|
|
|
|
if ($disableCount && $maxSize) {
|
|
$searchParams = $searchParams->withMaxSize($maxSize + 1);
|
|
}
|
|
|
|
$preparedSearchParams = $this->prepareSearchParams($searchParams);
|
|
|
|
$selectBuilder = $this->selectBuilderFactory->create();
|
|
|
|
$query = $selectBuilder
|
|
->from($this->entityType)
|
|
->withStrictAccessControl()
|
|
->withSearchParams($preparedSearchParams)
|
|
->build();
|
|
|
|
$collection = $this->getRepository()
|
|
->clone($query)
|
|
->find();
|
|
|
|
foreach ($collection as $entity) {
|
|
$this->loadListAdditionalFields($entity, $preparedSearchParams);
|
|
|
|
$this->prepareEntityForOutput($entity);
|
|
}
|
|
|
|
if (!$disableCount) {
|
|
$total = $this->getRepository()
|
|
->clone($query)
|
|
->count();
|
|
}
|
|
else if ($maxSize && count($collection) > $maxSize) {
|
|
$total = RecordCollection::TOTAL_HAS_MORE;
|
|
|
|
unset($collection[count($collection) - 1]);
|
|
}
|
|
else {
|
|
$total = RecordCollection::TOTAL_HAS_NO_MORE;
|
|
}
|
|
|
|
return new RecordCollection($collection, $total);
|
|
}
|
|
|
|
protected function getEntityEvenDeleted(string $id): ?Entity
|
|
{
|
|
$query = $this->entityManager->getQueryBuilder()
|
|
->select()
|
|
->from($this->entityType)
|
|
->where([
|
|
'id' => $id,
|
|
])
|
|
->withDeleted()
|
|
->build();
|
|
|
|
return $this->getRepository()->clone($query)->findOne();
|
|
}
|
|
|
|
/**
|
|
* Restore a deleted record.
|
|
*
|
|
* @throws NotFound If not found.
|
|
* @throws Forbidden If no access.
|
|
*/
|
|
public function restoreDeleted(string $id): void
|
|
{
|
|
if (!$this->user->isAdmin()) {
|
|
throw new Forbidden();
|
|
}
|
|
|
|
$entity = $this->getEntityEvenDeleted($id);
|
|
|
|
if (!$entity) {
|
|
throw new NotFound();
|
|
}
|
|
|
|
if (!$entity->get('deleted')) {
|
|
throw new Forbidden();
|
|
}
|
|
|
|
$this->getRepository()->restoreDeleted($entity->id);
|
|
}
|
|
|
|
public function getMaxSelectTextAttributeLength(): ?int
|
|
{
|
|
if ($this->maxSelectTextAttributeLengthDisabled) {
|
|
return null;
|
|
}
|
|
|
|
if ($this->maxSelectTextAttributeLength) {
|
|
return $this->maxSelectTextAttributeLength;
|
|
}
|
|
|
|
return $this->config->get('maxSelectTextAttributeLengthForList') ??
|
|
self::MAX_SELECT_TEXT_ATTRIBUTE_LENGTH;
|
|
}
|
|
|
|
/**
|
|
* Find linked records.
|
|
*
|
|
* @throws NotFound If a record not found.
|
|
* @throws Forbidden If no access.
|
|
* @throws Error
|
|
*/
|
|
public function findLinked(string $id, string $link, SearchParams $searchParams): RecordCollection
|
|
{
|
|
if (!$this->acl->check($this->entityType, AclTable::ACTION_READ)) {
|
|
throw new ForbiddenSilent("No access.");
|
|
}
|
|
|
|
$entity = $this->getRepository()->get($id);
|
|
|
|
if (!$entity) {
|
|
throw new NotFound();
|
|
}
|
|
|
|
if (!$this->acl->check($entity, AclTable::ACTION_READ)) {
|
|
throw new ForbiddenSilent();
|
|
}
|
|
|
|
if (!$link) {
|
|
throw new Error("Empty link.");
|
|
}
|
|
|
|
$this->processForbiddenLinkReadCheck($link);
|
|
|
|
$methodName = 'findLinked' . ucfirst($link);
|
|
|
|
if (method_exists($this, $methodName)) {
|
|
return $this->$methodName($id, $searchParams);
|
|
}
|
|
|
|
$foreignEntityType = $this->entityManager
|
|
->getDefs()
|
|
->getEntity($this->entityType)
|
|
->getRelation($link)
|
|
->getForeignEntityType();
|
|
|
|
$linkParams = $this->linkParams[$link] ?? [];
|
|
|
|
$skipAcl = $linkParams['skipAcl'] ?? false;
|
|
|
|
if (!$skipAcl) {
|
|
if (!$this->acl->check($foreignEntityType, AclTable::ACTION_READ)) {
|
|
throw new Forbidden();
|
|
}
|
|
}
|
|
|
|
$recordService = $this->recordServiceContainer->get($foreignEntityType);
|
|
|
|
$disableCountPropertyName = 'findLinked' . ucfirst($link) . 'CountQueryDisabled';
|
|
|
|
$disableCount =
|
|
property_exists($this, $disableCountPropertyName) &&
|
|
$this->$disableCountPropertyName;
|
|
|
|
$maxSize = $searchParams->getMaxSize();
|
|
|
|
if ($disableCount && $maxSize) {
|
|
$searchParams = $searchParams->withMaxSize($maxSize + 1);
|
|
}
|
|
|
|
$preparedSearchParams = $this->prepareLinkSearchParams(
|
|
$recordService->prepareSearchParams($searchParams),
|
|
$link
|
|
);
|
|
|
|
$selectBuilder = $this->selectBuilderFactory->create();
|
|
|
|
$selectBuilder
|
|
->from($foreignEntityType)
|
|
->withSearchParams($preparedSearchParams);
|
|
|
|
if (!$skipAcl) {
|
|
$selectBuilder->withStrictAccessControl();
|
|
}
|
|
else {
|
|
$selectBuilder->withComplexExpressionsForbidden();
|
|
$selectBuilder->withWherePermissionCheck();
|
|
}
|
|
|
|
$query = $selectBuilder->build();
|
|
|
|
$collection = $this->entityManager
|
|
->getRepository($this->entityType)
|
|
->getRelation($entity, $link)
|
|
->clone($query)
|
|
->find();
|
|
|
|
foreach ($collection as $itemEntity) {
|
|
$this->loadListAdditionalFields($itemEntity, $preparedSearchParams);
|
|
|
|
$recordService->prepareEntityForOutput($itemEntity);
|
|
}
|
|
|
|
if (!$disableCount) {
|
|
$total = $this->entityManager
|
|
->getRepository($this->entityType)
|
|
->getRelation($entity, $link)
|
|
->clone($query)
|
|
->count();
|
|
}
|
|
else if ($maxSize && count($collection) > $maxSize) {
|
|
$total = RecordCollection::TOTAL_HAS_MORE;
|
|
|
|
unset($collection[count($collection) - 1]);
|
|
}
|
|
else {
|
|
$total = RecordCollection::TOTAL_HAS_NO_MORE;
|
|
}
|
|
|
|
return new RecordCollection($collection, $total);
|
|
}
|
|
|
|
/**
|
|
* Link records.
|
|
*
|
|
* @throws BadRequest
|
|
* @throws Forbidden
|
|
* @throws NotFound
|
|
* @throws Error
|
|
*/
|
|
public function link(string $id, string $link, string $foreignId): void
|
|
{
|
|
if (!$this->acl->check($this->entityType)) {
|
|
throw new Forbidden();
|
|
}
|
|
|
|
if (empty($id) || empty($link) || empty($foreignId)) {
|
|
throw new BadRequest;
|
|
}
|
|
|
|
$this->processForbiddenLinkEditCheck($link);
|
|
|
|
$entity = $this->getRepository()->get($id);
|
|
|
|
if (!$entity) {
|
|
throw new NotFound();
|
|
}
|
|
|
|
if ($this->noEditAccessRequiredForLink) {
|
|
if (!$this->acl->check($entity, AclTable::ACTION_READ)) {
|
|
throw new Forbidden();
|
|
}
|
|
}
|
|
else if (!$this->acl->check($entity, AclTable::ACTION_EDIT)) {
|
|
throw new Forbidden();
|
|
}
|
|
|
|
$methodName = 'link' . ucfirst($link);
|
|
|
|
if ($link !== 'entity' && $link !== 'entityMass' && method_exists($this, $methodName)) {
|
|
$this->$methodName($id, $foreignId);
|
|
|
|
return;
|
|
}
|
|
|
|
$foreignEntityType = $entity->getRelationParam($link, 'entity');
|
|
|
|
if (!$foreignEntityType) {
|
|
throw new Error("Entity '{$this->entityType}' has not relation '{$link}'.");
|
|
}
|
|
|
|
$foreignEntity = $this->entityManager->getEntity($foreignEntityType, $foreignId);
|
|
|
|
if (!$foreignEntity) {
|
|
throw new NotFound();
|
|
}
|
|
|
|
$accessActionRequired = AclTable::ACTION_EDIT;
|
|
|
|
if (in_array($link, $this->noEditAccessRequiredLinkList)) {
|
|
$accessActionRequired = AclTable::ACTION_READ;
|
|
}
|
|
|
|
if (!$this->acl->check($foreignEntity, $accessActionRequired)) {
|
|
throw new Forbidden();
|
|
}
|
|
|
|
$this->getRepository()->relate($entity, $link, $foreignEntity);
|
|
}
|
|
|
|
/**
|
|
* Unlink records.
|
|
*
|
|
* @throws BadRequest
|
|
* @throws Forbidden
|
|
* @throws NotFound
|
|
* @throws Error
|
|
*/
|
|
public function unlink(string $id, string $link, string $foreignId): void
|
|
{
|
|
if (!$this->acl->check($this->entityType)) {
|
|
throw new Forbidden();
|
|
}
|
|
|
|
if (empty($id) || empty($link) || empty($foreignId)) {
|
|
throw new BadRequest;
|
|
}
|
|
|
|
$this->processForbiddenLinkEditCheck($link);
|
|
|
|
$entity = $this->getRepository()->get($id);
|
|
|
|
if (!$entity) {
|
|
throw new NotFound();
|
|
}
|
|
|
|
if ($this->noEditAccessRequiredForLink) {
|
|
if (!$this->acl->check($entity, AclTable::ACTION_READ)) {
|
|
throw new Forbidden();
|
|
}
|
|
}
|
|
else if (!$this->acl->check($entity, AclTable::ACTION_EDIT)) {
|
|
throw new Forbidden();
|
|
}
|
|
|
|
$methodName = 'unlink' . ucfirst($link);
|
|
|
|
if ($link !== 'entity' && method_exists($this, $methodName)) {
|
|
$this->$methodName($id, $foreignId);
|
|
|
|
return;
|
|
}
|
|
|
|
$foreignEntityType = $entity->getRelationParam($link, 'entity');
|
|
|
|
if (!$foreignEntityType) {
|
|
throw new Error("Entity '{$this->entityType}' has not relation '{$link}'.");
|
|
}
|
|
|
|
$foreignEntity = $this->entityManager->getEntity($foreignEntityType, $foreignId);
|
|
|
|
if (!$foreignEntity) {
|
|
throw new NotFound();
|
|
}
|
|
|
|
$accessActionRequired = AclTable::ACTION_EDIT;
|
|
|
|
if (in_array($link, $this->noEditAccessRequiredLinkList)) {
|
|
$accessActionRequired = AclTable::ACTION_READ;
|
|
}
|
|
|
|
if (!$this->acl->check($foreignEntity, $accessActionRequired)) {
|
|
throw new Forbidden();
|
|
}
|
|
|
|
$this->getRepository()->unrelate($entity, $link, $foreignEntity);
|
|
}
|
|
|
|
public function linkFollowers(string $id, string $foreignId): void
|
|
{
|
|
if (!$this->acl->check($this->entityType, AclTable::ACTION_EDIT)) {
|
|
throw new Forbidden();
|
|
}
|
|
|
|
if (!$this->metadata->get(['scopes', $this->entityType, 'stream'])) {
|
|
throw new NotFound();
|
|
}
|
|
|
|
$entity = $this->getRepository()->get($id);
|
|
|
|
if (!$entity) {
|
|
throw new NotFound();
|
|
}
|
|
|
|
$user = $this->entityManager->getEntity('User', $foreignId);
|
|
|
|
if (!$user) {
|
|
throw new NotFound();
|
|
}
|
|
|
|
if (!$this->acl->check($entity, AclTable::ACTION_EDIT)) {
|
|
throw new ForbiddenSilent("No 'edit' access.");
|
|
}
|
|
|
|
if (!$this->acl->check($entity, AclTable::ACTION_STREAM)) {
|
|
throw new ForbiddenSilent("No 'stream' access.");
|
|
}
|
|
|
|
if (!$user->isPortal() && !$this->acl->check($user, AclTable::ACTION_READ)) {
|
|
throw new ForbiddenSilent("No 'read' access to user.");
|
|
}
|
|
|
|
if ($user->isPortal() && $this->acl->get('portal') !== AclTable::LEVEL_YES) {
|
|
throw new ForbiddenSilent("No 'portal' permission.");
|
|
}
|
|
|
|
if (
|
|
!$user->isPortal() &&
|
|
$this->user->getId() !== $user->getId() &&
|
|
!$this->acl->checkUserPermission($user, 'followerManagement')
|
|
) {
|
|
throw new Forbidden();
|
|
}
|
|
|
|
$result = $this->getStreamService()->followEntity($entity, $foreignId);
|
|
|
|
if (!$result) {
|
|
throw new Forbidden("Could not add a user to followers. The user needs to have 'stream' access.");
|
|
}
|
|
}
|
|
|
|
public function unlinkFollowers(string $id, string $foreignId): void
|
|
{
|
|
if (!$this->acl->check($this->entityType, AclTable::ACTION_EDIT)) {
|
|
throw new Forbidden();
|
|
}
|
|
|
|
if (!$this->metadata->get(['scopes', $this->entityType, 'stream'])) {
|
|
throw new NotFound();
|
|
}
|
|
|
|
$entity = $this->getRepository()->get($id);
|
|
|
|
if (!$entity) {
|
|
throw new NotFound();
|
|
}
|
|
|
|
$user = $this->entityManager->getEntity('User', $foreignId);
|
|
|
|
if (!$user) {
|
|
throw new NotFound();
|
|
}
|
|
|
|
if (!$this->acl->check($entity, AclTable::ACTION_EDIT)) {
|
|
throw new ForbiddenSilent("No 'edit' access.");
|
|
}
|
|
|
|
if (!$this->acl->check($entity, AclTable::ACTION_STREAM)) {
|
|
throw new ForbiddenSilent("No 'stream' access.");
|
|
}
|
|
|
|
if (!$user->isPortal() && !$this->acl->check($user, AclTable::ACTION_READ)) {
|
|
throw new ForbiddenSilent("No 'read' access to user.");
|
|
}
|
|
|
|
if ($user->isPortal() && $this->acl->get('portal') !== AclTable::LEVEL_YES) {
|
|
throw new ForbiddenSilent("No 'portal' permission.");
|
|
}
|
|
|
|
if (
|
|
!$user->isPortal() &&
|
|
$this->user->getId() !== $user->getId() &&
|
|
!$this->acl->checkUserPermission($user, 'followerManagement')
|
|
) {
|
|
throw new Forbidden();
|
|
}
|
|
|
|
$this->getStreamService()->unfollowEntity($entity, $foreignId);
|
|
}
|
|
|
|
public function massLink(string $id, string $link, array $where, ?array $selectData = null)
|
|
{
|
|
if (!$this->acl->check($this->entityType, AclTable::ACTION_EDIT)) {
|
|
throw new Forbidden();
|
|
}
|
|
|
|
if (empty($id) || empty($link)) {
|
|
throw new BadRequest;
|
|
}
|
|
|
|
$this->processForbiddenLinkEditCheck($link);
|
|
|
|
$entity = $this->getRepository()->get($id);
|
|
|
|
if (!$entity) {
|
|
throw new NotFound();
|
|
}
|
|
|
|
if (!$this->acl->check($entity, AclTable::ACTION_EDIT)) {
|
|
throw new Forbidden();
|
|
}
|
|
|
|
$methodName = 'massLink' . ucfirst($link);
|
|
|
|
if (method_exists($this, $methodName)) {
|
|
return $this->$methodName($id, $where, $selectData);
|
|
}
|
|
|
|
$foreignEntityType = $entity->getRelationParam($link, 'entity');
|
|
|
|
if (empty($foreignEntityType)) {
|
|
throw new Error();
|
|
}
|
|
|
|
$accessActionRequired = AclTable::ACTION_EDIT;
|
|
|
|
if (in_array($link, $this->noEditAccessRequiredLinkList)) {
|
|
$accessActionRequired = AclTable::ACTION_READ;
|
|
}
|
|
|
|
if (!$this->acl->check($foreignEntityType, $accessActionRequired)) {
|
|
throw new Forbidden();
|
|
}
|
|
|
|
if (!is_array($where)) {
|
|
$where = [];
|
|
}
|
|
$params['where'] = $where;
|
|
|
|
if (is_array($selectData)) {
|
|
foreach ($selectData as $k => $v) {
|
|
$params[$k] = $v;
|
|
}
|
|
}
|
|
|
|
$query = $this->selectBuilderFactory->create()
|
|
->from($foreignEntityType)
|
|
->withStrictAccessControl()
|
|
->withSearchParams(SearchParams::fromRaw($params))
|
|
->build();
|
|
|
|
if ($this->acl->getLevel($foreignEntityType, $accessActionRequired) === AclTable::LEVEL_ALL) {
|
|
$this->getRepository()
|
|
->getRelation($entity, $link)
|
|
->massRelate($query);
|
|
|
|
return true;
|
|
}
|
|
|
|
$countRelated = 0;
|
|
|
|
$foreignCollection = $this->entityManager
|
|
->getRepository($foreignEntityType)
|
|
->clone($query)
|
|
->find();
|
|
|
|
foreach ($foreignCollection as $foreignEntity) {
|
|
if (!$this->acl->check($foreignEntity, $accessActionRequired)) {
|
|
continue;
|
|
}
|
|
|
|
$this->getRepository()
|
|
->getRelation($entity, $link)
|
|
->relate($foreignEntity);
|
|
|
|
$countRelated++;
|
|
}
|
|
|
|
if ($countRelated) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
protected function processForbiddenLinkReadCheck(string $link): void
|
|
{
|
|
$forbiddenLinkList = $this->acl
|
|
->getScopeForbiddenLinkList($this->entityType, AclTable::ACTION_READ);
|
|
|
|
if (in_array($link, $forbiddenLinkList)) {
|
|
throw new Forbidden();
|
|
}
|
|
|
|
if (in_array($link, $this->forbiddenLinkList)) {
|
|
throw new Forbidden();
|
|
}
|
|
|
|
if (in_array($link, $this->internalLinkList)) {
|
|
throw new Forbidden();
|
|
}
|
|
|
|
if (!$this->user->isAdmin() && in_array($link, $this->onlyAdminLinkList)) {
|
|
throw new Forbidden();
|
|
}
|
|
}
|
|
|
|
protected function processForbiddenLinkEditCheck(string $link): void
|
|
{
|
|
$forbiddenLinkList = $this->acl
|
|
->getScopeForbiddenLinkList($this->entityType, AclTable::ACTION_EDIT);
|
|
|
|
if (in_array($link, $forbiddenLinkList)) {
|
|
throw new Forbidden();
|
|
}
|
|
|
|
if (in_array($link, $this->forbiddenLinkList)) {
|
|
throw new Forbidden();
|
|
}
|
|
|
|
if (in_array($link, $this->readOnlyLinkList)) {
|
|
throw new Forbidden();
|
|
}
|
|
|
|
if (!$this->user->isAdmin() && in_array($link, $this->nonAdminReadOnlyLinkList)) {
|
|
throw new Forbidden();
|
|
}
|
|
|
|
if (!$this->user->isAdmin() && in_array($link, $this->onlyAdminLinkList)) {
|
|
throw new Forbidden();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Follow a record.
|
|
*
|
|
* @param $id A record ID.
|
|
* @param $userId A user ID. If not specified then a current user will be used.
|
|
*
|
|
* @throws NotFoundSilent
|
|
* @throws Forbidden
|
|
*/
|
|
public function follow(string $id, ?string $userId = null): void
|
|
{
|
|
if (!$this->acl->check($this->entityType, AclTable::ACTION_STREAM)) {
|
|
throw new Forbidden();
|
|
}
|
|
|
|
$entity = $this->getRepository()->get($id);
|
|
|
|
if (!$entity) {
|
|
throw new NotFoundSilent();
|
|
}
|
|
|
|
if (!$this->acl->check($entity, AclTable::ACTION_STREAM)) {
|
|
throw new Forbidden();
|
|
}
|
|
|
|
if (empty($userId)) {
|
|
$userId = $this->user->id;
|
|
}
|
|
|
|
$this->getStreamService()->followEntity($entity, $userId);
|
|
}
|
|
|
|
/**
|
|
* Unfollow a record.
|
|
*
|
|
* @param $id A record ID.
|
|
* @param string|null A user ID. If not specified then a current user will be used.
|
|
*
|
|
* @throws NotFoundSilent
|
|
*/
|
|
public function unfollow(string $id, ?string $userId = null): void
|
|
{
|
|
$entity = $this->getRepository()->get($id);
|
|
|
|
if (!$entity) {
|
|
throw new NotFoundSilent();
|
|
}
|
|
|
|
if (empty($userId)) {
|
|
$userId = $this->user->id;
|
|
}
|
|
|
|
$this->getStreamService()->unfollowEntity($entity, $userId);
|
|
}
|
|
|
|
protected function getDuplicateWhereClause(Entity $entity, $data)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
public function checkIsDuplicate(Entity $entity): bool
|
|
{
|
|
$where = $this->getDuplicateWhereClause($entity, (object) []);
|
|
|
|
if ($where) {
|
|
if ($entity->id) {
|
|
$where['id!='] = $entity->id;
|
|
}
|
|
|
|
$duplicate = $this->getRepository()
|
|
->select(['id'])
|
|
->where($where)
|
|
->findOne();
|
|
|
|
if ($duplicate) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Find duplicates for an entity.
|
|
*/
|
|
public function findDuplicates(Entity $entity, ?StdClass $data = null): ?Collection
|
|
{
|
|
if (!$data) {
|
|
$data = (object) [];
|
|
}
|
|
|
|
$where = $this->getDuplicateWhereClause($entity, $data);
|
|
|
|
if (!$where) {
|
|
return null;
|
|
}
|
|
|
|
if ($entity->id) {
|
|
$where['id!='] = $entity->id;
|
|
}
|
|
|
|
$select = $this->findDuplicatesSelectAttributeList;
|
|
|
|
$builder = $this->selectBuilderFactory->create();
|
|
|
|
$query = $builder
|
|
->from($this->entityType)
|
|
->withStrictAccessControl()
|
|
->build();
|
|
|
|
$duplicateCollection = $this->getRepository()
|
|
->clone($query)
|
|
->select($select)
|
|
->where($where)
|
|
->limit(0, self::FIND_DUPLICATES_LIMIT)
|
|
->find();
|
|
|
|
if (count($duplicateCollection)) {
|
|
return $duplicateCollection;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Prepare an entity for output. Clears not allowed attributes.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function prepareEntityForOutput(Entity $entity)
|
|
{
|
|
foreach ($this->internalAttributeList as $attribute) {
|
|
$entity->clear($attribute);
|
|
}
|
|
|
|
foreach ($this->forbiddenAttributeList as $attribute) {
|
|
$entity->clear($attribute);
|
|
}
|
|
|
|
if (!$this->user->isAdmin()) {
|
|
foreach ($this->onlyAdminAttributeList as $attribute) {
|
|
$entity->clear($attribute);
|
|
}
|
|
}
|
|
|
|
$forbiddenAttributeList = $this->acl
|
|
->getScopeForbiddenAttributeList($entity->getEntityType(), AclTable::ACTION_READ);
|
|
|
|
foreach ($forbiddenAttributeList as $attribute) {
|
|
$entity->clear($attribute);
|
|
}
|
|
}
|
|
|
|
protected function findLinkedFollowers(string $id, SearchParams $params): RecordCollection
|
|
{
|
|
$entity = $this->getRepository()->get($id);
|
|
|
|
if (!$entity) {
|
|
throw new NotFound();
|
|
}
|
|
|
|
if (!$this->acl->check($entity, AclTable::ACTION_READ)) {
|
|
throw new Forbidden();
|
|
}
|
|
|
|
return $this->getStreamService()->findEntityFollowers($entity, $params);
|
|
}
|
|
|
|
public function getDuplicateAttributes(string $id): StdClass
|
|
{
|
|
if (!$id) {
|
|
throw new BadRequest("No ID.");
|
|
}
|
|
|
|
if (!$this->acl->check($this->entityType, AclTable::ACTION_CREATE)) {
|
|
throw new Forbidden("No 'create' access.");
|
|
}
|
|
|
|
if (!$this->acl->check($this->entityType, AclTable::ACTION_READ)) {
|
|
throw new Forbidden("No 'read' access.");
|
|
}
|
|
|
|
$entity = $this->getEntity($id);
|
|
|
|
if (!$entity) {
|
|
throw new NotFound("Record not found.");
|
|
}
|
|
|
|
$attributes = $entity->getValueMap();
|
|
|
|
unset($attributes->id);
|
|
|
|
$fields = $this->metadata->get(['entityDefs', $this->entityType, 'fields'], []);
|
|
|
|
$fieldManager = $this->fieldUtil;
|
|
|
|
foreach ($fields as $field => $item) {
|
|
if (!empty($item['duplicateIgnore']) || in_array($field, $this->duplicateIgnoreFieldList)) {
|
|
$attributeToIgnoreList = $fieldManager->getAttributeList($this->entityType, $field);
|
|
|
|
foreach ($attributeToIgnoreList as $attribute) {
|
|
unset($attributes->$attribute);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (empty($item['type'])) {
|
|
continue;
|
|
}
|
|
|
|
$type = $item['type'];
|
|
|
|
if (in_array($type, ['file', 'image'])) {
|
|
$attachment = $entity->get($field);
|
|
|
|
if ($attachment) {
|
|
$attachment = $this->entityManager
|
|
->getRepository('Attachment')
|
|
->getCopiedAttachment($attachment);
|
|
|
|
$idAttribute = $field . 'Id';
|
|
|
|
if ($attachment) {
|
|
$attributes->$idAttribute = $attachment->id;
|
|
}
|
|
}
|
|
} else if (in_array($type, ['attachmentMultiple'])) {
|
|
$attachmentList = $entity->get($field);
|
|
|
|
if (count($attachmentList)) {
|
|
$idList = [];
|
|
$nameHash = (object) [];
|
|
$typeHash = (object) [];
|
|
|
|
foreach ($attachmentList as $attachment) {
|
|
$attachment = $this->entityManager
|
|
->getRepository('Attachment')
|
|
->getCopiedAttachment($attachment);
|
|
|
|
$attachment->set('field', $field);
|
|
|
|
$this->entityManager->saveEntity($attachment);
|
|
|
|
if ($attachment) {
|
|
$idList[] = $attachment->id;
|
|
$nameHash->{$attachment->id} = $attachment->get('name');
|
|
$typeHash->{$attachment->id} = $attachment->get('type');
|
|
}
|
|
}
|
|
|
|
$attributes->{$field . 'Ids'} = $idList;
|
|
$attributes->{$field . 'Names'} = $nameHash;
|
|
$attributes->{$field . 'Types'} = $typeHash;
|
|
}
|
|
} else if ($type === 'linkMultiple') {
|
|
$foreignLink = $entity->getRelationParam($field, 'foreign');
|
|
$foreignEntityType = $entity->getRelationParam($field, 'entity');
|
|
|
|
if ($foreignEntityType && $foreignLink) {
|
|
$foreignRelationType = $this->metadata->get(
|
|
['entityDefs', $foreignEntityType, 'links', $foreignLink, 'type']
|
|
);
|
|
|
|
if ($foreignRelationType !== 'hasMany') {
|
|
unset($attributes->{$field . 'Ids'});
|
|
unset($attributes->{$field . 'Names'});
|
|
unset($attributes->{$field . 'Columns'});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($this->duplicateIgnoreAttributeList as $attribute) {
|
|
unset($attributes->$attribute);
|
|
}
|
|
|
|
$attributes->_duplicatingEntityId = $id;
|
|
|
|
return $attributes;
|
|
}
|
|
|
|
protected function afterCreateProcessDuplicating(Entity $entity, $data)
|
|
{
|
|
if (!isset($data->_duplicatingEntityId)) {
|
|
return;
|
|
}
|
|
|
|
$duplicatingEntityId = $data->_duplicatingEntityId;
|
|
|
|
if (!$duplicatingEntityId) {
|
|
return;
|
|
}
|
|
|
|
$duplicatingEntity = $this->entityManager->getEntity($entity->getEntityType(), $duplicatingEntityId);
|
|
|
|
if (!$duplicatingEntity) {
|
|
return;
|
|
}
|
|
|
|
if (!$this->acl->check($duplicatingEntity, AclTable::ACTION_READ)) {
|
|
return;
|
|
}
|
|
|
|
$this->duplicateLinks($entity, $duplicatingEntity);
|
|
}
|
|
|
|
protected function duplicateLinks(Entity $entity, Entity $duplicatingEntity)
|
|
{
|
|
$repository = $this->getRepository();
|
|
|
|
foreach ($this->duplicatingLinkList as $link) {
|
|
$linkedList = $repository->findRelated($duplicatingEntity, $link);
|
|
|
|
foreach ($linkedList as $linked) {
|
|
$repository->relate($entity, $link, $linked);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function getFieldByTypeList($type)
|
|
{
|
|
return $this->fieldUtil->getFieldByTypeList($this->entityType, $type);
|
|
}
|
|
|
|
public function prepareSearchParams(SearchParams $searchParams): SearchParams
|
|
{
|
|
return $this
|
|
->prepareSearchParamsSelect($searchParams)
|
|
->withMaxTextAttributeLength(
|
|
$this->getMaxSelectTextAttributeLength()
|
|
);
|
|
}
|
|
|
|
protected function prepareSearchParamsSelect(SearchParams $searchParams): SearchParams
|
|
{
|
|
if ($this->forceSelectAllAttributes) {
|
|
return $searchParams->withSelect(null);
|
|
}
|
|
|
|
if ($this->selectAttributeList) {
|
|
return $searchParams->withSelect($this->selectAttributeList);
|
|
}
|
|
|
|
if (count($this->mandatorySelectAttributeList) && $searchParams->getSelect() !== null) {
|
|
$select = array_unique(
|
|
array_merge(
|
|
$searchParams->getSelect(),
|
|
$this->mandatorySelectAttributeList
|
|
)
|
|
);
|
|
|
|
return $searchParams->withSelect($select);
|
|
}
|
|
|
|
return $searchParams;
|
|
}
|
|
|
|
protected function prepareLinkSearchParams(SearchParams $searchParams, string $link): SearchParams
|
|
{
|
|
if ($searchParams->getSelect() === null) {
|
|
return $searchParams;
|
|
}
|
|
|
|
$mandatorySelectAttributeList = $this->linkMandatorySelectAttributeList[$link] ?? null;
|
|
|
|
if ($mandatorySelectAttributeList === null) {
|
|
return $searchParams;
|
|
}
|
|
|
|
$select = array_unique(
|
|
array_merge(
|
|
$searchParams->getSelect(),
|
|
$mandatorySelectAttributeList
|
|
)
|
|
);
|
|
|
|
return $searchParams->withSelect($select);
|
|
}
|
|
|
|
protected function beforeCreateEntity(Entity $entity, $data)
|
|
{
|
|
}
|
|
|
|
protected function afterCreateEntity(Entity $entity, $data)
|
|
{
|
|
}
|
|
|
|
protected function beforeUpdateEntity(Entity $entity, $data)
|
|
{
|
|
}
|
|
|
|
protected function afterUpdateEntity(Entity $entity, $data)
|
|
{
|
|
}
|
|
|
|
protected function beforeDeleteEntity(Entity $entity)
|
|
{
|
|
}
|
|
|
|
protected function afterDeleteEntity(Entity $entity)
|
|
{
|
|
}
|
|
}
|