Initial commit
This commit is contained in:
486
application/Espo/Tools/LeadCapture/FormService.php
Normal file
486
application/Espo/Tools/LeadCapture/FormService.php
Normal file
@@ -0,0 +1,486 @@
|
||||
<?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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user