Initial commit
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Tools\Workflow\Action\RunAction;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
*/
|
||||
interface ServiceAction
|
||||
{
|
||||
/**
|
||||
* @param Entity $entity
|
||||
*/
|
||||
public function run(Entity $entity, mixed $data): mixed;
|
||||
}
|
||||
28
custom/Espo/Modules/Advanced/Tools/Workflow/Alert.php
Normal file
28
custom/Espo/Modules/Advanced/Tools/Workflow/Alert.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Tools\Workflow;
|
||||
|
||||
class Alert
|
||||
{
|
||||
public function __construct(
|
||||
public string $message,
|
||||
public ?string $type = null,
|
||||
public bool $autoClose = false,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,464 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Tools\Workflow\Core;
|
||||
|
||||
use Espo\Core\Container;
|
||||
use Espo\Core\FieldProcessing\SpecificFieldLoader;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Core\Record\ServiceContainer;
|
||||
use Espo\Core\Utils\FieldUtil;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
use Exception;
|
||||
use stdClass;
|
||||
|
||||
class EntityHelper
|
||||
{
|
||||
/**
|
||||
* For bc the type is in the docblock.
|
||||
*
|
||||
* @var ?SpecificFieldLoader
|
||||
*/
|
||||
private $specificFieldLoader = null;
|
||||
|
||||
public function __construct(
|
||||
private Container $container,
|
||||
private EntityManager $entityManager,
|
||||
private ServiceContainer $serviceContainer,
|
||||
private Metadata $metadata,
|
||||
private FieldUtil $fieldUtil,
|
||||
) {}
|
||||
|
||||
private function getSpecificFieldLoader(): ?SpecificFieldLoader
|
||||
{
|
||||
if (!class_exists("Espo\\Core\\FieldProcessing\\SpecificFieldLoader")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$this->specificFieldLoader) {
|
||||
$this->specificFieldLoader = $this->container
|
||||
->getByClass(InjectableFactory::class)
|
||||
->create(SpecificFieldLoader::class);
|
||||
}
|
||||
|
||||
return $this->specificFieldLoader;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $fieldName
|
||||
* @return string
|
||||
*/
|
||||
private function normalizeRelatedFieldName(CoreEntity $entity, $fieldName)
|
||||
{
|
||||
if ($entity->hasRelation($fieldName)) {
|
||||
$type = $entity->getRelationType($fieldName);
|
||||
|
||||
$key = $entity->getRelationParam($fieldName, 'key');
|
||||
$foreignKey = $entity->getRelationParam($fieldName, 'foreignKey');
|
||||
|
||||
switch ($type) {
|
||||
case Entity::HAS_CHILDREN:
|
||||
if ($foreignKey) {
|
||||
$fieldName = $foreignKey;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case Entity::BELONGS_TO:
|
||||
if ($key) {
|
||||
$fieldName = $key;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case Entity::HAS_MANY:
|
||||
case Entity::MANY_MANY:
|
||||
$fieldName .= 'Ids';
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $fieldName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get actual attribute list w/o additional.
|
||||
*
|
||||
* @param Entity $entity
|
||||
* @param string $field
|
||||
* @return string[]
|
||||
*/
|
||||
public function getActualAttributes(Entity $entity, string $field): array
|
||||
{
|
||||
$entityType = $entity->getEntityType();
|
||||
|
||||
$list = [];
|
||||
$actualList = $this->fieldUtil->getActualAttributeList($entityType, $field);
|
||||
$additionalList = $this->fieldUtil->getAdditionalActualAttributeList($entityType, $field);
|
||||
|
||||
foreach ($actualList as $item) {
|
||||
if (!in_array($item, $additionalList)) {
|
||||
$list[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field value for a field/related field. If this field has a relation, get value from the relation.
|
||||
*/
|
||||
public function getFieldValues(
|
||||
CoreEntity $fromEntity,
|
||||
CoreEntity $toEntity,
|
||||
string $fromField,
|
||||
string $toField
|
||||
): stdClass {
|
||||
|
||||
$entity = $fromEntity;
|
||||
$field = $fromField;
|
||||
|
||||
$values = (object) [];
|
||||
|
||||
if (str_contains($field, '.')) {
|
||||
[$relation, $foreignField] = explode('.', $field);
|
||||
|
||||
$relatedEntity = $this->getRelatedEntity($entity, $relation);
|
||||
|
||||
if (!$relatedEntity instanceof CoreEntity) {
|
||||
$GLOBALS['log']->debug(
|
||||
"Workflow EntityHelper:getFieldValues: No related record for '$field', entity " .
|
||||
"{$entity->getEntityType()}.");
|
||||
|
||||
return (object) [];
|
||||
}
|
||||
|
||||
$entity = $relatedEntity;
|
||||
$field = $foreignField;
|
||||
}
|
||||
|
||||
if ($entity->hasRelation($field) && !$entity->isNew()) {
|
||||
$this->loadLink($entity, $field);
|
||||
}
|
||||
|
||||
$fromType = $this->getFieldType($entity, $field);
|
||||
$toType = $this->getFieldType($toEntity, $toField);
|
||||
|
||||
if (
|
||||
$fromType === 'link' &&
|
||||
$toType === 'linkParent'
|
||||
) {
|
||||
return $this->getFieldValuesLinkToLinkParent($entity, $field, $toField);
|
||||
}
|
||||
|
||||
if (
|
||||
$fromField === 'id' &&
|
||||
$toType === 'linkParent'
|
||||
) {
|
||||
return $this->getFieldValuesIdToLinkParent($entity, $toField);
|
||||
}
|
||||
|
||||
$attributeMap = $this->getRelevantAttributeMap($entity, $toEntity, $field, $toField);
|
||||
|
||||
$service = $this->serviceContainer->get($entity->getEntityType());
|
||||
|
||||
$toAttribute = null;
|
||||
|
||||
$this->loadFieldForAttributes($entity, $field, array_keys($attributeMap));
|
||||
|
||||
foreach ($attributeMap as $fromAttribute => $toAttribute) {
|
||||
// @todo Revise.
|
||||
$getCopiedMethodName = 'getCopied' . ucfirst($fromAttribute);
|
||||
|
||||
if (method_exists($entity, $getCopiedMethodName)) {
|
||||
$values->$toAttribute = $entity->$getCopiedMethodName();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// @todo Revise.
|
||||
$getCopiedMethodName = 'getCopiedEntityAttribute' . ucfirst($fromAttribute);
|
||||
|
||||
if (method_exists($service, $getCopiedMethodName)) {
|
||||
$values->$toAttribute = $service->$getCopiedMethodName($entity);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$values->$toAttribute = $entity->get($fromAttribute);
|
||||
}
|
||||
|
||||
$toFieldType = $this->getFieldType($toEntity, $toField);
|
||||
|
||||
if ($toFieldType === 'personName' && $toAttribute) {
|
||||
$this->handlePersonName($toAttribute, $values, $toField);
|
||||
}
|
||||
|
||||
// Correct field types. E.g. set teamsIds from defaultTeamId.
|
||||
if ($toEntity->hasRelation($toField)) {
|
||||
$normalizedFieldName = $this->normalizeRelatedFieldName($toEntity, $toField);
|
||||
|
||||
if (
|
||||
$toEntity->getRelationType($toField) === Entity::MANY_MANY &&
|
||||
isset($values->$normalizedFieldName) &&
|
||||
!is_array($values->$normalizedFieldName)
|
||||
) {
|
||||
$values->$normalizedFieldName = (array) $values->$normalizedFieldName;
|
||||
}
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function getRelevantAttributeMap(
|
||||
Entity $fromEntity,
|
||||
Entity $toEntity,
|
||||
string $fromField,
|
||||
string $toField
|
||||
): array {
|
||||
|
||||
$fromAttributeList = $this->getActualAttributes($fromEntity, $fromField);
|
||||
$toAttributeList = $this->getActualAttributes($toEntity, $toField);
|
||||
|
||||
$fromType = $this->getFieldType($fromEntity, $fromField);
|
||||
$toType = $this->getFieldType($toEntity, $toField);
|
||||
|
||||
$ignoreActualAttributesOnValueCopyFieldList = $this->metadata
|
||||
->get(['entityDefs', 'Workflow', 'ignoreActualAttributesOnValueCopyFieldList'], []);
|
||||
|
||||
if (in_array($fromType, $ignoreActualAttributesOnValueCopyFieldList)) {
|
||||
$fromAttributeList = [$fromField];
|
||||
}
|
||||
|
||||
if (in_array($toType, $ignoreActualAttributesOnValueCopyFieldList)) {
|
||||
$toAttributeList = [$toField];
|
||||
}
|
||||
|
||||
$attributeMap = [];
|
||||
|
||||
if (count($fromAttributeList) == count($toAttributeList)) {
|
||||
if (
|
||||
$fromType === 'datetimeOptional' &&
|
||||
$toType === 'datetimeOptional'
|
||||
) {
|
||||
if ($fromEntity->get($fromAttributeList[1])) {
|
||||
$attributeMap[$fromAttributeList[1]] = $toAttributeList[1];
|
||||
} else {
|
||||
$attributeMap[$fromAttributeList[0]] = $toAttributeList[0];
|
||||
}
|
||||
|
||||
return $attributeMap;
|
||||
}
|
||||
|
||||
foreach ($fromAttributeList as $key => $name) {
|
||||
$attributeMap[$name] = $toAttributeList[$key];
|
||||
}
|
||||
|
||||
return $attributeMap;
|
||||
}
|
||||
|
||||
if (
|
||||
$fromType === 'datetimeOptional' ||
|
||||
$toType === 'datetimeOptional'
|
||||
) {
|
||||
if (count($toAttributeList) > count($fromAttributeList)) {
|
||||
if ($fromType === 'date') {
|
||||
$attributeMap[$fromAttributeList[0]] = $toAttributeList[1];
|
||||
} else {
|
||||
$attributeMap[$fromAttributeList[0]] = $toAttributeList[0];
|
||||
}
|
||||
|
||||
return $attributeMap;
|
||||
}
|
||||
|
||||
if ($toType === 'date') {
|
||||
if ($fromEntity->get($fromAttributeList[1])) {
|
||||
$attributeMap[$fromAttributeList[1]] = $toAttributeList[0];
|
||||
} else {
|
||||
$attributeMap[$fromAttributeList[0]] = $toAttributeList[0];
|
||||
}
|
||||
} else {
|
||||
$attributeMap[$fromAttributeList[0]] = $toAttributeList[0];
|
||||
}
|
||||
}
|
||||
|
||||
return $attributeMap;
|
||||
}
|
||||
|
||||
private function handlePersonName(string $toAttribute, stdClass $values, string $toField): void
|
||||
{
|
||||
if (empty($values->$toAttribute)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fullNameValue = trim($values->$toAttribute);
|
||||
|
||||
$firstNameAttribute = 'first' . ucfirst($toField);
|
||||
$lastNameAttribute = 'last' . ucfirst($toField);
|
||||
|
||||
if (!str_contains($fullNameValue, ' ')) {
|
||||
$lastNameValue = $fullNameValue;
|
||||
$firstNameValue = null;
|
||||
} else {
|
||||
$index = strrpos($fullNameValue, ' ');
|
||||
$firstNameValue = substr($fullNameValue, 0, $index ?: 0);
|
||||
$lastNameValue = substr($fullNameValue, $index + 1);
|
||||
}
|
||||
|
||||
$values->$firstNameAttribute = $firstNameValue;
|
||||
$values->$lastNameAttribute = $lastNameValue;
|
||||
}
|
||||
|
||||
private function loadLink(Entity $entity, string $field): void
|
||||
{
|
||||
if (!$entity instanceof CoreEntity) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($entity->getRelationType($field)) { // ORM types
|
||||
case Entity::MANY_MANY:
|
||||
case Entity::HAS_CHILDREN:
|
||||
try {
|
||||
$entity->loadLinkMultipleField($field);
|
||||
} catch (Exception) {}
|
||||
|
||||
break;
|
||||
|
||||
case Entity::BELONGS_TO:
|
||||
case Entity::HAS_ONE:
|
||||
try {
|
||||
$entity->loadLinkField($field);
|
||||
} catch (Exception) {}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function getFieldType(Entity $entity, string $field): ?string
|
||||
{
|
||||
return $this->metadata->get(['entityDefs', $entity->getEntityType(), 'fields', $field, 'type']);
|
||||
}
|
||||
|
||||
private function getRelatedEntity(CoreEntity $entity, string $relation): ?Entity
|
||||
{
|
||||
if (!$entity->hasRelation($relation)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$relatedEntity = null;
|
||||
|
||||
if ($entity->hasId()) {
|
||||
$relatedEntity = $this->entityManager
|
||||
->getRDBRepository($entity->getEntityType())
|
||||
->getRelation($entity, $relation)
|
||||
->findOne();
|
||||
|
||||
if ($relatedEntity) {
|
||||
return $relatedEntity;
|
||||
}
|
||||
}
|
||||
|
||||
// If the entity is just created and doesn't have relations yet.
|
||||
|
||||
$foreignEntityType = $entity->getRelationParam($relation, 'entity');
|
||||
$idAttribute = $this->normalizeRelatedFieldName($entity, $relation);
|
||||
|
||||
if (
|
||||
$foreignEntityType &&
|
||||
$entity->hasAttribute($idAttribute) &&
|
||||
$entity->get($idAttribute)
|
||||
) {
|
||||
$relatedEntity = $this->entityManager->getEntityById($foreignEntityType, $entity->get($idAttribute));
|
||||
}
|
||||
|
||||
return $relatedEntity;
|
||||
}
|
||||
|
||||
private function getFieldValuesLinkToLinkParent(
|
||||
CoreEntity $fromEntity,
|
||||
string $fromField,
|
||||
string $toField
|
||||
): stdClass {
|
||||
|
||||
$sourceRecordId = $fromEntity->get($fromField . 'Id');
|
||||
$foreignEntityType = $fromEntity->getRelationParam($fromField, 'entity');
|
||||
|
||||
if (!$sourceRecordId || !$foreignEntityType) {
|
||||
return (object) [
|
||||
$toField . 'Id' => null,
|
||||
$toField . 'Type' => null,
|
||||
$toField . 'Name' => null,
|
||||
];
|
||||
}
|
||||
|
||||
return (object) [
|
||||
$toField . 'Id' => $sourceRecordId,
|
||||
$toField . 'Type' => $foreignEntityType,
|
||||
$toField . 'Name' => $fromEntity->get($fromField . 'Name'),
|
||||
];
|
||||
}
|
||||
|
||||
private function getFieldValuesIdToLinkParent(CoreEntity $fromEntity, string $toField): stdClass
|
||||
{
|
||||
return (object) [
|
||||
$toField . 'Id' => $fromEntity->getId(),
|
||||
$toField . 'Type' => $fromEntity->getEntityType(),
|
||||
$toField . 'Name' => $fromEntity->get('name'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $attributes
|
||||
*/
|
||||
private function loadFieldForAttributes(CoreEntity $entity, string $field, array $attributes): void
|
||||
{
|
||||
$hasNotSet = $this->hasNotSetAttribute($entity, $attributes);
|
||||
|
||||
if (!$hasNotSet) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->getSpecificFieldLoader()->process($entity, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $attributes
|
||||
*/
|
||||
private function hasNotSetAttribute(CoreEntity $entity, array $attributes): bool
|
||||
{
|
||||
$hasNotSet = false;
|
||||
|
||||
foreach ($attributes as $it) {
|
||||
if (!$entity->has($it)) {
|
||||
$hasNotSet = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $hasNotSet;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Tools\Workflow\Core;
|
||||
|
||||
use Espo\Core\FieldProcessing\SpecificFieldLoader;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\FieldUtil;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class FieldLoaderHelper
|
||||
{
|
||||
/**
|
||||
* For bc the type is in the docblock.
|
||||
*
|
||||
* @var ?SpecificFieldLoader
|
||||
*/
|
||||
private $specificFieldLoader = null;
|
||||
|
||||
public function __construct(
|
||||
private InjectableFactory $injectableFactory,
|
||||
private FieldUtil $fieldUtil,
|
||||
) {}
|
||||
|
||||
public function load(Entity $entity, string $path): void
|
||||
{
|
||||
/** @phpstan-ignore-next-line function.alreadyNarrowedType */
|
||||
if (!method_exists($this->fieldUtil, 'getFieldOfAttribute')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$field = $this->fieldUtil->getFieldOfAttribute($entity->getEntityType(), $path);
|
||||
|
||||
if (!$field) {
|
||||
return;
|
||||
}
|
||||
|
||||
$loader = $this->getSpecificFieldLoader();
|
||||
|
||||
if (!$loader) {
|
||||
return;
|
||||
}
|
||||
|
||||
$loader->process($entity, $field);
|
||||
}
|
||||
|
||||
private function getSpecificFieldLoader(): ?SpecificFieldLoader
|
||||
{
|
||||
if (!class_exists("Espo\\Core\\FieldProcessing\\SpecificFieldLoader")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$this->specificFieldLoader) {
|
||||
$this->specificFieldLoader = $this->injectableFactory->create(SpecificFieldLoader::class);
|
||||
}
|
||||
|
||||
return $this->specificFieldLoader;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Tools\Workflow\Core;
|
||||
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Modules\Advanced\Core\Workflow\Utils;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use RuntimeException;
|
||||
use stdClass;
|
||||
|
||||
class FieldValueHelper
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private Log $log,
|
||||
private FieldLoaderHelper $fieldLoaderHelper,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get field value for a field/related field. If this field has a relation, get the value from the relation.
|
||||
*
|
||||
* @param ?string $path A field path.
|
||||
*/
|
||||
public function getValue(
|
||||
CoreEntity $entity,
|
||||
?string $path,
|
||||
bool $returnEntity = false,
|
||||
?stdClass $createdEntitiesData = null
|
||||
): mixed {
|
||||
|
||||
if (str_starts_with($path, 'created:')) {
|
||||
[$alias, $field] = explode('.', substr($path, 8));
|
||||
|
||||
if (!$createdEntitiesData || !isset($createdEntitiesData->$alias)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$entityTypeValue = $createdEntitiesData->$alias->entityType ?? null;
|
||||
$entityIdValue = $createdEntitiesData->$alias->entityId ?? null;
|
||||
|
||||
if (!$entityTypeValue || !$entityIdValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$entity = $this->entityManager->getEntityById($entityTypeValue, $entityIdValue);
|
||||
|
||||
if (!$entity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$path = $field;
|
||||
} else if (str_contains($path, '.')) {
|
||||
[$first, $foreignName] = explode('.', $path);
|
||||
|
||||
$relatedEntity = $this->getRelatedEntity($entity, $first);
|
||||
|
||||
if ($relatedEntity instanceof CoreEntity) {
|
||||
$entity = $relatedEntity;
|
||||
|
||||
$path = $foreignName;
|
||||
} else {
|
||||
$this->log->warning("Workflow: Could not get related entity by path '$path'.");
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$entity instanceof CoreEntity) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
if ($path && $entity->hasRelation($path)) {
|
||||
$relatedEntity = $this->getRelatedEntityForRelation($entity, $path);
|
||||
|
||||
if ($relatedEntity instanceof CoreEntity) {
|
||||
$foreignKey = $entity->getRelationParam($path, 'foreignKey') ?? 'id';
|
||||
|
||||
return $returnEntity ? $relatedEntity : $relatedEntity->get($foreignKey);
|
||||
}
|
||||
|
||||
if (!$relatedEntity) {
|
||||
$normalizedFieldName = Utils::normalizeFieldName($entity, $path);
|
||||
|
||||
if (!$entity->isNew() && $entity->hasLinkMultipleField($path)) {
|
||||
$entity->loadLinkMultipleField($path);
|
||||
}
|
||||
|
||||
if ($entity->getRelationType($path) === Entity::BELONGS_TO_PARENT && !$returnEntity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$fieldValue = $returnEntity ?
|
||||
$this->getParentEntity($entity, $path) :
|
||||
$this->getParentValue($entity, $normalizedFieldName);
|
||||
|
||||
if (isset($fieldValue)) {
|
||||
return $fieldValue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($entity->hasLinkMultipleField($path)) {
|
||||
$entity->loadLinkMultipleField($path);
|
||||
}
|
||||
|
||||
if ($relatedEntity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $entity->get($path . 'Ids');
|
||||
}
|
||||
|
||||
switch ($entity->getAttributeType($path)) {
|
||||
// @todo Revise.
|
||||
case 'linkParent':
|
||||
$path .= 'Id';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ($returnEntity) {
|
||||
return $entity;
|
||||
}
|
||||
|
||||
if (!$entity->hasAttribute($path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$entity->has($path)) {
|
||||
$this->fieldLoaderHelper->load($entity, $path);
|
||||
}
|
||||
|
||||
return $entity->get($path);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CoreEntity|Entity|null
|
||||
*/
|
||||
private function getParentEntity(CoreEntity $entity, string $fieldName)
|
||||
{
|
||||
if (!$entity->hasRelation($fieldName)) {
|
||||
return $entity;
|
||||
}
|
||||
|
||||
$normalizedFieldName = Utils::normalizeFieldName($entity, $fieldName);
|
||||
|
||||
$fieldValue = $this->getParentValue($entity, $normalizedFieldName);
|
||||
|
||||
if (isset($fieldValue) && is_string($fieldValue)) {
|
||||
$fieldEntityDefs = $this->entityManager->getMetadata()->get($entity->getEntityType());
|
||||
|
||||
if (isset($fieldEntityDefs['relations'][$fieldName]['entity'])) {
|
||||
$fieldEntity = $fieldEntityDefs['relations'][$fieldName]['entity'];
|
||||
|
||||
return $this->entityManager->getEntityById($fieldEntity, $fieldValue);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parent field value. Works for parent and regular fields,
|
||||
*
|
||||
* @param string|string[] $normalizedFieldName
|
||||
* @return mixed
|
||||
*/
|
||||
private function getParentValue(Entity $entity, $normalizedFieldName)
|
||||
{
|
||||
if (is_array($normalizedFieldName)) {
|
||||
$value = [];
|
||||
|
||||
foreach ($normalizedFieldName as $fieldName) {
|
||||
if ($entity->hasAttribute($fieldName)) {
|
||||
$value[$fieldName] = $entity->get($fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($entity->hasAttribute($normalizedFieldName)) {
|
||||
return $entity->get($normalizedFieldName);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getRelatedEntityForRelation(CoreEntity $entity, string $relation): ?Entity
|
||||
{
|
||||
if ($entity->getRelationType($relation) === Entity::BELONGS_TO_PARENT) {
|
||||
$valueType = $entity->get($relation . 'Type');
|
||||
$valueId = $entity->get($relation . 'Id');
|
||||
|
||||
if ($valueType && $valueId) {
|
||||
return $this->entityManager->getEntityById($valueType, $valueId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (in_array($entity->getRelationType($relation), [Entity::BELONGS_TO, Entity::HAS_ONE])) {
|
||||
return $this->entityManager
|
||||
->getRDBRepository($entity->getEntityType())
|
||||
->getRelation($entity, $relation)
|
||||
->findOne();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getRelatedEntity(CoreEntity $entity, string $relation): ?Entity
|
||||
{
|
||||
if (!$entity->hasRelation($relation)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
in_array($entity->getRelationType($relation), [
|
||||
Entity::BELONGS_TO,
|
||||
Entity::HAS_ONE,
|
||||
Entity::BELONGS_TO_PARENT,
|
||||
])
|
||||
) {
|
||||
$relatedEntity = $this->entityManager
|
||||
->getRDBRepository($entity->getEntityType())
|
||||
->getRelation($entity, $relation)
|
||||
->findOne();
|
||||
|
||||
if ($relatedEntity) {
|
||||
return $relatedEntity;
|
||||
}
|
||||
}
|
||||
|
||||
// If the entity is just created and doesn't have added relations.
|
||||
|
||||
$foreignEntityType = $entity->getRelationParam($relation, 'entity');
|
||||
$idAttribute = Utils::normalizeFieldName($entity, $relation);
|
||||
|
||||
if (
|
||||
!$foreignEntityType ||
|
||||
!$entity->hasAttribute($idAttribute) ||
|
||||
!$entity->get($idAttribute)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->entityManager->getEntityById($foreignEntityType, $entity->get($idAttribute));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Tools\Workflow\Core;
|
||||
|
||||
class PlaceholderHelper
|
||||
{
|
||||
public function __construct(
|
||||
private SecretProvider $secretProvider,
|
||||
) {}
|
||||
|
||||
public function applySecrets(string $content): string
|
||||
{
|
||||
return preg_replace_callback('/{#secrets\.([A-Za-z0-9_]+)}/', function ($matches) {
|
||||
$name = trim($matches[1]);
|
||||
|
||||
$secret = $this->secretProvider->get($name);
|
||||
|
||||
return $secret ?? '';
|
||||
}, $content);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Tools\Workflow\Core;
|
||||
|
||||
class RecipientIds
|
||||
{
|
||||
/**
|
||||
* @param string[] $ids
|
||||
*/
|
||||
public function __construct(
|
||||
private ?string $entityType = null,
|
||||
private array $ids = [],
|
||||
private bool $isOne = false,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getIds(): array
|
||||
{
|
||||
return $this->ids;
|
||||
}
|
||||
|
||||
public function getEntityType(): ?string
|
||||
{
|
||||
return $this->entityType;
|
||||
}
|
||||
|
||||
public function isOne(): bool
|
||||
{
|
||||
return $this->isOne;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Tools\Workflow\Core;
|
||||
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Core\Record\ServiceFactory;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Advanced\Tools\Workflow\Core\FieldValueHelper;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Tools\Stream\Service;
|
||||
use RuntimeException;
|
||||
|
||||
class RecipientProvider
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private InjectableFactory $injectableFactory,
|
||||
private ServiceFactory $serviceFactory,
|
||||
private FieldValueHelper $fieldValueHelper,
|
||||
) {}
|
||||
|
||||
public function get(Entity $entity, string $target): RecipientIds
|
||||
{
|
||||
if (!$entity instanceof CoreEntity) {
|
||||
return new RecipientIds();
|
||||
}
|
||||
|
||||
$link = $target;
|
||||
$targetEntity = $entity;
|
||||
|
||||
if (str_starts_with($link, 'link:')) {
|
||||
$link = substr($link, 5);
|
||||
}
|
||||
|
||||
if (strpos($link, '.')) {
|
||||
[$firstLink, $link] = explode('.', $link);
|
||||
|
||||
$relationType = $entity->getRelationType($firstLink);
|
||||
|
||||
if (in_array($relationType, [Entity::HAS_MANY, Entity::MANY_MANY])) {
|
||||
$collection = $this->entityManager
|
||||
->getRDBRepository($entity->getEntityType())
|
||||
->getRelation($entity, $firstLink)
|
||||
->sth()
|
||||
->find();
|
||||
|
||||
$ids = [];
|
||||
$entityType = null;
|
||||
|
||||
foreach ($collection as $targetEntity) {
|
||||
$entityType ??= $targetEntity->getEntityType();
|
||||
|
||||
$itemIds = $this->get($targetEntity, "link:$link")->getIds();
|
||||
|
||||
$ids = array_merge($ids, $itemIds);
|
||||
}
|
||||
|
||||
return new RecipientIds($entityType, array_unique($ids));
|
||||
}
|
||||
|
||||
$targetEntity = $this->entityManager
|
||||
->getRDBRepository($entity->getEntityType())
|
||||
->getRelation($entity, $firstLink)
|
||||
->findOne();
|
||||
|
||||
if (!$targetEntity) {
|
||||
return new RecipientIds();
|
||||
}
|
||||
}
|
||||
|
||||
if ($link === 'followers') {
|
||||
if (!class_exists("Espo\\Tools\\Stream\\Service")) {
|
||||
/** @noinspection PhpUndefinedMethodInspection */
|
||||
return new RecipientIds(
|
||||
User::ENTITY_TYPE,
|
||||
/** @phpstan-ignore-next-line */
|
||||
$this->serviceFactory->create('Stream')->getEntityFolowerIdList($targetEntity)
|
||||
);
|
||||
}
|
||||
|
||||
/** @var Service $streamService */
|
||||
$streamService = $this->injectableFactory->create("Espo\\Tools\\Stream\\Service");
|
||||
|
||||
return new RecipientIds(
|
||||
User::ENTITY_TYPE,
|
||||
$streamService->getEntityFollowerIdList($targetEntity)
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
$targetEntity->hasRelation($link) &&
|
||||
(
|
||||
$targetEntity->getRelationType($link) === Entity::HAS_MANY ||
|
||||
$targetEntity->getRelationType($link) === Entity::MANY_MANY
|
||||
)
|
||||
) {
|
||||
$collection = $this->entityManager
|
||||
->getRDBRepository($targetEntity->getEntityType())
|
||||
->getRelation($targetEntity, $link)
|
||||
->select(['id'])
|
||||
->sth()
|
||||
->find();
|
||||
|
||||
$ids = [];
|
||||
$entityType = null;
|
||||
|
||||
foreach ($collection as $e) {
|
||||
$ids[] = $e->getId();
|
||||
|
||||
$entityType ??= $e->getEntityType();
|
||||
}
|
||||
|
||||
return new RecipientIds($entityType, $ids);
|
||||
}
|
||||
|
||||
if (!$targetEntity instanceof CoreEntity) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
$fieldEntity = $this->fieldValueHelper->getValue($targetEntity, $link, true);
|
||||
|
||||
if ($fieldEntity instanceof Entity) {
|
||||
return new RecipientIds($fieldEntity->getEntityType(), [$fieldEntity->getId()], true);
|
||||
}
|
||||
|
||||
return new RecipientIds();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Tools\Workflow\Core;
|
||||
|
||||
use Espo\Core\ORM\Repository\Option\SaveContext;
|
||||
|
||||
class SaveContextHelper
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
* @return ?SaveContext
|
||||
*/
|
||||
public static function createDerived(array $options)
|
||||
{
|
||||
if (!class_exists("Espo\\Core\\ORM\\Repository\\Option\\SaveContext")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$newSaveContext = null;
|
||||
|
||||
$saveContext = $options[SaveContext::NAME] ?? null;
|
||||
|
||||
if (
|
||||
$saveContext instanceof SaveContext &&
|
||||
/** @phpstan-ignore-next-line function.alreadyNarrowedType */
|
||||
method_exists($saveContext, 'getActionId')
|
||||
) {
|
||||
$newSaveContext = new SaveContext($saveContext->getActionId());
|
||||
}
|
||||
|
||||
return $newSaveContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
* @return ?SaveContext
|
||||
*/
|
||||
public static function obtainFromRawOptions(array $options)
|
||||
{
|
||||
if (!class_exists("Espo\\Core\\ORM\\Repository\\Option\\SaveContext")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$saveContext = $options[SaveContext::NAME] ?? null;
|
||||
|
||||
if (!$saveContext instanceof SaveContext) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $saveContext;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Tools\Workflow\Core;
|
||||
|
||||
use Espo\Core\Utils\Crypt;
|
||||
use Espo\Entities\AppSecret;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Query\Part\Condition;
|
||||
use Espo\ORM\Query\Part\Expression;
|
||||
|
||||
class SecretProvider
|
||||
{
|
||||
public function __construct(
|
||||
private Crypt $crypt,
|
||||
private EntityManager $entityManager,
|
||||
) {}
|
||||
|
||||
public function get(string $name): ?string
|
||||
{
|
||||
if (!$this->entityManager->hasRepository('AppSecret')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$secret = $this->entityManager
|
||||
->getRDBRepositoryByClass(AppSecret::class)
|
||||
->where(
|
||||
Condition::equal(
|
||||
Expression::binary(Expression::column('name')),
|
||||
$name
|
||||
)
|
||||
)
|
||||
->findOne();
|
||||
|
||||
if (!$secret) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->crypt->decrypt($secret->getValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Tools\Workflow\Core;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use stdClass;
|
||||
|
||||
class TargetProvider
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private Metadata $metadata,
|
||||
private User $user
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return iterable<Entity>
|
||||
*/
|
||||
public function get(Entity $entity, ?string $target, ?stdClass $createdEntitiesData = null): iterable
|
||||
{
|
||||
if (!$target || $target === 'targetEntity') {
|
||||
if (!$entity->hasId()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$targetEntity = $this->entityManager->getEntityById($entity->getEntityType(), $entity->getId());
|
||||
|
||||
return self::wrapEntityIntoArray($targetEntity);
|
||||
}
|
||||
|
||||
if (str_starts_with($target, 'created:')) {
|
||||
return self::wrapEntityIntoArray(
|
||||
$this->getCreated($target, $createdEntitiesData)
|
||||
);
|
||||
}
|
||||
|
||||
if (str_starts_with($target, 'link:')) {
|
||||
$path = explode('.', substr($target, 5));
|
||||
|
||||
$pointerEntity = $entity;
|
||||
|
||||
foreach ($path as $i => $link) {
|
||||
$type = $this->metadata->get(['entityDefs', $pointerEntity->getEntityType(), 'links', $link, 'type']);
|
||||
|
||||
if (!$type) {
|
||||
throw new Error("Workflow action: Bad target $target. Not existing link.");
|
||||
}
|
||||
|
||||
$isLast = $i === count($path) - 1;
|
||||
|
||||
$relation = $this->entityManager
|
||||
->getRDBRepository($pointerEntity->getEntityType())
|
||||
->getRelation($pointerEntity, $link);
|
||||
|
||||
if ($isLast) {
|
||||
return $relation->sth()->find();
|
||||
}
|
||||
|
||||
$pointerEntity = $this->entityManager
|
||||
->getRDBRepository($pointerEntity->getEntityType())
|
||||
->getRelation($pointerEntity, $link)
|
||||
->findOne();
|
||||
|
||||
if (!$pointerEntity instanceof Entity) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($target == 'currentUser') {
|
||||
return [$this->user];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getCreated(string $target, ?stdClass $createdEntitiesData): ?Entity
|
||||
{
|
||||
$alias = str_starts_with($target, 'created:') ? substr($target, 8) : $target;
|
||||
|
||||
if (!$createdEntitiesData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!property_exists($createdEntitiesData, $alias)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$id = $createdEntitiesData->$alias->entityId ?? null;
|
||||
$entityType = $createdEntitiesData->$alias->entityType ?? null;
|
||||
|
||||
if (!$id || !$entityType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->entityManager->getEntityById($entityType, $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Entity|null $entity
|
||||
* @return Entity[]
|
||||
*/
|
||||
private static function wrapEntityIntoArray(?Entity $entity): array
|
||||
{
|
||||
if (!$entity) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [$entity];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Tools\Workflow\Jobs;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Formula\Exceptions\Error as FormulaError;
|
||||
use Espo\Core\Job\Job;
|
||||
use Espo\Core\Job\Job\Data;
|
||||
use Espo\Core\Job\JobSchedulerFactory;
|
||||
use Espo\Modules\Advanced\Entities\Workflow;
|
||||
use Espo\Modules\Advanced\Tools\Report\ListType\RunParams as ListRunParams;
|
||||
use Espo\Modules\Advanced\Tools\Report\Service as ReportService;
|
||||
use Espo\Modules\Advanced\Tools\Workflow\Service;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
|
||||
class RunScheduledWorkflow implements Job
|
||||
{
|
||||
public function __construct(
|
||||
private ReportService $reportService,
|
||||
private EntityManager $entityManager,
|
||||
private Service $service,
|
||||
private JobSchedulerFactory $jobSchedulerFactory,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function run(Data $data): void
|
||||
{
|
||||
$workflowId = $data->getTargetId() ?? $data->get('workflowId');
|
||||
|
||||
if (!$workflowId) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
$workflow = $this->entityManager->getRDBRepositoryByClass(Workflow::class)->getById($workflowId);
|
||||
|
||||
if (!$workflow) {
|
||||
throw new RuntimeException("Workflow $workflowId not found.");
|
||||
}
|
||||
|
||||
if (!$workflow->isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$targetReport = $this->entityManager
|
||||
->getRDBRepository(Workflow::ENTITY_TYPE)
|
||||
->getRelation($workflow, 'targetReport')
|
||||
->findOne();
|
||||
|
||||
if (!$targetReport) {
|
||||
throw new RuntimeException("Workflow $workflowId: Target report not found.");
|
||||
}
|
||||
|
||||
$result = $this->reportService->runList(
|
||||
id: $targetReport->getId(),
|
||||
runParams: ListRunParams::create()->withReturnSthCollection(),
|
||||
);
|
||||
|
||||
foreach ($result->getCollection() as $entity) {
|
||||
try {
|
||||
$this->runScheduledWorkflowForEntity(
|
||||
$workflow->getId(),
|
||||
$entity->getEntityType(),
|
||||
$entity->getId()
|
||||
);
|
||||
} catch (Exception) {
|
||||
// @todo Revise.
|
||||
|
||||
$this->jobSchedulerFactory
|
||||
->create()
|
||||
->setClassName(RunScheduledWorkflowForEntity::class)
|
||||
->setGroup('scheduled-workflows')
|
||||
->setData([
|
||||
'workflowId' => $workflow->getId(),
|
||||
'entityType' => $entity->getEntityType(),
|
||||
'entityId' => $entity->getId(),
|
||||
])
|
||||
->schedule();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FormulaError
|
||||
* @throws Error
|
||||
*/
|
||||
private function runScheduledWorkflowForEntity(string $workflowId, string $entityType, string $id): void
|
||||
{
|
||||
// @todo Create jobs if a parameter is enabled.
|
||||
|
||||
$entity = $this->entityManager->getEntityById($entityType, $id);
|
||||
|
||||
if (!$entity) {
|
||||
throw new RuntimeException("Workflow $workflowId: Entity $entityType $id not found.");
|
||||
}
|
||||
|
||||
$this->service->triggerWorkflow($entity, $workflowId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Tools\Workflow\Jobs;
|
||||
|
||||
use Espo\Core\Job\Job;
|
||||
use Espo\Core\Job\Job\Data;
|
||||
use Espo\Modules\Advanced\Tools\Workflow\Service;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class RunScheduledWorkflowForEntity implements Job
|
||||
{
|
||||
private EntityManager $entityManager;
|
||||
private Service $service;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
Service $service
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
public function run(Data $data): void
|
||||
{
|
||||
$data = $data->getRaw();
|
||||
|
||||
$entityType = $data->entityType;
|
||||
$id = $data->entityId;
|
||||
$workflowId = $data->workflowId;
|
||||
|
||||
$entity = $this->entityManager->getEntityById($entityType, $id);
|
||||
|
||||
if (!$entity) {
|
||||
throw new RuntimeException("Workflow $workflowId: Entity $entityType $id not found.");
|
||||
}
|
||||
|
||||
$this->service->triggerWorkflow($entity, $workflowId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Tools\Workflow\Jobs;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Job\Job;
|
||||
use Espo\Core\Job\Job\Data;
|
||||
use Espo\Core\Mail\Exceptions\NoSmtp;
|
||||
use Espo\Modules\Advanced\Tools\Workflow\SendEmailService;
|
||||
|
||||
class SendEmail implements Job
|
||||
{
|
||||
private SendEmailService $service;
|
||||
|
||||
public function __construct(SendEmailService $service)
|
||||
{
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
* @throws NoSmtp
|
||||
*/
|
||||
public function run(Data $data): void
|
||||
{
|
||||
$this->service->send($data->getRaw());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Tools\Workflow\Jobs;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Job\Job;
|
||||
use Espo\Core\Job\Job\Data;
|
||||
use Espo\Modules\Advanced\Tools\Workflow\Service;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
class TriggerWorkflow implements Job
|
||||
{
|
||||
public function __construct(
|
||||
private Service $service,
|
||||
private EntityManager $entityManager
|
||||
) {}
|
||||
|
||||
public function run(Data $data): void
|
||||
{
|
||||
$data = $data->getRaw();
|
||||
|
||||
if (
|
||||
empty($data->entityId) ||
|
||||
empty($data->entityType) ||
|
||||
empty($data->nextWorkflowId)
|
||||
) {
|
||||
throw new Error("Workflow[$data->workflowId][triggerWorkflow]: Not sufficient job data.");
|
||||
}
|
||||
|
||||
$entityId = $data->entityId;
|
||||
$entityType = $data->entityType;
|
||||
|
||||
$entity = $this->entityManager->getEntityById($entityType, $entityId);
|
||||
|
||||
if (!$entity) {
|
||||
throw new Error("Workflow[$data->workflowId][triggerWorkflow]: Entity not found.");
|
||||
}
|
||||
|
||||
$values = $data->values ?? null;
|
||||
|
||||
if (is_object($values)) {
|
||||
$values = get_object_vars($values);
|
||||
|
||||
foreach ($values as $attribute => $value) {
|
||||
$entity->setFetched($attribute, $value);
|
||||
}
|
||||
}
|
||||
|
||||
$this->service->triggerWorkflow($entity, $data->nextWorkflowId, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Tools\Workflow\Jobs;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Job\Job;
|
||||
use Espo\Core\Job\Job\Data;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Modules\Advanced\Entities\Workflow;
|
||||
use Espo\Modules\Advanced\Tools\Workflow\Core\TargetProvider;
|
||||
use Espo\Modules\Advanced\Tools\Workflow\Service;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
|
||||
class TriggerWorkflowMany implements Job
|
||||
{
|
||||
public function __construct(
|
||||
private TargetProvider $targetProvider,
|
||||
private EntityManager $entityManager,
|
||||
private Service $service,
|
||||
private Log $log
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
public function run(Data $data): void
|
||||
{
|
||||
$workflowId = $data->get('nextWorkflowId');
|
||||
$entityId = $data->get('entityId');
|
||||
$entityType = $data->get('entityType');
|
||||
$target = $data->get('target');
|
||||
|
||||
if (!is_string($target)) {
|
||||
throw new RuntimeException("No target.");
|
||||
}
|
||||
|
||||
if (!is_string($workflowId)) {
|
||||
throw new RuntimeException("No nextWorkflowId.");
|
||||
}
|
||||
|
||||
if (!is_string($entityId)) {
|
||||
throw new RuntimeException("No entityId.");
|
||||
}
|
||||
|
||||
if (!is_string($entityType)) {
|
||||
throw new RuntimeException("No entityType.");
|
||||
}
|
||||
|
||||
$entity = $this->entityManager->getEntityById($entityType, $entityId);
|
||||
|
||||
if (!$entity) {
|
||||
return;
|
||||
}
|
||||
|
||||
$workflow = $this->entityManager->getRDBRepositoryByClass(Workflow::class)->getById($workflowId);
|
||||
|
||||
if (!$workflow) {
|
||||
throw new RuntimeException("No workflow $workflowId.");
|
||||
}
|
||||
|
||||
$targetEntityList = $this->targetProvider->get($entity, $target);
|
||||
|
||||
foreach ($targetEntityList as $targetEntity) {
|
||||
try {
|
||||
$this->service->triggerWorkflow($targetEntity, $workflowId);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$this->log->error("Trigger workflow $workflowId for entity $entityId: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
661
custom/Espo/Modules/Advanced/Tools/Workflow/SendEmailService.php
Normal file
661
custom/Espo/Modules/Advanced/Tools/Workflow/SendEmailService.php
Normal file
@@ -0,0 +1,661 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Tools\Workflow;
|
||||
|
||||
use Espo\Core\Mail\Exceptions\NoSmtp;
|
||||
use Espo\Core\Mail\Sender;
|
||||
use Espo\Core\Mail\SenderParams;
|
||||
use Espo\Core\Mail\SmtpParams;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Tools\EmailTemplate\Result;
|
||||
use Laminas\Mail\Message;
|
||||
|
||||
use Espo\Core\Mail\Account\GroupAccount\AccountFactory as GroupAccountFactory;
|
||||
use Espo\Core\Mail\Account\PersonalAccount\AccountFactory as PersonalAccountFactory;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Mail\EmailSender;
|
||||
use Espo\Core\Record\ServiceContainer;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Hasher;
|
||||
use Espo\Core\Utils\Language;
|
||||
use Espo\Entities\Attachment;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\EmailAccount;
|
||||
use Espo\Entities\EmailTemplate;
|
||||
use Espo\Entities\InboundEmail;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Advanced\Core\Workflow\Helper;
|
||||
use Espo\Modules\Advanced\Entities\BpmnProcess as BpmnProcessEntity;
|
||||
use Espo\Modules\Advanced\Entities\Workflow as WorkflowEntity;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Tools\EmailTemplate\Processor as EmailTemplateProcessor;
|
||||
use Espo\Tools\EmailTemplate\Data as EmailTemplateData;
|
||||
use Espo\Tools\EmailTemplate\Params as EmailTemplateParams;
|
||||
|
||||
use RuntimeException;
|
||||
use Exception;
|
||||
use stdClass;
|
||||
|
||||
class SendEmailService
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private ServiceContainer $recordServiceContainer,
|
||||
private Config $config,
|
||||
private Helper $workflowHelper,
|
||||
private EmailSender $emailSender,
|
||||
private Hasher $hasher,
|
||||
private Language $defaultLanguage,
|
||||
private EmailTemplateProcessor $emailTemplateProcessor,
|
||||
private InjectableFactory $injectableFactory
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Send email for a workflow.
|
||||
* @return bool|string
|
||||
* @throws Error
|
||||
* @throws NoSmtp
|
||||
* @todo Introduce SendEmailData class.
|
||||
*/
|
||||
public function send(stdClass $data)
|
||||
{
|
||||
$workflowId = $data->workflowId;
|
||||
|
||||
if (!$this->validateSendEmailData($data)) {
|
||||
throw new Error("Workflow[$workflowId][sendEmail]: Email data is invalid.");
|
||||
}
|
||||
|
||||
$data->doNotStore ??= false;
|
||||
$data->returnEmailId ??= false;
|
||||
$data->from ??= (object) [];
|
||||
$data->to ??= (object) [];
|
||||
$data->cc ??= null;
|
||||
$data->replyTo ??= null;
|
||||
$data->attachmentIds ??= [];
|
||||
|
||||
/**
|
||||
* @var object{
|
||||
* variables?: stdClass,
|
||||
* optOutLink?: bool,
|
||||
* attachmentIds: string[],
|
||||
* entityType?: string|null,
|
||||
* entityId?: string|null,
|
||||
* from: stdClass,
|
||||
* to: stdClass,
|
||||
* cc: stdClass|null,
|
||||
* replyTo: stdClass|null,
|
||||
* doNotStore: bool,
|
||||
* returnEmailId: bool,
|
||||
* } & stdClass $data
|
||||
*/
|
||||
|
||||
if ($workflowId) {
|
||||
$workflow = $this->entityManager->getRDBRepositoryByClass(WorkflowEntity::class)->getById($workflowId);
|
||||
|
||||
if (!$workflow || !$workflow->isActive()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$entity = null;
|
||||
|
||||
if (!empty($data->entityType) && !empty($data->entityId)) {
|
||||
$entity = $this->entityManager->getEntityById($data->entityType, $data->entityId);
|
||||
}
|
||||
|
||||
if (!$entity) {
|
||||
throw new Error("Workflow[$workflowId][sendEmail]: Target Entity is not found.");
|
||||
}
|
||||
|
||||
$this->recordServiceContainer->get($entity->getEntityType())
|
||||
->loadAdditionalFields($entity);
|
||||
|
||||
$fromAddress = $this->getEmailAddress($data->from);
|
||||
$toAddress = $this->getEmailAddress($data->to);
|
||||
$replyToAddress = !empty($data->replyTo) ? $this->getEmailAddress($data->replyTo) : null;
|
||||
$ccAddress = !empty($data->cc) ? $this->getEmailAddress($data->cc) : null;
|
||||
|
||||
if (!$fromAddress) {
|
||||
throw new Error("Workflow[$workflowId][sendEmail]: From email address is empty or could not be obtained.");
|
||||
}
|
||||
|
||||
if (!$toAddress) {
|
||||
throw new Error("Workflow[$workflowId][sendEmail]: To email address is empty.");
|
||||
}
|
||||
|
||||
/** @var array<string, Entity> $entityHash */
|
||||
$entityHash = [$data->entityType => $entity];
|
||||
|
||||
if (
|
||||
isset($data->to->entityType) &&
|
||||
isset($data->to->entityId) &&
|
||||
$data->to->entityType !== $data->entityType
|
||||
) {
|
||||
/** @var string $toEntityType */
|
||||
$toEntityType = $data->to->entityType;
|
||||
|
||||
$toEntity = $this->entityManager->getEntityById($toEntityType, $data->to->entityId);
|
||||
|
||||
if ($toEntity) {
|
||||
$entityHash[$toEntityType] = $toEntity;
|
||||
}
|
||||
}
|
||||
|
||||
$fromName = null;
|
||||
|
||||
if (
|
||||
isset($data->from->entityType) &&
|
||||
isset($data->from->entityId) &&
|
||||
$data->from->entityType === User::ENTITY_TYPE
|
||||
) {
|
||||
$user = $this->entityManager->getRDBRepositoryByClass(User::class)->getById($data->from->entityId);
|
||||
|
||||
if ($user) {
|
||||
$entityHash[User::ENTITY_TYPE] = $user;
|
||||
|
||||
$fromName = $user->getName();
|
||||
}
|
||||
}
|
||||
|
||||
$sender = $this->emailSender->create();
|
||||
|
||||
$templateResult = $this->getTemplateResult(
|
||||
data: $data,
|
||||
entityHash: $entityHash,
|
||||
toEmailAddress: $toAddress,
|
||||
entity: $entity,
|
||||
);
|
||||
|
||||
[$subject, $body] = $this->prepareSubjectBody(
|
||||
templateResult: $templateResult,
|
||||
data: $data,
|
||||
toEmailAddress: $toAddress,
|
||||
sender: $sender,
|
||||
);
|
||||
|
||||
$emailData = [
|
||||
'from' => $fromAddress,
|
||||
'to' => $toAddress,
|
||||
'cc' => $ccAddress,
|
||||
'replyTo' => $replyToAddress,
|
||||
'subject' => $subject,
|
||||
'body' => $body,
|
||||
'isHtml' => $templateResult->isHtml(),
|
||||
'parentId' => $entity->getId(),
|
||||
'parentType' => $entity->getEntityType(),
|
||||
];
|
||||
|
||||
if ($fromName !== null) {
|
||||
$emailData['fromName'] = $fromName;
|
||||
}
|
||||
|
||||
$email = $this->entityManager->getRDBRepositoryByClass(Email::class)->getNew();
|
||||
|
||||
$email->setMultiple($emailData);
|
||||
|
||||
$attachmentList = $this->getAttachmentList($templateResult, $data->attachmentIds);
|
||||
|
||||
if (!$data->doNotStore) {
|
||||
// Additional attachments not added intentionally?
|
||||
$email->set('attachmentsIds', $templateResult->getAttachmentIdList());
|
||||
}
|
||||
|
||||
$smtpParams = $this->prepareSmtpParams($data, $fromAddress);
|
||||
|
||||
if ($smtpParams) {
|
||||
$sender->withSmtpParams($smtpParams);
|
||||
}
|
||||
|
||||
$sender->withAttachments($attachmentList);
|
||||
|
||||
if ($replyToAddress) {
|
||||
$senderParams = SenderParams::create()->withReplyToAddress($replyToAddress);
|
||||
|
||||
$sender->withParams($senderParams);
|
||||
}
|
||||
|
||||
try {
|
||||
$sender->send($email);
|
||||
} catch (Exception $e) {
|
||||
$sendExceptionMessage = $e->getMessage();
|
||||
|
||||
throw new Error("Workflow[$workflowId][sendEmail]: $sendExceptionMessage.", 0, $e);
|
||||
}
|
||||
|
||||
if ($data->doNotStore) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->storeEmail($email, $data);
|
||||
|
||||
if ($data->returnEmailId) {
|
||||
return $email->getId();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function validateSendEmailData(stdClass $data): bool
|
||||
{
|
||||
if (
|
||||
!isset($data->entityId) ||
|
||||
!(isset($data->entityType)) ||
|
||||
!isset($data->emailTemplateId) ||
|
||||
!isset($data->from) ||
|
||||
!isset($data->to)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function getEmailAddress(stdClass $data): ?string
|
||||
{
|
||||
if (isset($data->email)) {
|
||||
return $data->email;
|
||||
}
|
||||
|
||||
$entityType = $data->entityType ?? $data->entityName ?? null;
|
||||
|
||||
$entity = null;
|
||||
|
||||
if (isset($entityType) && isset($data->entityId)) {
|
||||
$entity = $this->entityManager->getEntityById($entityType, $data->entityId);
|
||||
}
|
||||
|
||||
$workflowHelper = $this->workflowHelper;
|
||||
|
||||
if (isset($data->type)) {
|
||||
switch ($data->type) {
|
||||
case 'specifiedTeams':
|
||||
$userIds = $workflowHelper->getUserIdsByTeamIds($data->entityIds);
|
||||
|
||||
return implode('; ', $workflowHelper->getUsersEmailAddress($userIds));
|
||||
|
||||
case 'teamUsers':
|
||||
if (!$entity instanceof CoreEntity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$entity->loadLinkMultipleField('teams');
|
||||
$userIds = $workflowHelper->getUserIdsByTeamIds($entity->get('teamsIds'));
|
||||
|
||||
return implode('; ', $workflowHelper->getUsersEmailAddress($userIds));
|
||||
|
||||
case 'followers':
|
||||
if (!$entity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$userIds = $workflowHelper->getFollowerUserIds($entity);
|
||||
|
||||
return implode('; ', $workflowHelper->getUsersEmailAddress($userIds));
|
||||
|
||||
case 'followersExcludingAssignedUser':
|
||||
if (!$entity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$userIds = $workflowHelper->getFollowerUserIdsExcludingAssignedUser($entity);
|
||||
|
||||
return implode('; ', $workflowHelper->getUsersEmailAddress($userIds));
|
||||
|
||||
case 'system':
|
||||
return $this->config->get('outboundEmailFromAddress');
|
||||
|
||||
case 'specifiedUsers':
|
||||
return implode('; ', $workflowHelper->getUsersEmailAddress($data->entityIds));
|
||||
|
||||
case 'specifiedContacts':
|
||||
return implode('; ', $workflowHelper->getEmailAddressesForEntity('Contact', $data->entityIds));
|
||||
}
|
||||
}
|
||||
|
||||
if ($entity instanceof Entity && $entity->hasAttribute('emailAddress')) {
|
||||
return $entity->get('emailAddress');
|
||||
}
|
||||
|
||||
if (
|
||||
isset($data->type) &&
|
||||
isset($entityType) &&
|
||||
isset($data->entityIds) &&
|
||||
is_array($data->entityIds)
|
||||
) {
|
||||
return implode('; ', $workflowHelper->getEmailAddressesForEntity($entityType, $data->entityIds));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function applyTrackingUrlsToEmailBody(string $body, string $toEmailAddress): string
|
||||
{
|
||||
$siteUrl = $this->config->get('siteUrl');
|
||||
|
||||
if (!str_contains($body, '{trackingUrl:')) {
|
||||
return $body;
|
||||
}
|
||||
|
||||
$hash = $this->hasher->hash($toEmailAddress);
|
||||
|
||||
preg_match_all('/\{trackingUrl:(.*?)}/', $body, $matches);
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
if (!$matches || !count($matches)) {
|
||||
return $body;
|
||||
}
|
||||
|
||||
foreach ($matches[0] as $item) {
|
||||
$id = explode(':', trim($item, '{}'), 2)[1] ?? null;
|
||||
|
||||
if (!$id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strpos($id, '.')) {
|
||||
[$id, $uid] = explode('.', $id);
|
||||
|
||||
$uidHash = $this->hasher->hash($uid);
|
||||
|
||||
$url = "$siteUrl?entryPoint=campaignUrl&id=$id&uid=$uid&hash=$uidHash";
|
||||
} else {
|
||||
$url = "$siteUrl?entryPoint=campaignUrl&id=$id&emailAddress=$toEmailAddress&hash=$hash";
|
||||
}
|
||||
|
||||
$body = str_replace($item, $url, $body);
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
* @throws NoSmtp
|
||||
*/
|
||||
private function getUserSmtpParams(string $emailAddress, string $userId): ?SmtpParams
|
||||
{
|
||||
$user = $this->entityManager->getRDBRepositoryByClass(User::class)->getById($userId);
|
||||
|
||||
if (!$user || !$user->isActive()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$emailAccount = $this->entityManager
|
||||
->getRDBRepositoryByClass(EmailAccount::class)
|
||||
->where([
|
||||
'emailAddress' => $emailAddress,
|
||||
'assignedUserId' => $userId,
|
||||
'useSmtp' => true,
|
||||
'status' => EmailAccount::STATUS_ACTIVE,
|
||||
])
|
||||
->findOne();
|
||||
|
||||
if (!$emailAccount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$factory = $this->injectableFactory->create(PersonalAccountFactory::class);
|
||||
|
||||
$params = $factory->create($emailAccount->getId())
|
||||
->getSmtpParams();
|
||||
|
||||
if (!$params) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $params->withFromName($user->getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
* @throws NoSmtp
|
||||
*/
|
||||
private function getGroupSmtpParams(string $emailAddress): ?SmtpParams
|
||||
{
|
||||
$inboundEmail = $this->entityManager
|
||||
->getRDBRepositoryByClass(InboundEmail::class)
|
||||
->where([
|
||||
'status' => InboundEmail::STATUS_ACTIVE,
|
||||
'useSmtp' => true,
|
||||
'smtpHost!=' => null,
|
||||
'emailAddress' => $emailAddress,
|
||||
])
|
||||
->findOne();
|
||||
|
||||
if (!$inboundEmail) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->injectableFactory
|
||||
->create(GroupAccountFactory::class)
|
||||
->create($inboundEmail->getId())
|
||||
->getSmtpParams();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Result $templateResult
|
||||
* @param string[] $attachmentIds
|
||||
* @return Attachment[]
|
||||
*/
|
||||
private function getAttachmentList(Result $templateResult, array $attachmentIds): array
|
||||
{
|
||||
$attachmentList = [];
|
||||
|
||||
foreach (array_merge($templateResult->getAttachmentIdList(), $attachmentIds) as $attachmentId) {
|
||||
$attachment = $this->entityManager
|
||||
->getRDBRepositoryByClass(Attachment::class)
|
||||
->getById($attachmentId);
|
||||
|
||||
if ($attachment) {
|
||||
$attachmentList[] = $attachment;
|
||||
}
|
||||
}
|
||||
|
||||
return $attachmentList;
|
||||
}
|
||||
|
||||
private function storeEmail(Email $email, stdClass $data): void
|
||||
{
|
||||
$processId = $data->processId ?? null;
|
||||
$emailTemplateId = $data->emailTemplateId ?? null;
|
||||
|
||||
$teamsIds = [];
|
||||
|
||||
if ($processId) {
|
||||
$process = $this->entityManager
|
||||
->getRDBRepositoryByClass(BpmnProcessEntity::class)
|
||||
->getById($processId);
|
||||
|
||||
if ($process) {
|
||||
$teamsIds = $process->getLinkMultipleIdList('teams');
|
||||
}
|
||||
} else if ($emailTemplateId) {
|
||||
$emailTemplate = $this->entityManager
|
||||
->getRDBRepositoryByClass(EmailTemplate::class)
|
||||
->getById($emailTemplateId);
|
||||
|
||||
if ($emailTemplate) {
|
||||
$teamsIds = $emailTemplate->getLinkMultipleIdList('teams');
|
||||
}
|
||||
}
|
||||
|
||||
if (count($teamsIds)) {
|
||||
$email->set('teamsIds', $teamsIds);
|
||||
}
|
||||
|
||||
$this->entityManager->saveEntity($email, ['createdById' => 'system']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
* @throws NoSmtp
|
||||
*/
|
||||
private function prepareSmtpParams(stdClass $data, string $fromEmailAddress): ?SmtpParams
|
||||
{
|
||||
if (
|
||||
isset($data->from->entityType) &&
|
||||
$data->from->entityType === User::ENTITY_TYPE &&
|
||||
isset($data->from->entityId)
|
||||
) {
|
||||
return $this->getUserSmtpParams($fromEmailAddress, $data->from->entityId);
|
||||
}
|
||||
|
||||
if (isset($data->from->email)) {
|
||||
return $this->getGroupSmtpParams($fromEmailAddress);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getEmailTemplate(stdClass $data): EmailTemplate
|
||||
{
|
||||
$emailTemplateId = $data->emailTemplateId ?? null;
|
||||
|
||||
if (!$emailTemplateId) {
|
||||
throw new RuntimeException("No email template.");
|
||||
}
|
||||
|
||||
$emailTemplate = $this->entityManager
|
||||
->getRDBRepositoryByClass(EmailTemplate::class)
|
||||
->getById($emailTemplateId);
|
||||
|
||||
if (!$emailTemplate) {
|
||||
throw new RuntimeException("Email template $emailTemplateId not found.");
|
||||
}
|
||||
|
||||
return $emailTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, Entity> $entityHash
|
||||
* @return Result
|
||||
*/
|
||||
private function getTemplateResult(
|
||||
stdClass $data,
|
||||
array $entityHash,
|
||||
string $toEmailAddress,
|
||||
Entity $entity
|
||||
): Result {
|
||||
|
||||
$emailTemplate = $this->getEmailTemplate($data);
|
||||
|
||||
$emailTemplateData = EmailTemplateData::create()
|
||||
->withEntityHash($entityHash)
|
||||
->withEmailAddress($toEmailAddress)
|
||||
->withParentId($entity->getId())
|
||||
->withParentType($entity->getEntityType());
|
||||
|
||||
if (
|
||||
$entity->hasAttribute('parentId') &&
|
||||
$entity->hasAttribute('parentType')
|
||||
) {
|
||||
$emailTemplateData = $emailTemplateData
|
||||
->withRelatedId($entity->get('parentId'))
|
||||
->withRelatedType($entity->get('parentType'));
|
||||
}
|
||||
|
||||
return $this->emailTemplateProcessor->process(
|
||||
$emailTemplate,
|
||||
EmailTemplateParams::create()->withCopyAttachments(),
|
||||
$emailTemplateData
|
||||
);
|
||||
}
|
||||
|
||||
private function applyOptOutLink(
|
||||
string $toEmailAddress,
|
||||
string $body,
|
||||
Result $templateResult,
|
||||
Sender $sender,
|
||||
): string {
|
||||
|
||||
$siteUrl = $this->config->get('siteUrl');
|
||||
|
||||
$hash = $this->hasher->hash($toEmailAddress);
|
||||
|
||||
$optOutUrl = "$siteUrl?entryPoint=unsubscribe&emailAddress=$toEmailAddress&hash=$hash";
|
||||
|
||||
$optOutLink = "<a href=\"$optOutUrl\">" .
|
||||
"{$this->defaultLanguage->translateLabel('Unsubscribe', 'labels', 'Campaign')}</a>";
|
||||
|
||||
$body = str_replace('{optOutUrl}', $optOutUrl, $body);
|
||||
$body = str_replace('{optOutLink}', $optOutLink, $body);
|
||||
|
||||
if (stripos($body, '?entryPoint=unsubscribe') === false) {
|
||||
if ($templateResult->isHtml()) {
|
||||
$body .= "<br><br>" . $optOutLink;
|
||||
} else {
|
||||
$body .= "\n\n" . $optOutUrl;
|
||||
}
|
||||
}
|
||||
|
||||
if (method_exists($sender, 'withAddedHeader')) { /** @phpstan-ignore-line */
|
||||
$sender->withAddedHeader('List-Unsubscribe', '<' . $optOutUrl . '>');
|
||||
} else {
|
||||
$message = new Message();
|
||||
$message->getHeaders()->addHeaderLine('List-Unsubscribe', '<' . $optOutUrl . '>');
|
||||
|
||||
$sender->withMessage($message);
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Result $templateResult
|
||||
* @param object{variables?: stdClass, optOutLink?: bool}&stdClass $data
|
||||
* @return array{?string, ?string}
|
||||
*/
|
||||
private function prepareSubjectBody(
|
||||
Result $templateResult,
|
||||
stdClass $data,
|
||||
string $toEmailAddress,
|
||||
Sender $sender
|
||||
): array {
|
||||
|
||||
$subject = $templateResult->getSubject();
|
||||
$body = $templateResult->getBody();
|
||||
|
||||
if (isset($data->variables)) {
|
||||
foreach (get_object_vars($data->variables) as $key => $value) {
|
||||
if (!is_string($value) && !is_int($value) && !is_float($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_int($value) || is_float($value)) {
|
||||
$value = strval($value);
|
||||
} else if (!$value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$subject = str_replace('{$$' . $key . '}', $value, $subject);
|
||||
$body = str_replace('{$$' . $key . '}', $value, $body);
|
||||
}
|
||||
}
|
||||
|
||||
$body = $this->applyTrackingUrlsToEmailBody($body, $toEmailAddress);
|
||||
|
||||
if ($data->optOutLink ?? false) {
|
||||
$body = $this->applyOptOutLink($toEmailAddress, $body, $templateResult, $sender);
|
||||
}
|
||||
|
||||
return [$subject, $body];
|
||||
}
|
||||
}
|
||||
248
custom/Espo/Modules/Advanced/Tools/Workflow/Service.php
Normal file
248
custom/Espo/Modules/Advanced/Tools/Workflow/Service.php
Normal file
@@ -0,0 +1,248 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Tools\Workflow;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Formula\Exceptions\Error as FormulaError;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Core\Record\ServiceContainer;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Advanced\Controllers\WorkflowLogRecord;
|
||||
use Espo\Modules\Advanced\Core\WorkflowManager;
|
||||
use Espo\Modules\Advanced\Entities\Workflow;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Tools\DynamicLogic\ConditionCheckerFactory;
|
||||
use Espo\Tools\DynamicLogic\Exceptions\BadCondition;
|
||||
use Espo\Tools\DynamicLogic\Item as LogicItem;
|
||||
use RuntimeException;
|
||||
use stdClass;
|
||||
|
||||
class Service
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private Acl $acl,
|
||||
private User $user,
|
||||
private WorkflowManager $workflowManager,
|
||||
private Log $log,
|
||||
private InjectableFactory $injectableFactory,
|
||||
private ServiceContainer $serviceContainer,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function runManual(string $id, string $targetId): TriggerResult
|
||||
{
|
||||
$workflow = $this->getManualWorkflow($id);
|
||||
$entity = $this->getEntityForManualWorkflow($workflow, $targetId);
|
||||
|
||||
$this->processManualWorkflowAccess($workflow, $entity);
|
||||
$this->processCheckManualWorkflowConditions($workflow, $entity);
|
||||
|
||||
try {
|
||||
$result = $this->triggerWorkflow($entity, $workflow->getId(), true);
|
||||
} catch (FormulaError $e) {
|
||||
throw new Error("Formula error.", 500, $e);
|
||||
}
|
||||
|
||||
if (!$result) {
|
||||
throw new RuntimeException("No result.");
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FormulaError
|
||||
* @throws Error
|
||||
*/
|
||||
public function triggerWorkflow(Entity $entity, string $workflowId, bool $mandatory = false): ?TriggerResult
|
||||
{
|
||||
/** @var ?Workflow $workflow */
|
||||
$workflow = $this->entityManager->getEntityById(Workflow::ENTITY_TYPE, $workflowId);
|
||||
|
||||
if (!$workflow) {
|
||||
throw new Error("Workflow $workflowId does not exist.");
|
||||
}
|
||||
|
||||
if (!$workflow->isActive()) {
|
||||
if (!$mandatory) {
|
||||
$this->log->debug("Workflow $workflowId not triggerred as it's not active.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
throw new Error("Workflow $workflowId is not active.");
|
||||
}
|
||||
|
||||
if (!$this->workflowManager->checkConditions($workflow, $entity)) {
|
||||
$this->log->debug("Workflow $workflowId not triggerred as conditions are not met.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$workflowLogRecord = $this->entityManager->getNewEntity(WorkflowLogRecord::ENTITY_TYPE);
|
||||
|
||||
$workflowLogRecord->set([
|
||||
'workflowId' => $workflowId,
|
||||
'targetId' => $entity->getId(),
|
||||
'targetType' => $entity->getEntityType()
|
||||
]);
|
||||
|
||||
$this->entityManager->saveEntity($workflowLogRecord);
|
||||
|
||||
$alertObject = new stdClass();
|
||||
$variables = ['__alert' => $alertObject];
|
||||
|
||||
$this->workflowManager->runActions($workflow, $entity, $variables);
|
||||
|
||||
return $this->prepareTriggerResult($alertObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
*/
|
||||
private function processCheckManualWorkflowConditions(Workflow $workflow, CoreEntity $entity): void
|
||||
{
|
||||
$conditionGroup = $workflow->getManualDynamicLogicConditionGroup();
|
||||
|
||||
if (
|
||||
!$conditionGroup ||
|
||||
!class_exists("Espo\\Tools\\DynamicLogic\\ConditionCheckerFactory")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$conditionCheckerFactory = $this->injectableFactory->create(ConditionCheckerFactory::class);
|
||||
|
||||
$checker = $conditionCheckerFactory->create($entity);
|
||||
|
||||
try {
|
||||
$item = LogicItem::fromGroupDefinition($conditionGroup);
|
||||
|
||||
$isTrue = $checker->check($item);
|
||||
} catch (BadCondition $e) {
|
||||
throw new Error($e->getMessage(), 500, $e);
|
||||
}
|
||||
|
||||
if (!$isTrue) {
|
||||
throw new Forbidden("Workflow conditions are not met.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NotFound
|
||||
*/
|
||||
private function getEntityForManualWorkflow(Workflow $workflow, string $targetId): CoreEntity
|
||||
{
|
||||
$targetEntityType = $workflow->getTargetEntityType();
|
||||
|
||||
$entity = $this->entityManager->getRDBRepository($targetEntityType)->getById($targetId);
|
||||
|
||||
if (!$entity) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
$this->serviceContainer->get($targetEntityType)->loadAdditionalFields($entity);
|
||||
|
||||
if (!$entity instanceof CoreEntity) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
private function getManualWorkflow(string $id): Workflow
|
||||
{
|
||||
$workflow = $this->entityManager->getRDBRepositoryByClass(Workflow::class)->getById($id);
|
||||
|
||||
if (!$workflow) {
|
||||
throw new NotFound("Workflow $id not found.");
|
||||
}
|
||||
|
||||
if ($workflow->getType() !== Workflow::TYPE_MANUAL) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
return $workflow;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function processManualWorkflowAccess(Workflow $workflow, CoreEntity $entity): void
|
||||
{
|
||||
if ($this->user->isPortal()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$accessRequired = $workflow->getManualAccessRequired();
|
||||
|
||||
if ($accessRequired === Workflow::MANUAL_ACCESS_ADMIN) {
|
||||
if (!$this->user->isAdmin()) {
|
||||
throw new Forbidden("No admin access.");
|
||||
}
|
||||
} else if ($accessRequired === Workflow::MANUAL_ACCESS_READ) {
|
||||
if (!$this->acl->checkEntityRead($entity)) {
|
||||
throw new Forbidden("No read access.");
|
||||
}
|
||||
} else if (!$this->acl->checkEntityEdit($entity)) {
|
||||
throw new Forbidden("No edit access.");
|
||||
}
|
||||
|
||||
if (!$this->user->isAdmin()) {
|
||||
$teamIdList = $workflow->getLinkMultipleIdList('manualTeams');
|
||||
|
||||
if (array_intersect($teamIdList, $this->user->getTeamIdList()) === []) {
|
||||
throw new Forbidden("User is not from allowed team.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function prepareTriggerResult(stdClass $alertObject): TriggerResult
|
||||
{
|
||||
$alert = null;
|
||||
|
||||
if (property_exists($alertObject, 'message') && is_string($alertObject->message)) {
|
||||
$alert = new Alert(
|
||||
message: $alertObject->message,
|
||||
type: $alertObject->type ?? null,
|
||||
autoClose: $alertObject->autoClose ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
return new TriggerResult(
|
||||
alert: $alert,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Tools\Workflow;
|
||||
|
||||
class TriggerResult
|
||||
{
|
||||
public function __construct(
|
||||
public ?Alert $alert = null,
|
||||
) {}
|
||||
}
|
||||
Reference in New Issue
Block a user