![]() Server : Apache System : Linux server2.corals.io 4.18.0-348.2.1.el8_5.x86_64 #1 SMP Mon Nov 15 09:17:08 EST 2021 x86_64 User : corals ( 1002) PHP Version : 7.4.33 Disable Function : exec,passthru,shell_exec,system Directory : /home/corals/mautic.corals.io/app/bundles/FormBundle/Model/ |
<?php namespace Mautic\FormBundle\Model; use Doctrine\ORM\EntityManager; use Doctrine\ORM\ORMException; use Mautic\CampaignBundle\Entity\Campaign; use Mautic\CampaignBundle\Membership\MembershipManager; use Mautic\CampaignBundle\Model\CampaignModel; use Mautic\CoreBundle\Exception\FileUploadException; use Mautic\CoreBundle\Helper\Chart\ChartQuery; use Mautic\CoreBundle\Helper\Chart\LineChart; use Mautic\CoreBundle\Helper\CoreParametersHelper; use Mautic\CoreBundle\Helper\DateTimeHelper; use Mautic\CoreBundle\Helper\InputHelper; use Mautic\CoreBundle\Helper\IpLookupHelper; use Mautic\CoreBundle\Helper\UserHelper; use Mautic\CoreBundle\Model\FormModel as CommonFormModel; use Mautic\CoreBundle\Security\Permissions\CorePermissions; use Mautic\CoreBundle\Translation\Translator; use Mautic\CoreBundle\Twig\Helper\DateHelper; use Mautic\FormBundle\Crate\UploadFileCrate; use Mautic\FormBundle\Entity\Action; use Mautic\FormBundle\Entity\Field; use Mautic\FormBundle\Entity\Form; use Mautic\FormBundle\Entity\Submission; use Mautic\FormBundle\Entity\SubmissionRepository; use Mautic\FormBundle\Event\Service\FieldValueTransformer; use Mautic\FormBundle\Event\SubmissionEvent; use Mautic\FormBundle\Event\ValidationEvent; use Mautic\FormBundle\Exception\FileValidationException; use Mautic\FormBundle\Exception\NoFileGivenException; use Mautic\FormBundle\Exception\ValidationException; use Mautic\FormBundle\FormEvents; use Mautic\FormBundle\Helper\FormFieldHelper; use Mautic\FormBundle\Helper\FormUploader; use Mautic\FormBundle\ProgressiveProfiling\DisplayManager; use Mautic\FormBundle\Validator\UploadFieldValidator; use Mautic\LeadBundle\DataObject\LeadManipulator; use Mautic\LeadBundle\Deduplicate\ContactMerger; use Mautic\LeadBundle\Deduplicate\Exception\SameContactException; use Mautic\LeadBundle\Entity\Company; use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Helper\CustomFieldValueHelper; use Mautic\LeadBundle\Helper\IdentifyCompanyHelper; use Mautic\LeadBundle\Model\CompanyModel; use Mautic\LeadBundle\Model\FieldModel as LeadFieldModel; use Mautic\LeadBundle\Model\LeadModel; use Mautic\LeadBundle\Tracker\ContactTracker; use Mautic\LeadBundle\Tracker\Service\DeviceTrackingService\DeviceTrackingServiceInterface; use Mautic\PageBundle\Model\PageModel; use PhpOffice\PhpSpreadsheet\IOFactory; use PhpOffice\PhpSpreadsheet\Spreadsheet; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Twig\Environment; /** * @extends CommonFormModel<Submission> */ class SubmissionModel extends CommonFormModel { public function __construct( protected IpLookupHelper $ipLookupHelper, protected Environment $twig, protected FormModel $formModel, protected PageModel $pageModel, protected LeadModel $leadModel, protected CampaignModel $campaignModel, protected MembershipManager $membershipManager, protected LeadFieldModel $leadFieldModel, protected CompanyModel $companyModel, protected FormFieldHelper $fieldHelper, private UploadFieldValidator $uploadFieldValidator, private FormUploader $formUploader, private DeviceTrackingServiceInterface $deviceTrackingService, private FieldValueTransformer $fieldValueTransformer, private DateHelper $dateHelper, private ContactTracker $contactTracker, private ContactMerger $contactMerger, EntityManager $em, CorePermissions $security, EventDispatcherInterface $dispatcher, UrlGeneratorInterface $router, Translator $translator, UserHelper $userHelper, LoggerInterface $mauticLogger, CoreParametersHelper $coreParametersHelper ) { parent::__construct($em, $security, $dispatcher, $router, $translator, $userHelper, $mauticLogger, $coreParametersHelper); } public function getRepository(): SubmissionRepository { return $this->em->getRepository(Submission::class); } /** * @param bool $returnEvent * * @return bool|array * * @throws ORMException */ public function saveSubmission($post, $server, Form $form, Request $request, $returnEvent = false) { $leadFields = $this->leadFieldModel->getFieldListWithProperties(false); // everything matches up so let's save the results $submission = new Submission(); $submission->setDateSubmitted(new \DateTime()); $submission->setForm($form); // set the landing page the form was submitted from if applicable if (!empty($post['mauticpage'])) { $page = $this->pageModel->getEntity((int) $post['mauticpage']); if (null != $page) { $submission->setPage($page); } } $ipAddress = $this->ipLookupHelper->getIpAddress(); $submission->setIpAddress($ipAddress); if (!empty($post['return'])) { $referer = $post['return']; } elseif (!empty($server['HTTP_REFERER'])) { $referer = $server['HTTP_REFERER']; } else { $referer = ''; } // clean the referer by removing mauticError and mauticMessage $referer = InputHelper::url($referer, null, null, ['mauticError', 'mauticMessage']); $submission->setReferer($referer); // Create an event to be dispatched through the processes $submissionEvent = new SubmissionEvent($submission, $post, $server, $request); // Get a list of components to build custom fields from $components = $this->formModel->getCustomComponents(); $fields = $form->getFields(); $fieldArray = []; $results = []; $tokens = []; $leadFieldMatches = []; $validationErrors = []; $filesToUpload = new UploadFileCrate(); /** @var Field $f */ foreach ($fields as $f) { $id = $f->getId(); $type = $f->getType(); $alias = $f->getAlias(); $value = $post[$alias] ?? ''; $fieldArray[$id] = [ 'id' => $id, 'type' => $type, 'alias' => $alias, ]; if ($f->isCaptchaType()) { $captcha = $this->fieldHelper->validateFieldValue($type, $value, $f); if (!empty($captcha)) { $props = $f->getProperties(); // check for a custom message $validationErrors[$alias] = (!empty($props['errorMessage'])) ? $props['errorMessage'] : implode('<br />', $captcha); } continue; } elseif ($f->isFileType()) { try { $file = $this->uploadFieldValidator->processFileValidation($f, $request); $value = $file->getClientOriginalName(); $filesToUpload->addFile($file, $f); } catch (NoFileGivenException) { // No error here, we just move to another validation, eg. if a field is required } catch (FileValidationException $e) { $validationErrors[$alias] = $e->getMessage(); } } if (!$f->showForConditionalField($post)) { continue; } if ('' === $value && $f->isRequired()) { // field is required, but hidden from form because of 'ShowWhenValueExists' if (false === $f->getShowWhenValueExists() && !isset($post[$alias])) { continue; } // somehow the user got passed the JS validation $msg = $f->getValidationMessage(); if (empty($msg)) { $msg = $this->translator->trans( 'mautic.form.field.generic.validationfailed', [ '%label%' => $f->getLabel(), ], 'validators' ); } $validationErrors[$alias] = $msg; continue; } if (isset($components['viewOnlyFields']) && in_array($type, $components['viewOnlyFields'])) { // don't save items that don't have a value associated with it continue; } // clean and validate the input if ($f->isCustom()) { if (!isset($components['fields'][$f->getType()])) { continue; } $params = $components['fields'][$f->getType()]; if (!empty($value)) { if (isset($params['valueFilter'])) { if (is_string($params['valueFilter']) && is_callable([InputHelper::class, $params['valueFilter']])) { $value = InputHelper::_($value, $params['valueFilter']); } elseif (is_callable($params['valueFilter'])) { $value = call_user_func_array($params['valueFilter'], [$f, $value]); } else { $value = InputHelper::_($value, 'clean'); } } else { $value = InputHelper::_($value, 'clean'); } } } elseif (!empty($value)) { $filter = $this->fieldHelper->getFieldFilter($type); $value = InputHelper::_($value, $filter); $isValid = $this->validateFieldValue($f, $value); if (true !== $isValid) { $validationErrors[$alias] = is_array($isValid) ? implode('<br />', $isValid) : $isValid; } } // Check for custom validators $isValid = $this->validateFieldValue($f, $value); if (true !== $isValid) { $validationErrors[$alias] = $isValid; } $mappedField = $f->getMappedField(); if (!empty($mappedField) && in_array($f->getMappedObject(), ['company', 'contact'])) { $leadValue = $value; $leadFieldMatches[$mappedField] = $leadValue; } $tokens["{formfield={$alias}}"] = $this->normalizeValue($value, $f); // convert array from checkbox groups and multiple selects if (is_array($value)) { $value = implode(', ', $value); } // save the result if (false !== $f->getSaveResult()) { $results[$alias] = $value; } } // Set the results $submission->setResults($results); // Update the event $submissionEvent->setFields($fieldArray) ->setTokens($tokens) ->setResults($results) ->setContactFieldMatches($leadFieldMatches); $lead = $this->contactTracker->getContact(); // Remove validation errors if the field is not visible if ($lead && $form->usesProgressiveProfiling()) { $leadSubmissions = $this->formModel->getLeadSubmissions($form, $lead->getId()); $displayManager = new DisplayManager($form, $this->formModel->getCustomComponents()['viewOnlyFields']); foreach ($fields as $field) { if ($field->showForContact($leadSubmissions, $lead, $form, $displayManager)) { $displayManager->increaseDisplayedFields($field); } elseif (isset($validationErrors[$field->getAlias()])) { unset($validationErrors[$field->getAlias()]); } } } // return errors if there any if (!empty($validationErrors)) { return ['errors' => $validationErrors]; } // Create/update lead if (!empty($leadFieldMatches)) { $lead = $this->createLeadFromSubmit($form, $leadFieldMatches, $leadFields); } $trackedDevice = $this->deviceTrackingService->getTrackedDevice(); $trackingId = (null === $trackedDevice ? null : $trackedDevice->getTrackingId()); // set tracking ID for stats purposes to determine unique hits $submission->setTrackingId($trackingId) ->setLead($lead); /* * Process File upload and save the result to the entity * Upload is here to minimize a need for deleting file if there is a validation error * The action can still be invalidated below - deleteEntity takes care for File deletion * * @todo Refactor form validation to execute this code only if Submission is valid */ try { $this->formUploader->uploadFiles($filesToUpload, $submission); } catch (FileUploadException $e) { $msg = $this->translator->trans('mautic.form.submission.error.file.uploadFailed', [], 'validators'); $validationErrors[$e->getMessage()] = $msg; return ['errors' => $validationErrors]; } // set results after uploader what can change file name if file name exists $submissionEvent->setResults($submission->getResults()); // Save the submission $this->saveEntity($submission); $this->fieldValueTransformer->transformValuesAfterSubmit($submissionEvent); // Now handle post submission actions try { $this->executeFormActions($submissionEvent); } catch (ValidationException $exception) { // The action invalidated the form for whatever reason $this->deleteEntity($submission); if ($validationErrors = $exception->getViolations()) { return ['errors' => $validationErrors]; } return ['errors' => [$exception->getMessage()]]; } // update contact fields with transform values if (!empty($this->fieldValueTransformer->getContactFieldsToUpdate())) { $this->leadModel->setFieldValues($lead, $this->fieldValueTransformer->getContactFieldsToUpdate()); $this->leadModel->saveEntity($lead, false); } if (!$form->isStandalone()) { // Find and add the lead to the associated campaigns $campaigns = $this->campaignModel->getCampaignsByForm($form); /** @var Campaign $campaign */ foreach ($campaigns as $campaign) { if ($campaign->isPublished()) { $this->membershipManager->addContact($lead, $campaign); } } } if ($this->dispatcher->hasListeners(FormEvents::FORM_ON_SUBMIT)) { // Reset action config from executeFormActions() $submissionEvent->setAction(null); // Dispatch to on submit listeners $this->dispatcher->dispatch($submissionEvent, FormEvents::FORM_ON_SUBMIT); } // get callback commands from the submit action if ($submissionEvent->hasPostSubmitCallbacks()) { return ['callback' => $submissionEvent]; } // made it to the end so return the submission event to give the calling method access to tokens, results, etc // otherwise return false that no errors were encountered (to keep BC really) return ($returnEvent) ? ['submission' => $submissionEvent] : false; } /** * @param Submission $submission */ public function deleteEntity($submission): void { $this->formUploader->deleteUploadedFiles($submission); parent::deleteEntity($submission); } public function getEntities(array $args = []) { return $this->getRepository()->getEntities($args); } /** * @param array<string, mixed> $args * * @return array<mixed> */ public function getEntitiesByPage(array $args = []): array { return $this->getRepository()->getEntitiesByPage($args); } /** * @return StreamedResponse|Response * * @throws \Exception */ public function exportResults($format, $form, $queryArgs) { $viewOnlyFields = $this->formModel->getCustomComponents()['viewOnlyFields']; $queryArgs['viewOnlyFields'] = $viewOnlyFields; $queryArgs['simpleResults'] = true; $results = $this->getEntities($queryArgs); $date = (new DateTimeHelper())->toLocalString(); $name = str_replace(' ', '_', $date).'_'.$form->getAlias(); switch ($format) { case 'csv': $response = new StreamedResponse( function () use ($results, $form, $viewOnlyFields): void { $handle = fopen('php://output', 'r+'); // build the header row $header = $this->getExportHeader($form, $viewOnlyFields); // write the row $this->putCsvExportRow($handle, $header); // build the data rows foreach ($results as $k => $s) { $row = $this->getExportRow($s, $viewOnlyFields); $this->putCsvExportRow($handle, $row); // free memory unset($row, $results[$k]); } fclose($handle); } ); $this->setResponseHeaders($response, $name.'.csv', [ 'application/force-download', 'application/octet-stream', ]); return $response; case 'html': $content = $this->twig->render( '@MauticForm/Result/export.html.twig', [ 'form' => $form, 'results' => $results, 'pageTitle' => $name, 'viewOnlyFields' => $viewOnlyFields, ] ); return new Response($content); case 'xlsx': if (class_exists(Spreadsheet::class)) { $response = new StreamedResponse( function () use ($results, $form, $name, $viewOnlyFields): void { $objPHPExcel = new Spreadsheet(); $objPHPExcel->getProperties()->setTitle($name); $objPHPExcel->createSheet(); // build the header row $header = $this->getExportHeader($form, $viewOnlyFields); // write the row $objPHPExcel->getActiveSheet()->fromArray($header, null, 'A1'); // build the data rows $count = 2; foreach ($results as $k => $s) { $row = $this->getExportRow($s, $viewOnlyFields); $objPHPExcel->getActiveSheet()->fromArray($row, null, "A{$count}"); // free memory unset($row, $results[$k]); // increment letter ++$count; } $objWriter = IOFactory::createWriter($objPHPExcel, 'Xlsx'); $objWriter->setPreCalculateFormulas(false); $objWriter->save('php://output'); } ); $this->setResponseHeaders($response, $name.'.xlsx', [ 'application/force-download', 'application/octet-stream', ]); return $response; } throw new \Exception('PHPSpreadsheet is required to export to Excel spreadsheets'); default: return new Response(); } } /** * @param string $format * @param object $page * @param array<string, mixed> $queryArgs * * @return StreamedResponse|Response * * @throws \Exception */ public function exportResultsForPage($format, $page, $queryArgs) { $results = $this->getEntitiesByPage($queryArgs); $results = $results['results']; $date = (new DateTimeHelper())->toLocalString(); $name = str_replace(' ', '_', $date).'_'.$page->getAlias(); switch ($format) { case 'csv': $response = new StreamedResponse( function () use ($results): void { $handle = fopen('php://output', 'r+'); // build the header row $header = $this->getExportHeaderForPage(); $this->putCsvExportRow($handle, $header); // build the data rows foreach ($results as $k => $s) { $row = $this->getExportRowForPage($s); $this->putCsvExportRow($handle, $row); // free memory unset($row, $results[$k]); } fclose($handle); } ); $this->setResponseHeaders($response, $name.'.csv', [ 'text/csv; charset=UTF-8', ]); return $response; case 'html': $content = $this->twig->render( '@MauticPage/Result/export.html.twig', [ 'page' => $page, 'results' => $results, 'pageTitle' => $name, ] ); return new Response($content); case 'xlsx': if (!class_exists(Spreadsheet::class)) { throw new \Exception('PHPSpreadsheet is required to export to Excel spreadsheets'); } $response = new StreamedResponse( function () use ($results, $name): void { $objPHPExcel = new Spreadsheet(); $objPHPExcel->getProperties()->setTitle($name); $objPHPExcel->createSheet(); $header = $this->getExportHeaderForPage('xlsx'); // write the row $objPHPExcel->getActiveSheet()->fromArray($header, null, 'A1'); // build the data rows $count = 2; foreach ($results as $k => $s) { $row = $this->getExportRowForPage($s, 'xlsx'); $objPHPExcel->getActiveSheet()->fromArray($row, null, "A{$count}"); // free memory unset($row, $results[$k]); // increment letter ++$count; } $objWriter = IOFactory::createWriter($objPHPExcel, 'Xlsx'); $objWriter->setPreCalculateFormulas(false); $objWriter->save('php://output'); } ); $this->setResponseHeaders($response, $name.'.xlsx', [ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ]); return $response; default: return new Response(); } } /** * @param array<string> $contentType */ private function setResponseHeaders(StreamedResponse $response, string $filename, array $contentType): void { foreach ($contentType as $ct) { $response->headers->set('Content-Type', $ct); } $response->headers->set('Content-Disposition', 'attachment; filename="'.$filename.'"'); $response->headers->set('Expires', '0'); $response->headers->set('Cache-Control', 'must-revalidate'); $response->headers->set('Pragma', 'public'); } /** * @param resource $handle * @param array<mixed> $row */ private function putCsvExportRow($handle, array $row): bool|int { return fputcsv($handle, $row); } /** * @param array<mixed> $values * * @return array<mixed> */ private function getExportRowForPage(array $values, string $format = 'csv'): array { $row = [ $values['id'], $values['leadId'], $this->dateHelper->toFull($values['dateSubmitted'], 'UTC'), $values['ipAddress'], $values['referer'], ]; if ('csv' === $format) { array_splice($row, 2, 0, $values['formId']); } return $row; } /** * @param array<mixed> $values * @param array<mixed> $viewOnlyFields * * @return array<mixed> */ private function getExportRow(array $values, array $viewOnlyFields = []): array { $row = [ $values['id'], $values['leadId'], $this->dateHelper->toFull($values['dateSubmitted'], 'UTC'), $values['ipAddress'], $values['referer'], ]; foreach ($values['results'] as $k2 => $r) { if (in_array($r['type'], $viewOnlyFields)) { continue; } $row[] = htmlspecialchars_decode($r['value'], ENT_QUOTES); // free memory unset($values['results'][$k2]); } return $row; } /** * @return array<string> */ private function getExportHeaderForPage(string $format = 'csv'): array { $header = [ $this->translator->trans('mautic.form.report.submission.id'), $this->translator->trans('mautic.lead.report.contact_id'), $this->translator->trans('mautic.form.result.thead.date'), $this->translator->trans('mautic.core.ipaddress'), $this->translator->trans('mautic.form.result.thead.referrer'), ]; if ('csv' === $format) { array_splice($header, 2, 0, $this->translator->trans('mautic.form.report.form_id')); } return $header; } /** * @param array<mixed> $viewOnlyFields * * @return array<string> */ private function getExportHeader(Form $form, $viewOnlyFields): array { $fields = $form->getFields(); $header = [ $this->translator->trans('mautic.form.report.submission.id'), $this->translator->trans('mautic.lead.report.contact_id'), $this->translator->trans('mautic.form.result.thead.date'), $this->translator->trans('mautic.core.ipaddress'), $this->translator->trans('mautic.form.result.thead.referrer'), ]; foreach ($fields as $f) { if (in_array($f->getType(), $viewOnlyFields) || false === $f->getSaveResult()) { continue; } $header[] = $f->getLabel(); } return $header; } /** * Get line chart data of submissions. * * @param string|null $unit {@link php.net/manual/en/function.date.php#refsect1-function.date-parameters} * @param string $dateFormat * @param array $filter * @param bool $canViewOthers */ public function getSubmissionsLineChartData( ?string $unit, \DateTime $dateFrom, \DateTime $dateTo, $dateFormat = null, $filter = [], $canViewOthers = true ): array { $chart = new LineChart($unit, $dateFrom, $dateTo, $dateFormat); $query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo); $q = $query->prepareTimeDataQuery('form_submissions', 'date_submitted', $filter); if (!$canViewOthers) { $q->join('t', MAUTIC_TABLE_PREFIX.'forms', 'f', 'f.id = t.form_id') ->andWhere('f.created_by = :userId') ->setParameter('userId', $this->userHelper->getUser()->getId()); } $data = $query->loadAndBuildTimeData($q); $chart->setDataset($this->translator->trans('mautic.form.submission.count'), $data); return $chart->render(); } /** * Get a list of top submission referrers. * * @param int $limit * @param string $dateFrom * @param string $dateTo * @param array $filters * @param bool $canViewOthers * * @return array */ public function getTopSubmissionReferrers($limit = 10, $dateFrom = null, $dateTo = null, $filters = [], $canViewOthers = true) { $q = $this->em->getConnection()->createQueryBuilder(); $q->select('COUNT(DISTINCT t.id) AS submissions, t.referer') ->from(MAUTIC_TABLE_PREFIX.'form_submissions', 't') ->orderBy('submissions', 'DESC') ->groupBy('t.referer') ->setMaxResults($limit); if (!$canViewOthers) { $q->join('t', MAUTIC_TABLE_PREFIX.'forms', 'f', 'f.id = t.form_id') ->andWhere('f.created_by = :userId') ->setParameter('userId', $this->userHelper->getUser()->getId()); } $chartQuery = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo); $chartQuery->applyFilters($q, $filters); $chartQuery->applyDateFilters($q, 'date_submitted'); return $q->execute()->fetchAllAssociative(); } /** * Get a list of the most submisions per lead. * * @param int $limit * @param string $dateFrom * @param string $dateTo * @param array $filters * @param bool $canViewOthers * * @return array */ public function getTopSubmitters($limit = 10, $dateFrom = null, $dateTo = null, $filters = [], $canViewOthers = true) { $q = $this->em->getConnection()->createQueryBuilder(); $q->select('COUNT(DISTINCT t.id) AS submissions, t.lead_id, l.firstname, l.lastname, l.email') ->from(MAUTIC_TABLE_PREFIX.'form_submissions', 't') ->join('t', MAUTIC_TABLE_PREFIX.'leads', 'l', 'l.id = t.lead_id') ->orderBy('submissions', 'DESC') ->groupBy('t.lead_id, l.firstname, l.lastname, l.email') ->setMaxResults($limit); if (!$canViewOthers) { $q->join('t', MAUTIC_TABLE_PREFIX.'forms', 'f', 'f.id = t.form_id') ->andWhere('f.created_by = :userId') ->setParameter('userId', $this->userHelper->getUser()->getId()); } $chartQuery = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo); $chartQuery->applyFilters($q, $filters); $chartQuery->applyDateFilters($q, 'date_submitted'); return $q->execute()->fetchAllAssociative(); } /** * Execute a form submit action. * * @throws ValidationException */ protected function executeFormActions(SubmissionEvent $event): void { $actions = $event->getSubmission()->getForm()->getActions(); $customComponents = $this->formModel->getCustomComponents(); $availableActions = $customComponents['actions'] ?? []; $actions->filter(fn (Action $action): bool => array_key_exists($action->getType(), $availableActions))->map(function (Action $action) use ($event, $availableActions): void { $event->setAction($action); $this->dispatcher->dispatch($event, $availableActions[$action->getType()]['eventName']); }); } /** * Create/update lead from form submit. * * @throws ORMException */ protected function createLeadFromSubmit(Form $form, array $leadFieldMatches, $leadFields): Lead { // set the mapped data $inKioskMode = $form->isInKioskMode(); $leadId = null; $lead = new Lead(); $currentFields = $leadFieldMatches; $companyFields = $this->leadFieldModel->getFieldListWithProperties('company'); if (!$inKioskMode) { // Default to currently tracked lead if ($currentLead = $this->contactTracker->getContact()) { $lead = $currentLead; $leadId = $lead->getId(); $currentFields = $lead->getProfileFields(); } $this->logger->debug('FORM: Not in kiosk mode so using current contact ID #'.$leadId); } else { // Default to a new lead in kiosk mode $lead->setNewlyCreated(true); $this->logger->debug('FORM: In kiosk mode so assuming a new contact'); } $uniqueLeadFields = $this->leadFieldModel->getUniqueIdentifierFields(); // Closure to get data and unique fields $getData = function ($currentFields, $uniqueOnly = false) use ($leadFields, $uniqueLeadFields): array { $uniqueFieldsWithData = $data = []; foreach ($leadFields as $alias => $properties) { if (isset($currentFields[$alias])) { $value = $currentFields[$alias]; $data[$alias] = $value; // make sure the value is actually there and the field is one of our uniques if (!empty($value) && array_key_exists($alias, $uniqueLeadFields)) { $uniqueFieldsWithData[$alias] = $value; } } } return ($uniqueOnly) ? $uniqueFieldsWithData : [$data, $uniqueFieldsWithData]; }; // Closure to get data and unique fields $getCompanyData = function ($currentFields) use ($companyFields): array { $companyData = []; // force add company contact field to company fields check $companyFields = array_merge($companyFields, ['company'=> 'company']); foreach ($companyFields as $alias => $properties) { if (isset($currentFields[$alias])) { $value = $currentFields[$alias]; $companyData[$alias] = $value; } } return $companyData; }; // Closure to help search for a conflict $checkForIdentifierConflict = function ($fieldSet1, $fieldSet2): array { // Find fields in both sets $potentialConflicts = array_keys( array_intersect_key($fieldSet1, $fieldSet2) ); $this->logger->debug( 'FORM: Potential conflicts '.implode(', ', array_keys($potentialConflicts)).' = '.implode(', ', $potentialConflicts) ); $conflicts = []; foreach ($potentialConflicts as $field) { if (!empty($fieldSet1[$field]) && !empty($fieldSet2[$field])) { if (strtolower($fieldSet1[$field]) !== strtolower($fieldSet2[$field])) { $conflicts[] = $field; } } } return [count($conflicts), $conflicts]; }; // Get data for the form submission [$data, $uniqueFieldsWithData] = $getData($leadFieldMatches); $this->logger->debug('FORM: Unique fields submitted include '.implode(', ', $uniqueFieldsWithData)); // Check for duplicate lead /** @var \Mautic\LeadBundle\Entity\Lead[] $leads */ $leads = (!empty($uniqueFieldsWithData)) ? $this->em->getRepository(Lead::class)->getLeadsByUniqueFields( $uniqueFieldsWithData, $leadId ) : []; $uniqueFieldsCurrent = $getData($currentFields, true); if (count($leads)) { $this->logger->debug(count($leads).' found based on unique identifiers'); /** @var Lead $foundLead */ $foundLead = $leads[0]; $this->logger->debug('FORM: Testing contact ID# '.$foundLead->getId().' for conflicts'); // Check for a conflict with the currently tracked lead $foundLeadFields = $foundLead->getProfileFields(); // Get unique identifier fields for the found lead then compare with the lead currently tracked $uniqueFieldsFound = $getData($foundLeadFields, true); [$hasConflict, $conflicts] = $checkForIdentifierConflict($uniqueFieldsFound, $uniqueFieldsCurrent); if ($inKioskMode || $hasConflict || !$lead->getId()) { // Use the found lead without merging because there is some sort of conflict with unique identifiers or in kiosk mode and thus should not merge $lead = $foundLead; if ($hasConflict) { $this->logger->debug('FORM: Conflicts found in '.implode(', ', $conflicts).' so not merging'); } else { $this->logger->debug('FORM: In kiosk mode so not merging'); } } else { $this->logger->debug('FORM: Merging contacts '.$lead->getId().' and '.$foundLead->getId()); // Merge the found lead with currently tracked lead try { $lead = $this->contactMerger->merge($lead, $foundLead); } catch (SameContactException) { } } // Update unique fields data for comparison with submitted data $currentFields = $lead->getProfileFields(); $uniqueFieldsCurrent = $getData($currentFields, true); } if (!$inKioskMode) { // Check for conflicts with the submitted data and the currently tracked lead [$hasConflict, $conflicts] = $checkForIdentifierConflict($uniqueFieldsWithData, $uniqueFieldsCurrent); $this->logger->debug( 'FORM: Current unique contact fields '.implode(', ', array_keys($uniqueFieldsCurrent)).' = '.implode(', ', $uniqueFieldsCurrent) ); $this->logger->debug( 'FORM: Submitted unique contact fields '.implode(', ', array_keys($uniqueFieldsWithData)).' = '.implode(', ', $uniqueFieldsWithData) ); if ($hasConflict) { // There's a conflict so create a new lead $lead = new Lead(); $lead->setNewlyCreated(true); $this->logger->debug( 'FORM: Conflicts found in '.implode(', ', $conflicts) .' between current tracked contact and submitted data so assuming a new contact' ); } } // check for existing IP address $ipAddress = $this->ipLookupHelper->getIpAddress(); // no lead was found by a mapped email field so create a new one if ($lead->isNewlyCreated()) { if (!$inKioskMode) { $lead->addIpAddress($ipAddress); $this->logger->debug('FORM: Associating '.$ipAddress->getIpAddress().' to contact'); } } elseif (!$inKioskMode) { $leadIpAddresses = $lead->getIpAddresses(); if (!$leadIpAddresses->contains($ipAddress)) { $lead->addIpAddress($ipAddress); $this->logger->debug('FORM: Associating '.$ipAddress->getIpAddress().' to contact'); } } // set the mapped fields $this->leadModel->setFieldValues($lead, $data, false, true, true); // last active time $lead->setLastActive(new \DateTime()); // create a new lead $lead->setManipulator(new LeadManipulator( 'form', 'submission', $form->getId(), $form->getName() )); $this->leadModel->saveEntity($lead, false); if (!$inKioskMode) { // Set the current lead which will generate tracking cookies $this->contactTracker->setTrackedContact($lead); } else { // Set system current lead which will still allow execution of events without generating tracking cookies $this->contactTracker->setSystemContact($lead); } $companyFieldMatches = $getCompanyData($leadFieldMatches); if (!empty($companyFieldMatches)) { [$company, $leadAdded, $companyEntity] = IdentifyCompanyHelper::identifyLeadsCompany($companyFieldMatches, $lead, $this->companyModel); $companyChangeLog = null; if ($leadAdded) { $companyChangeLog = $lead->addCompanyChangeLogEntry('form', 'Identify Company', 'Lead added to the company, '.$company['companyname'], $company['id']); } elseif ($companyEntity instanceof Company) { $this->companyModel->setFieldValues($companyEntity, $companyFieldMatches); $this->companyModel->saveEntity($companyEntity); } if (!empty($company) and $companyEntity instanceof Company) { // Save after the lead in for new leads created through the API and maybe other places $this->companyModel->addLeadToCompany($companyEntity, $lead); $this->leadModel->setPrimaryCompany($companyEntity->getId(), $lead->getId()); } if (null !== $companyChangeLog) { $this->companyModel->getCompanyLeadRepository()->detachEntity($companyChangeLog); } } return $lead; } /** * Validates a field value. * * @return bool|string True if valid; otherwise string with invalid reason */ protected function validateFieldValue(Field $field, $value) { $standardValidation = $this->fieldHelper->validateFieldValue($field->getType(), $value, $field); if (!empty($standardValidation)) { return $standardValidation; } $components = $this->formModel->getCustomComponents(); foreach ([$field->getType(), 'form'] as $type) { if (isset($components['validators'][$type])) { if (!is_array($components['validators'][$type])) { $components['validators'][$type] = [$components['validators'][$type]]; } foreach ($components['validators'][$type] as $validator) { if (!is_array($validator)) { $validator = ['eventName' => $validator]; } $event = $this->dispatcher->dispatch(new ValidationEvent($field, $value), $validator['eventName']); if (!$event->isValid()) { return $event->getInvalidReason(); } } } } return true; } private function normalizeValue($value, Field $f): string { $value = !is_array($value) ? [$value] : $value; // select and multiselect normalization if ($properties = $f->getProperties()['list'] ?? null) { foreach ($value as $key => $item) { $value[$key] = CustomFieldValueHelper::setValueFromPropertiesList($properties, $item); } } return implode(', ', $value); } }