. * * 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} * @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 */ 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> $languageData * @return array> */ 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 */ private function getDetailLayout(array $fieldList): array { $rows = []; foreach ($fieldList as $field) { $rows[] = [['name' => $field]]; } return [['rows' => $rows]]; } /** * @param string[] $fieldList * @return array */ 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 */ 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 */ private function getAppParams(): array { return [ 'addressCountryData' => $this->countryDataProvider->get(), ]; } /** * @param array> $fieldDefs * @param array> $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 $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 $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 */ 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 */ private function getFromCache(string $cacheKey): array { /** @var array */ return $this->dataCache->get($cacheKey); } /** * @return array */ 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()); } }