Files
espocrm/application/Espo/Tools/LeadCapture/FormService.php
2026-01-19 17:46:06 +01:00

487 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU 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\Tools\LeadCapture;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\ORM\Type\FieldType;
use Espo\Core\Utils\Address\CountryDataProvider;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\DataCache;
use Espo\Core\Utils\Language;
use Espo\Core\Utils\Metadata;
use Espo\Core\Utils\Theme\MetadataProvider as ThemeMetadataProvider;
use Espo\Core\Utils\ThemeManager;
use Espo\Entities\Integration;
use Espo\Entities\LeadCapture;
use Espo\Modules\Crm\Entities\Lead;
use Espo\ORM\Defs\EntityDefs;
use Espo\ORM\EntityManager;
use Espo\Tools\App\LanguageService;
use RuntimeException;
class FormService
{
private const CACHE_KEY_PREFIX = 'leadCaptureForm';
public function __construct(
private EntityManager $entityManager,
private Config $config,
private Metadata $metadata,
private Language $defaultLanguage,
private CountryDataProvider $countryDataProvider,
private LanguageService $languageService,
private Language\LanguageFactory $languageFactory,
private DataCache $dataCache,
private ThemeManager $themeManager,
private Config\SystemConfig $systemConfig,
private ThemeMetadataProvider $themeMetadataProvider,
) {}
/**
* @return array{LeadCapture, array<string, mixed>, ?string}
* @throws NotFound
*/
public function getData(string $id): array
{
$leadCapture = $this->getLeadCapture($id);
$captchaKey = $this->getCaptchaKey($leadCapture);
$captchaScript = $this->getCaptchaScript($captchaKey);
$data = $this->getDataInternal($leadCapture);
$data['captchaKey'] = $captchaKey;
return [$leadCapture, $data, $captchaScript];
}
/**
* @return array<string, mixed>
*/
private function getDataInternal(LeadCapture $leadCapture): array
{
$cacheKey = $this->getCacheKey($leadCapture);
if ($this->systemConfig->useCache() && $this->dataCache->has($cacheKey)) {
return $this->getFromCache($cacheKey);
}
$data = $this->prepareData($leadCapture);
$this->dataCache->store($cacheKey, $data);
return $data;
}
private function getRequestUrl(LeadCapture $leadCapture): string
{
$formId = $leadCapture->getFormId();
if (!$formId) {
throw new RuntimeException("No API key.");
}
return "LeadCapture/form/$formId";
}
/**
* @return string[]
*/
private function getFieldList(LeadCapture $leadCapture): array
{
/** @var string[] $allowedTypeList */
$allowedTypeList = $this->metadata->get("entityDefs.LeadCapture.fields.fieldList.webFormFieldTypeList") ?? [];
$entityDefs = $this->entityManager->getDefs()->getEntity(Lead::ENTITY_TYPE);
$fieldList = [];
foreach ($leadCapture->getFieldList() as $field) {
if (!$entityDefs->hasField($field)) {
continue;
}
$itemDefs = $entityDefs->getField($field);
if (!in_array($itemDefs->getType(), $allowedTypeList)) {
continue;
}
$fieldList[] = $field;
}
return $fieldList;
}
/**
* @param string[] $fieldList
* @param array<string, array<string, mixed>> $languageData
* @return array<string, array<string, mixed>>
*/
private function getFieldDefs(
array $fieldList,
LeadCapture $leadCapture,
array &$languageData,
Language $language
): array {
$entityDefs = $this->entityManager->getDefs()->getEntity(Lead::ENTITY_TYPE);
$fieldDefs = [];
foreach ($fieldList as $field) {
$fieldDefs[$field] = $this->metadata->get("entityDefs.Lead.fields.$field");
if (!$fieldDefs[$field]) {
continue;
}
$this->applyFieldDefsItem(
$leadCapture,
$entityDefs,
$field,
$fieldDefs,
$languageData,
$language
);
}
return $fieldDefs;
}
/**
* @param string[] $fieldList
* @return array<int, mixed>
*/
private function getDetailLayout(array $fieldList): array
{
$rows = [];
foreach ($fieldList as $field) {
$rows[] = [['name' => $field]];
}
return [['rows' => $rows]];
}
/**
* @param string[] $fieldList
* @return array<string, mixed>
*/
private function getMetadataFields(array $fieldList): array
{
$metadataFields = [];
$entityDefs = $this->entityManager->getDefs()->getEntity(Lead::ENTITY_TYPE);
foreach ($fieldList as $field) {
$type = $entityDefs->getField($field)->getType();
if (array_key_exists($type, $metadataFields)) {
continue;
}
$metadataFields[$type] = $this->metadata->get("fields.$type");
}
return $metadataFields;
}
/**
* @return array<string, mixed>
*/
private function getConfig(): array
{
$params = [
'decimalMark',
'thousandSeparator',
'phoneNumberInternational',
'phoneNumberExtensions',
'phoneNumberPreferredCountryList',
'defaultCurrency',
'currencyList',
'currencyDecimalPlaces',
'addressFormat',
'dateFormat',
'timeFormat',
'timeZone',
'weekStart',
];
$data = [];
foreach ($params as $param) {
$data[$param] = $this->config->get($param);
}
return $data;
}
/**
* @return array<string, mixed>
*/
private function getAppParams(): array
{
return [
'addressCountryData' => $this->countryDataProvider->get(),
];
}
/**
* @param array<string, array<string, mixed>> $fieldDefs
* @param array<string, array<string, mixed>> $languageData
*/
private function applyFieldDefsItem(
LeadCapture $leadCapture,
EntityDefs $entityDefs,
string $field,
array &$fieldDefs,
array &$languageData,
Language $language,
): void {
$fieldDefs[$field]['required'] = $leadCapture->isFieldRequired($field);
$itDefs = $entityDefs->getField($field);
$type = $itDefs->getType();
if ($type === FieldType::ADDRESS) {
$subList = [
$field . 'Street',
$field . 'Country',
$field . 'State',
$field . 'PostalCode',
$field . 'City',
];
foreach ($subList as $sub) {
/** @var array<string, mixed> $subItem */
$subItem = $this->metadata->get("entityDefs.Lead.fields.$sub");
$fieldDefs[$sub] = $subItem;
}
}
if ($type === FieldType::PERSON_NAME) {
$subList = [
'first' . ucfirst($field),
'middle' . ucfirst($field),
'last' . ucfirst($field),
'salutation' . ucfirst($field),
];
foreach ($subList as $sub) {
/** @var array<string, mixed> $subItem */
$subItem = $this->metadata->get("entityDefs.Lead.fields.$sub");
$fieldDefs[$sub] = $subItem;
}
}
if ($type === FieldType::EMAIL) {
if ($leadCapture->optInConfirmation()) {
$fieldDefs[$field]['required'] = true;
}
$fieldDefs[$field]['onlyPrimary'] = true;
}
if ($type === FieldType::PHONE) {
$fieldDefs[$field]['onlyPrimary'] = true;
}
if (
in_array($type, [
FieldType::ENUM,
FieldType::MULTI_ENUM,
FieldType::ARRAY,
FieldType::CHECKLIST,
])
) {
$reference = $itDefs->getParam('optionsReference');
if ($reference) {
[$refEntityType, $refField] = explode('.', $reference);
$options = $this->entityManager
->getDefs()
->tryGetEntity($refEntityType)
?->tryGetField($refField)
?->getParam('options');
$fieldDefs[$field]['options'] = $options;
unset($fieldDefs[$field]['optionsReference']);
$languageData[Lead::ENTITY_TYPE] ??= [];
$languageData[Lead::ENTITY_TYPE]['options'] ??= [];
$languageData[Lead::ENTITY_TYPE]['options'][$field] =
$language->get("$refEntityType.options.$refField");
}
}
}
/**
* @return array<string, mixed>
*/
private function getLanguageData(Language $language): array
{
$data = $this->languageService->getDataForFrontendFromLanguage($language);
$data[Lead::ENTITY_TYPE] = $language->get(Lead::ENTITY_TYPE);
return $data;
}
/**
* @throws NotFound
*/
public function getLeadCapture(string $id): LeadCapture
{
$leadCapture = $this->entityManager
->getRDBRepositoryByClass(LeadCapture::class)
->where(['formId' => $id])
->findOne();
if (!$leadCapture || !$leadCapture->hasFormEnabled() || !$leadCapture->isActive()) {
throw new NotFound();
}
return $leadCapture;
}
private function getSuccessText(LeadCapture $leadCapture): string
{
return $leadCapture->getFormSuccessText() ?? $this->defaultLanguage->translateLabel('Posted');
}
private function getCacheKey(LeadCapture $leadCapture): string
{
return self::CACHE_KEY_PREFIX . '/' . $leadCapture->getId();
}
/**
* @return array<string, mixed>
*/
private function getFromCache(string $cacheKey): array
{
/** @var array<string, mixed> */
return $this->dataCache->get($cacheKey);
}
/**
* @return array<string, mixed>
*/
private function prepareData(LeadCapture $leadCapture): array
{
$language = $this->getLanguage($leadCapture);
$languageData = $this->getLanguageData($language);
$fieldList = $this->getFieldList($leadCapture);
$fieldDefs = $this->getFieldDefs($fieldList, $leadCapture, $languageData, $language);
$detailLayout = $this->getDetailLayout($fieldList);
$metadataFields = $this->getMetadataFields($fieldList);
$successText = $this->getSuccessText($leadCapture);
$text = $leadCapture->getFormText();
$config = $this->getConfig();
$appParams = $this->getAppParams();
return [
'requestUrl' => $this->getRequestUrl($leadCapture),
'fieldDefs' => (object) $fieldDefs,
'metadata' => [
'fields' => (object) $metadataFields,
'app' => [
'regExpPatterns' => $this->metadata->get("app.regExpPatterns"),
],
],
'isDark' => $this->isDark($leadCapture),
'detailLayout' => $detailLayout,
'language' => $languageData,
'successText' => $successText,
'text' => $text,
'title' => $leadCapture->getFormTitle(),
'config' => (object) $config,
'appParams' => (object) $appParams,
];
}
private function getLanguage(LeadCapture $leadCapture): Language
{
$language = $this->defaultLanguage;
if ($leadCapture->getFormLanguage()) {
$language = $this->languageFactory->create($leadCapture->getFormLanguage());
}
return $language;
}
private function getCaptchaKey(LeadCapture $leadCapture): ?string
{
if (!$leadCapture->hasFormCaptcha()) {
return null;
}
$entity = $this->entityManager
->getRepositoryByClass(Integration::class)
->getById('GoogleReCaptcha');
if (!$entity) {
return null;
}
$siteKey = $entity->get('siteKey');
if (!$siteKey) {
return null;
}
return $siteKey;
}
private function getCaptchaScript(?string $siteKey): ?string
{
if (!$siteKey) {
return null;
}
return 'https://www.google.com/recaptcha/api.js?render=' . $siteKey;
}
private function isDark(LeadCapture $leadCapture): bool
{
if (!$leadCapture->getFormTheme()) {
return $this->themeManager->isDark();
}
return $this->themeMetadataProvider->isDark($leadCapture->getFormTheme());
}
}