diff --git a/application/Espo/Classes/Record/User/DeletedRestorer.php b/application/Espo/Classes/Record/User/DeletedRestorer.php new file mode 100644 index 0000000000..532f4ac3d9 --- /dev/null +++ b/application/Espo/Classes/Record/User/DeletedRestorer.php @@ -0,0 +1,69 @@ +. + * + * 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 Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Classes\Record\User; + +use Espo\Core\Exceptions\Conflict; +use Espo\Core\Record\Deleted\DefaultRestorer; +use Espo\Core\Record\Deleted\Restorer; +use Espo\Entities\User; +use Espo\ORM\Entity; +use Espo\Tools\User\UserUtil; + +/** + * @implements Restorer + */ +class DeletedRestorer implements Restorer +{ + public function __construct( + private DefaultRestorer $defaultRestorer, + private UserUtil $userUtil, + ) {} + + + /** + * @inheritDoc + */ + public function restore(Entity $entity): void + { + $this->processUserExistsChecking($entity); + + $this->defaultRestorer->restore($entity); + } + + /** + * @throws Conflict + */ + private function processUserExistsChecking(User $user): void + { + if ($this->userUtil->checkExists($user)) { + throw new Conflict('userNameExists'); + } + } +} diff --git a/application/Espo/Core/Controllers/RecordBase.php b/application/Espo/Core/Controllers/RecordBase.php index 0d4c2ddea0..8891363aea 100644 --- a/application/Espo/Core/Controllers/RecordBase.php +++ b/application/Espo/Core/Controllers/RecordBase.php @@ -317,6 +317,7 @@ class RecordBase extends Base implements * @throws BadRequest * @throws Forbidden * @throws NotFound + * @throws Conflict * @noinspection PhpUnused */ public function postActionRestoreDeleted(Request $request): bool diff --git a/application/Espo/Core/Record/Deleted/DefaultRestorer.php b/application/Espo/Core/Record/Deleted/DefaultRestorer.php new file mode 100644 index 0000000000..eb9e580850 --- /dev/null +++ b/application/Espo/Core/Record/Deleted/DefaultRestorer.php @@ -0,0 +1,76 @@ +. + * + * 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 Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Core\Record\Deleted; + +use Espo\Core\Exceptions\Forbidden; +use Espo\Core\ORM\Repository\Option\SaveOption; +use Espo\Core\Utils\Metadata; +use Espo\ORM\Entity; +use Espo\ORM\EntityManager; +use Espo\ORM\Name\Attribute; + +/** + * @implements Restorer + */ +class DefaultRestorer implements Restorer +{ + public function __construct( + private EntityManager $entityManager, + private Metadata $metadata, + ) {} + + public function restore(Entity $entity): void + { + if (!$entity->get(Attribute::DELETED)) { + throw new Forbidden("No 'deleted' attribute."); + } + + $this->entityManager + ->getTransactionManager() + ->run(fn () => $this->restoreInTransaction($entity)); + } + + private function restoreInTransaction(Entity $entity): void + { + $repository = $this->entityManager->getRDBRepository($entity->getEntityType()); + + $repository->restoreDeleted($entity->getId()); + + if ( + $entity->hasAttribute('deleteId') && + $this->metadata->get("entityDefs.{$entity->getEntityType()}.deleteId") + ) { + $this->entityManager->refreshEntity($entity); + + $entity->set('deleteId', '0'); + $repository->save($entity, [SaveOption::SILENT => true]); + } + } +} diff --git a/application/Espo/Core/Record/Deleted/Restorer.php b/application/Espo/Core/Record/Deleted/Restorer.php new file mode 100644 index 0000000000..4024a0e536 --- /dev/null +++ b/application/Espo/Core/Record/Deleted/Restorer.php @@ -0,0 +1,47 @@ +. + * + * 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 Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Core\Record\Deleted; + +use Espo\Core\Exceptions\Conflict; +use Espo\Core\Exceptions\Forbidden; +use Espo\ORM\Entity; + +/** + * @template TEntity of Entity + */ +interface Restorer +{ + /** + * @param TEntity $entity A deleted entity. + * @throws Forbidden + * @throws Conflict + */ + public function restore(Entity $entity): void; +} diff --git a/application/Espo/Core/Record/Service.php b/application/Espo/Core/Record/Service.php index 854748af2c..9efe9cffb1 100644 --- a/application/Espo/Core/Record/Service.php +++ b/application/Espo/Core/Record/Service.php @@ -51,6 +51,8 @@ use Espo\Core\Record\ActionHistory\ActionLogger; use Espo\Core\Record\ConcurrencyControl\OptimisticProcessor; use Espo\Core\Record\Defaults\Populator as DefaultsPopulator; use Espo\Core\Record\Defaults\PopulatorFactory as DefaultsPopulatorFactory; +use Espo\Core\Record\Deleted\DefaultRestorer; +use Espo\Core\Record\Deleted\Restorer; use Espo\Core\Record\DynamicLogic\InputFilterProcessor; use Espo\Core\Record\Formula\Processor as FormulaProcessor; use Espo\Core\Record\Input\Data; @@ -950,11 +952,12 @@ class Service implements Crud, * * @throws NotFound If not found. * @throws Forbidden If no access. + * @throws Conflict If a conflict occurred. */ public function restoreDeleted(string $id): void { if (!$this->user->isAdmin()) { - throw new Forbidden(); + throw new Forbidden("Only admin can restore."); } $entity = $this->getEntityEvenDeleted($id); @@ -963,24 +966,14 @@ class Service implements Crud, throw new NotFound(); } - if (!$entity->get(Attribute::DELETED)) { - throw new Forbidden("No 'deleted' attribute."); - } + /** @var class-string> $restorerClassName */ + $restorerClassName = $this->metadata->get("recordDefs.$this->entityType.deletedRestorerClassName") ?? + DefaultRestorer::class; - $this->entityManager->getTransactionManager() - ->run(function () use ($entity) { - $this->getRepository()->restoreDeleted($entity->getId()); + /** @var Restorer $restorer */ + $restorer = $this->injectableFactory->createWithBinding($restorerClassName, $this->createBinding()); - if ( - $entity->hasAttribute('deleteId') && - $this->metadata->get("entityDefs.$this->entityType.deleteId") - ) { - $this->entityManager->refreshEntity($entity); - - $entity->set('deleteId', '0'); - $this->getRepository()->save($entity, [SaveOption::SILENT => true]); - } - }); + $restorer->restore($entity); } public function getMaxSelectTextAttributeLength(): ?int diff --git a/application/Espo/Resources/metadata/recordDefs/User.json b/application/Espo/Resources/metadata/recordDefs/User.json index 13ac541cde..944b9dffb7 100644 --- a/application/Espo/Resources/metadata/recordDefs/User.json +++ b/application/Espo/Resources/metadata/recordDefs/User.json @@ -35,5 +35,6 @@ ], "beforeDeleteHookClassNameList": [ "Espo\\Classes\\RecordHooks\\User\\BeforeDelete" - ] + ], + "deletedRestorerClassName": "Espo\\Classes\\Record\\User\\DeletedRestorer" } diff --git a/application/Espo/Services/User.php b/application/Espo/Services/User.php index 51e34c7fd9..1b93a645ac 100644 --- a/application/Espo/Services/User.php +++ b/application/Espo/Services/User.php @@ -31,8 +31,6 @@ namespace Espo\Services; use Espo\Core\Di\LogAware; use Espo\Core\Di\LogSetter; -use Espo\Core\Exceptions\Conflict; -use Espo\Core\Exceptions\NotFound; use Espo\Core\Mail\Exceptions\SendingError; use Espo\Entities\User as UserEntity; use Espo\Core\Exceptions\BadRequest; @@ -41,9 +39,6 @@ use Espo\Core\Record\CreateParams; use Espo\Core\Record\UpdateParams; use Espo\Core\Utils\PasswordHash; use Espo\ORM\Entity; -use Espo\ORM\Name\Attribute; -use Espo\ORM\Query\SelectBuilder; -use Espo\Tools\User\UserUtil; use Espo\Tools\UserSecurity\Password\Checker as PasswordChecker; use Espo\Tools\UserSecurity\Password\Generator as PasswordGenerator; use Espo\Tools\UserSecurity\Password\Sender as PasswordSender; @@ -201,42 +196,6 @@ class User extends Record implements LogAware ->sendPassword($user, $password); } - /** - * @throws Conflict - */ - private function processUserExistsChecking(UserEntity $user): void - { - $util = $this->injectableFactory->create(UserUtil::class); - - if ($util->checkExists($user)) { - throw new Conflict('userNameExists'); - } - } - - /** - * @throws Forbidden - * @throws NotFound - * @throws Conflict - */ - public function restoreDeleted(string $id): void - { - $entity = $this->getRepository() - ->clone( - SelectBuilder::create() - ->from(UserEntity::ENTITY_TYPE) - ->withDeleted() - ->build() - ) - ->where([Attribute::ID => $id]) - ->findOne(); - - if ($entity) { - $this->processUserExistsChecking($entity); - } - - parent::restoreDeleted($id); - } - private function createPasswordChecker(): PasswordChecker { return $this->injectableFactory->create(PasswordChecker::class); diff --git a/schema/metadata/recordDefs.json b/schema/metadata/recordDefs.json index de7bc114c6..3fffb8260e 100644 --- a/schema/metadata/recordDefs.json +++ b/schema/metadata/recordDefs.json @@ -281,6 +281,10 @@ "items": { "type": "string" } + }, + "deletedRestorerClassName": { + "type": "string", + "description": "A deleted record restorer. Should implement `Espo\\Core\\Record\\Deleted\\Restorer`. As of v9.2." } } }