1 | <?php |
||
2 | |||
3 | /* |
||
4 | * @copyright 2014 Mautic Contributors. All rights reserved |
||
5 | * @author Mautic |
||
6 | * |
||
7 | * @link http://mautic.org |
||
8 | * |
||
9 | * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html |
||
10 | */ |
||
11 | |||
12 | namespace Mautic\FormBundle\Model; |
||
13 | |||
14 | use Doctrine\ORM\ORMException; |
||
15 | use Mautic\CampaignBundle\Membership\MembershipManager; |
||
16 | use Mautic\CampaignBundle\Model\CampaignModel; |
||
17 | use Mautic\CoreBundle\Exception\FileUploadException; |
||
18 | use Mautic\CoreBundle\Helper\Chart\ChartQuery; |
||
19 | use Mautic\CoreBundle\Helper\Chart\LineChart; |
||
20 | use Mautic\CoreBundle\Helper\DateTimeHelper; |
||
21 | use Mautic\CoreBundle\Helper\InputHelper; |
||
22 | use Mautic\CoreBundle\Helper\IpLookupHelper; |
||
23 | use Mautic\CoreBundle\Helper\TemplatingHelper; |
||
24 | use Mautic\CoreBundle\Model\FormModel as CommonFormModel; |
||
25 | use Mautic\CoreBundle\Templating\Helper\DateHelper; |
||
26 | use Mautic\FormBundle\Crate\UploadFileCrate; |
||
27 | use Mautic\FormBundle\Entity\Action; |
||
28 | use Mautic\FormBundle\Entity\Field; |
||
29 | use Mautic\FormBundle\Entity\Form; |
||
30 | use Mautic\FormBundle\Entity\Submission; |
||
31 | use Mautic\FormBundle\Entity\SubmissionRepository; |
||
32 | use Mautic\FormBundle\Event\Service\FieldValueTransformer; |
||
33 | use Mautic\FormBundle\Event\SubmissionEvent; |
||
34 | use Mautic\FormBundle\Event\ValidationEvent; |
||
35 | use Mautic\FormBundle\Exception\FileValidationException; |
||
36 | use Mautic\FormBundle\Exception\NoFileGivenException; |
||
37 | use Mautic\FormBundle\Exception\ValidationException; |
||
38 | use Mautic\FormBundle\FormEvents; |
||
39 | use Mautic\FormBundle\Helper\FormFieldHelper; |
||
40 | use Mautic\FormBundle\Helper\FormUploader; |
||
41 | use Mautic\FormBundle\Validator\UploadFieldValidator; |
||
42 | use Mautic\LeadBundle\DataObject\LeadManipulator; |
||
43 | use Mautic\LeadBundle\Entity\Company; |
||
44 | use Mautic\LeadBundle\Entity\CompanyChangeLog; |
||
45 | use Mautic\LeadBundle\Entity\Lead; |
||
46 | use Mautic\LeadBundle\Helper\IdentifyCompanyHelper; |
||
47 | use Mautic\LeadBundle\Model\CompanyModel; |
||
48 | use Mautic\LeadBundle\Model\FieldModel as LeadFieldModel; |
||
49 | use Mautic\LeadBundle\Model\LeadModel; |
||
50 | use Mautic\LeadBundle\Tracker\ContactTracker; |
||
51 | use Mautic\LeadBundle\Tracker\Service\DeviceTrackingService\DeviceTrackingServiceInterface; |
||
52 | use Mautic\PageBundle\Model\PageModel; |
||
53 | use PhpOffice\PhpSpreadsheet\IOFactory; |
||
54 | use PhpOffice\PhpSpreadsheet\Spreadsheet; |
||
55 | use Symfony\Component\HttpFoundation\Request; |
||
56 | use Symfony\Component\HttpFoundation\Response; |
||
57 | use Symfony\Component\HttpFoundation\StreamedResponse; |
||
58 | |||
59 | class SubmissionModel extends CommonFormModel |
||
60 | { |
||
61 | /** |
||
62 | * @var IpLookupHelper |
||
63 | */ |
||
64 | protected $ipLookupHelper; |
||
65 | |||
66 | /** |
||
67 | * @var TemplatingHelper |
||
68 | */ |
||
69 | protected $templatingHelper; |
||
70 | |||
71 | /** |
||
72 | * @var FormModel |
||
73 | */ |
||
74 | protected $formModel; |
||
75 | |||
76 | /** |
||
77 | * @var PageModel |
||
78 | */ |
||
79 | protected $pageModel; |
||
80 | |||
81 | /** |
||
82 | * @var LeadModel |
||
83 | */ |
||
84 | protected $leadModel; |
||
85 | |||
86 | /** |
||
87 | * @var CampaignModel |
||
88 | */ |
||
89 | protected $campaignModel; |
||
90 | |||
91 | /** |
||
92 | * @var MembershipManager |
||
93 | */ |
||
94 | protected $membershipManager; |
||
95 | |||
96 | /** |
||
97 | * @var LeadFieldModel |
||
98 | */ |
||
99 | protected $leadFieldModel; |
||
100 | |||
101 | /** |
||
102 | * @var CompanyModel |
||
103 | */ |
||
104 | protected $companyModel; |
||
105 | |||
106 | /** |
||
107 | * @var FormFieldHelper |
||
108 | */ |
||
109 | protected $fieldHelper; |
||
110 | |||
111 | /** |
||
112 | * @var UploadFieldValidator |
||
113 | */ |
||
114 | private $uploadFieldValidator; |
||
115 | |||
116 | /** |
||
117 | * @var FormUploader |
||
118 | */ |
||
119 | private $formUploader; |
||
120 | |||
121 | /** |
||
122 | * @var DeviceTrackingServiceInterface |
||
123 | */ |
||
124 | private $deviceTrackingService; |
||
125 | |||
126 | /** |
||
127 | * @var FieldValueTransformer |
||
128 | */ |
||
129 | private $fieldValueTransformer; |
||
130 | |||
131 | /** |
||
132 | * @var DateHelper |
||
133 | */ |
||
134 | private $dateHelper; |
||
135 | |||
136 | /** |
||
137 | * @var ContactTracker |
||
138 | */ |
||
139 | private $contactTracker; |
||
140 | |||
141 | public function __construct( |
||
142 | IpLookupHelper $ipLookupHelper, |
||
143 | TemplatingHelper $templatingHelper, |
||
144 | FormModel $formModel, |
||
145 | PageModel $pageModel, |
||
146 | LeadModel $leadModel, |
||
147 | CampaignModel $campaignModel, |
||
148 | MembershipManager $membershipManager, |
||
149 | LeadFieldModel $leadFieldModel, |
||
150 | CompanyModel $companyModel, |
||
151 | FormFieldHelper $fieldHelper, |
||
152 | UploadFieldValidator $uploadFieldValidator, |
||
153 | FormUploader $formUploader, |
||
154 | DeviceTrackingServiceInterface $deviceTrackingService, |
||
155 | FieldValueTransformer $fieldValueTransformer, |
||
156 | DateHelper $dateHelper, |
||
157 | ContactTracker $contactTracker |
||
158 | ) { |
||
159 | $this->ipLookupHelper = $ipLookupHelper; |
||
160 | $this->templatingHelper = $templatingHelper; |
||
161 | $this->formModel = $formModel; |
||
162 | $this->pageModel = $pageModel; |
||
163 | $this->leadModel = $leadModel; |
||
164 | $this->campaignModel = $campaignModel; |
||
165 | $this->membershipManager = $membershipManager; |
||
166 | $this->leadFieldModel = $leadFieldModel; |
||
167 | $this->companyModel = $companyModel; |
||
168 | $this->fieldHelper = $fieldHelper; |
||
169 | $this->uploadFieldValidator = $uploadFieldValidator; |
||
170 | $this->formUploader = $formUploader; |
||
171 | $this->deviceTrackingService = $deviceTrackingService; |
||
172 | $this->fieldValueTransformer = $fieldValueTransformer; |
||
173 | $this->dateHelper = $dateHelper; |
||
174 | $this->contactTracker = $contactTracker; |
||
175 | } |
||
176 | |||
177 | /** |
||
178 | * {@inheritdoc} |
||
179 | * |
||
180 | * @return SubmissionRepository |
||
181 | */ |
||
182 | public function getRepository() |
||
183 | { |
||
184 | return $this->em->getRepository('MauticFormBundle:Submission'); |
||
185 | } |
||
186 | |||
187 | /** |
||
188 | * @param $post |
||
189 | * @param $server |
||
190 | * @param bool $returnEvent |
||
191 | * |
||
192 | * @return bool|array |
||
193 | * |
||
194 | * @throws ORMException |
||
195 | */ |
||
196 | public function saveSubmission($post, $server, Form $form, Request $request, $returnEvent = false) |
||
197 | { |
||
198 | $leadFields = $this->leadFieldModel->getFieldListWithProperties(false); |
||
199 | |||
200 | //everything matches up so let's save the results |
||
201 | $submission = new Submission(); |
||
202 | $submission->setDateSubmitted(new \DateTime()); |
||
203 | $submission->setForm($form); |
||
204 | |||
205 | //set the landing page the form was submitted from if applicable |
||
206 | if (!empty($post['mauticpage'])) { |
||
207 | $page = $this->pageModel->getEntity((int) $post['mauticpage']); |
||
208 | if (null != $page) { |
||
209 | $submission->setPage($page); |
||
210 | } |
||
211 | } |
||
212 | |||
213 | $ipAddress = $this->ipLookupHelper->getIpAddress(); |
||
214 | $submission->setIpAddress($ipAddress); |
||
215 | |||
216 | if (!empty($post['return'])) { |
||
217 | $referer = $post['return']; |
||
218 | } elseif (!empty($server['HTTP_REFERER'])) { |
||
219 | $referer = $server['HTTP_REFERER']; |
||
220 | } else { |
||
221 | $referer = ''; |
||
222 | } |
||
223 | |||
224 | //clean the referer by removing mauticError and mauticMessage |
||
225 | $referer = InputHelper::url($referer, null, null, ['mauticError', 'mauticMessage']); |
||
226 | $submission->setReferer($referer); |
||
227 | |||
228 | // Create an event to be dispatched through the processes |
||
229 | $submissionEvent = new SubmissionEvent($submission, $post, $server, $request); |
||
230 | |||
231 | // Get a list of components to build custom fields from |
||
232 | $components = $this->formModel->getCustomComponents(); |
||
233 | |||
234 | $fields = $form->getFields(); |
||
235 | $fieldArray = []; |
||
236 | $results = []; |
||
237 | $tokens = []; |
||
238 | $leadFieldMatches = []; |
||
239 | $validationErrors = []; |
||
240 | $filesToUpload = new UploadFileCrate(); |
||
241 | |||
242 | /** @var Field $f */ |
||
243 | foreach ($fields as $f) { |
||
244 | $id = $f->getId(); |
||
245 | $type = $f->getType(); |
||
246 | $alias = $f->getAlias(); |
||
247 | $value = (isset($post[$alias])) ? $post[$alias] : ''; |
||
248 | |||
249 | $fieldArray[$id] = [ |
||
250 | 'id' => $id, |
||
251 | 'type' => $type, |
||
252 | 'alias' => $alias, |
||
253 | ]; |
||
254 | |||
255 | if ($f->isCaptchaType()) { |
||
256 | $captcha = $this->fieldHelper->validateFieldValue($type, $value, $f); |
||
257 | if (!empty($captcha)) { |
||
258 | $props = $f->getProperties(); |
||
259 | //check for a custom message |
||
260 | $validationErrors[$alias] = (!empty($props['errorMessage'])) ? $props['errorMessage'] : implode('<br />', $captcha); |
||
261 | } |
||
262 | continue; |
||
263 | } elseif ($f->isFileType()) { |
||
264 | try { |
||
265 | $file = $this->uploadFieldValidator->processFileValidation($f, $request); |
||
266 | $value = $file->getClientOriginalName(); |
||
267 | $filesToUpload->addFile($file, $f); |
||
268 | } catch (NoFileGivenException $e) { //No error here, we just move to another validation, eg. if a field is required |
||
269 | } catch (FileValidationException $e) { |
||
270 | $validationErrors[$alias] = $e->getMessage(); |
||
271 | } |
||
272 | } |
||
273 | |||
274 | if ('' === $value && $f->isRequired()) { |
||
275 | //field is required, but hidden from form because of 'ShowWhenValueExists' |
||
276 | if (false === $f->getShowWhenValueExists() && !isset($post[$alias])) { |
||
277 | continue; |
||
278 | } |
||
279 | |||
280 | //somehow the user got passed the JS validation |
||
281 | $msg = $f->getValidationMessage(); |
||
282 | if (empty($msg)) { |
||
283 | $msg = $this->translator->trans( |
||
284 | 'mautic.form.field.generic.validationfailed', |
||
285 | [ |
||
286 | '%label%' => $f->getLabel(), |
||
287 | ], |
||
288 | 'validators' |
||
289 | ); |
||
290 | } |
||
291 | |||
292 | $validationErrors[$alias] = $msg; |
||
293 | |||
294 | continue; |
||
295 | } |
||
296 | |||
297 | if (isset($components['viewOnlyFields']) && in_array($type, $components['viewOnlyFields'])) { |
||
298 | //don't save items that don't have a value associated with it |
||
299 | continue; |
||
300 | } |
||
301 | |||
302 | //clean and validate the input |
||
303 | if ($f->isCustom()) { |
||
304 | if (!isset($components['fields'][$f->getType()])) { |
||
305 | continue; |
||
306 | } |
||
307 | |||
308 | $params = $components['fields'][$f->getType()]; |
||
309 | if (!empty($value)) { |
||
310 | if (isset($params['valueFilter'])) { |
||
311 | if (is_string($params['valueFilter']) && is_callable(['\Mautic\CoreBundle\Helper\InputHelper', $params['valueFilter']])) { |
||
312 | $value = InputHelper::_($value, $params['valueFilter']); |
||
313 | } elseif (is_callable($params['valueFilter'])) { |
||
314 | $value = call_user_func_array($params['valueFilter'], [$f, $value]); |
||
315 | } else { |
||
316 | $value = InputHelper::_($value, 'clean'); |
||
317 | } |
||
318 | } else { |
||
319 | $value = InputHelper::_($value, 'clean'); |
||
320 | } |
||
321 | } |
||
322 | } elseif (!empty($value)) { |
||
323 | $filter = $this->fieldHelper->getFieldFilter($type); |
||
324 | $value = InputHelper::_($value, $filter); |
||
325 | |||
326 | $isValid = $this->validateFieldValue($f, $value); |
||
327 | if (true !== $isValid) { |
||
328 | $validationErrors[$alias] = is_array($isValid) ? implode('<br />', $isValid) : $isValid; |
||
329 | } |
||
330 | } |
||
331 | |||
332 | // Check for custom validators |
||
333 | $isValid = $this->validateFieldValue($f, $value); |
||
334 | if (true !== $isValid) { |
||
335 | $validationErrors[$alias] = $isValid; |
||
336 | } |
||
337 | |||
338 | $leadField = $f->getLeadField(); |
||
339 | if (!empty($leadField)) { |
||
340 | $leadValue = $value; |
||
341 | |||
342 | $leadFieldMatches[$leadField] = $leadValue; |
||
343 | } |
||
344 | |||
345 | //convert array from checkbox groups and multiple selects |
||
346 | if (is_array($value)) { |
||
347 | $value = implode(', ', $value); |
||
348 | } |
||
349 | |||
350 | $tokens["{formfield={$alias}}"] = $value; |
||
351 | |||
352 | //save the result |
||
353 | if (false !== $f->getSaveResult()) { |
||
354 | $results[$alias] = $value; |
||
355 | } |
||
356 | } |
||
357 | |||
358 | // Set the results |
||
359 | $submission->setResults($results); |
||
360 | |||
361 | // Update the event |
||
362 | $submissionEvent->setFields($fieldArray) |
||
363 | ->setTokens($tokens) |
||
364 | ->setResults($results) |
||
365 | ->setContactFieldMatches($leadFieldMatches); |
||
366 | |||
367 | $lead = $this->contactTracker->getContact(); |
||
368 | |||
369 | // Remove validation errors if the field is not visible |
||
370 | if ($lead && $form->usesProgressiveProfiling()) { |
||
371 | $leadSubmissions = $this->formModel->getLeadSubmissions($form, $lead->getId()); |
||
372 | |||
373 | foreach ($fields as $field) { |
||
374 | if (isset($validationErrors[$field->getAlias()]) && !$field->showForContact($leadSubmissions, $lead, $form)) { |
||
375 | unset($validationErrors[$field->getAlias()]); |
||
376 | } |
||
377 | } |
||
378 | } |
||
379 | |||
380 | //return errors if there any |
||
381 | if (!empty($validationErrors)) { |
||
382 | return ['errors' => $validationErrors]; |
||
383 | } |
||
384 | |||
385 | // Create/update lead |
||
386 | if (!empty($leadFieldMatches)) { |
||
387 | $lead = $this->createLeadFromSubmit($form, $leadFieldMatches, $leadFields); |
||
388 | } |
||
389 | |||
390 | $trackedDevice = $this->deviceTrackingService->getTrackedDevice(); |
||
391 | $trackingId = (null === $trackedDevice ? null : $trackedDevice->getTrackingId()); |
||
392 | |||
393 | //set tracking ID for stats purposes to determine unique hits |
||
394 | $submission->setTrackingId($trackingId) |
||
395 | ->setLead($lead); |
||
396 | |||
397 | /* |
||
398 | * Process File upload and save the result to the entity |
||
399 | * Upload is here to minimize a need for deleting file if there is a validation error |
||
400 | * The action can still be invalidated below - deleteEntity takes care for File deletion |
||
401 | * |
||
402 | * @todo Refactor form validation to execute this code only if Submission is valid |
||
403 | */ |
||
404 | try { |
||
405 | $this->formUploader->uploadFiles($filesToUpload, $submission); |
||
406 | } catch (FileUploadException $e) { |
||
407 | $msg = $this->translator->trans('mautic.form.submission.error.file.uploadFailed', [], 'validators'); |
||
408 | $validationErrors[$e->getMessage()] = $msg; |
||
409 | |||
410 | return ['errors' => $validationErrors]; |
||
411 | } |
||
412 | |||
413 | // set results after uploader what can change file name if file name exists |
||
414 | $submissionEvent->setResults($submission->getResults()); |
||
415 | |||
416 | // Save the submission |
||
417 | $this->saveEntity($submission); |
||
418 | $this->fieldValueTransformer->transformValuesAfterSubmit($submissionEvent); |
||
419 | // Now handle post submission actions |
||
420 | try { |
||
421 | $this->executeFormActions($submissionEvent); |
||
422 | } catch (ValidationException $exception) { |
||
423 | // The action invalidated the form for whatever reason |
||
424 | $this->deleteEntity($submission); |
||
425 | |||
426 | if ($validationErrors = $exception->getViolations()) { |
||
427 | return ['errors' => $validationErrors]; |
||
428 | } |
||
429 | |||
430 | return ['errors' => [$exception->getMessage()]]; |
||
431 | } |
||
432 | |||
433 | // update contact fields with transform values |
||
434 | if (!empty($this->fieldValueTransformer->getContactFieldsToUpdate())) { |
||
435 | $this->leadModel->setFieldValues($lead, $this->fieldValueTransformer->getContactFieldsToUpdate()); |
||
436 | $this->leadModel->saveEntity($lead, false); |
||
437 | } |
||
438 | |||
439 | if (!$form->isStandalone()) { |
||
440 | // Find and add the lead to the associated campaigns |
||
441 | $campaigns = $this->campaignModel->getCampaignsByForm($form); |
||
442 | if (!empty($campaigns)) { |
||
443 | foreach ($campaigns as $campaign) { |
||
444 | $this->membershipManager->addContact($lead, $campaign); |
||
445 | } |
||
446 | } |
||
447 | } |
||
448 | |||
449 | if ($this->dispatcher->hasListeners(FormEvents::FORM_ON_SUBMIT)) { |
||
450 | // Reset action config from executeFormActions() |
||
451 | $submissionEvent->setAction(null); |
||
452 | |||
453 | // Dispatch to on submit listeners |
||
454 | $this->dispatcher->dispatch(FormEvents::FORM_ON_SUBMIT, $submissionEvent); |
||
455 | } |
||
456 | |||
457 | //get callback commands from the submit action |
||
458 | if ($submissionEvent->hasPostSubmitCallbacks()) { |
||
459 | return ['callback' => $submissionEvent]; |
||
460 | } |
||
461 | |||
462 | // made it to the end so return the submission event to give the calling method access to tokens, results, etc |
||
463 | // otherwise return false that no errors were encountered (to keep BC really) |
||
464 | return ($returnEvent) ? ['submission' => $submissionEvent] : false; |
||
465 | } |
||
466 | |||
467 | /** |
||
468 | * @param Submission $submission |
||
469 | */ |
||
470 | public function deleteEntity($submission) |
||
471 | { |
||
472 | $this->formUploader->deleteUploadedFiles($submission); |
||
473 | |||
474 | parent::deleteEntity($submission); |
||
475 | } |
||
476 | |||
477 | /** |
||
478 | * {@inheritdoc} |
||
479 | */ |
||
480 | public function getEntities(array $args = []) |
||
481 | { |
||
482 | return $this->getRepository()->getEntities($args); |
||
483 | } |
||
484 | |||
485 | /** |
||
486 | * @param $format |
||
487 | * @param $form |
||
488 | * @param $queryArgs |
||
489 | * |
||
490 | * @return StreamedResponse|Response |
||
491 | * |
||
492 | * @throws \Exception |
||
493 | */ |
||
494 | public function exportResults($format, $form, $queryArgs) |
||
495 | { |
||
496 | $viewOnlyFields = $this->formModel->getCustomComponents()['viewOnlyFields']; |
||
497 | $queryArgs['viewOnlyFields'] = $viewOnlyFields; |
||
498 | $queryArgs['simpleResults'] = true; |
||
499 | $results = $this->getEntities($queryArgs); |
||
500 | $translator = $this->translator; |
||
501 | |||
502 | $date = (new DateTimeHelper())->toLocalString(); |
||
503 | $name = str_replace(' ', '_', $date).'_'.$form->getAlias(); |
||
504 | |||
505 | switch ($format) { |
||
506 | case 'csv': |
||
507 | $response = new StreamedResponse( |
||
508 | function () use ($results, $form, $translator, $viewOnlyFields) { |
||
509 | $handle = fopen('php://output', 'r+'); |
||
510 | |||
511 | //build the header row |
||
512 | $fields = $form->getFields(); |
||
513 | $header = [ |
||
514 | $translator->trans('mautic.core.id'), |
||
515 | $translator->trans('mautic.form.result.thead.date'), |
||
516 | $translator->trans('mautic.core.ipaddress'), |
||
517 | $translator->trans('mautic.form.result.thead.referrer'), |
||
518 | ]; |
||
519 | foreach ($fields as $f) { |
||
520 | if (in_array($f->getType(), $viewOnlyFields) || false === $f->getSaveResult()) { |
||
521 | continue; |
||
522 | } |
||
523 | $header[] = $f->getLabel(); |
||
524 | } |
||
525 | //free memory |
||
526 | unset($fields); |
||
527 | |||
528 | //write the row |
||
529 | fputcsv($handle, $header); |
||
530 | |||
531 | //build the data rows |
||
532 | foreach ($results as $k => $s) { |
||
533 | $row = [ |
||
534 | $s['id'], |
||
535 | $this->dateHelper->toFull($s['dateSubmitted'], 'UTC'), |
||
536 | $s['ipAddress'], |
||
537 | $s['referer'], |
||
538 | ]; |
||
539 | foreach ($s['results'] as $k2 => $r) { |
||
540 | if (in_array($r['type'], $viewOnlyFields)) { |
||
541 | continue; |
||
542 | } |
||
543 | $row[] = htmlspecialchars_decode($r['value'], ENT_QUOTES); |
||
544 | //free memory |
||
545 | unset($s['results'][$k2]); |
||
546 | } |
||
547 | |||
548 | fputcsv($handle, $row); |
||
549 | |||
550 | //free memory |
||
551 | unset($row, $results[$k]); |
||
552 | } |
||
553 | |||
554 | fclose($handle); |
||
555 | } |
||
556 | ); |
||
557 | |||
558 | $response->headers->set('Content-Type', 'application/force-download'); |
||
559 | $response->headers->set('Content-Type', 'application/octet-stream'); |
||
560 | $response->headers->set('Content-Disposition', 'attachment; filename="'.$name.'.csv"'); |
||
561 | $response->headers->set('Expires', 0); |
||
562 | $response->headers->set('Cache-Control', 'must-revalidate'); |
||
563 | $response->headers->set('Pragma', 'public'); |
||
564 | |||
565 | return $response; |
||
566 | case 'html': |
||
567 | $content = $this->templatingHelper->getTemplating()->renderResponse( |
||
568 | 'MauticFormBundle:Result:export.html.php', |
||
569 | [ |
||
570 | 'form' => $form, |
||
571 | 'results' => $results, |
||
572 | 'pageTitle' => $name, |
||
573 | 'viewOnlyFields' => $viewOnlyFields, |
||
574 | ] |
||
575 | )->getContent(); |
||
576 | |||
577 | return new Response($content); |
||
578 | case 'xlsx': |
||
579 | if (class_exists(Spreadsheet::class)) { |
||
580 | $response = new StreamedResponse( |
||
581 | function () use ($results, $form, $translator, $name, $viewOnlyFields) { |
||
582 | $objPHPExcel = new Spreadsheet(); |
||
583 | $objPHPExcel->getProperties()->setTitle($name); |
||
584 | |||
585 | $objPHPExcel->createSheet(); |
||
586 | |||
587 | //build the header row |
||
588 | $fields = $form->getFields(); |
||
589 | $header = [ |
||
590 | $translator->trans('mautic.core.id'), |
||
591 | $translator->trans('mautic.form.result.thead.date'), |
||
592 | $translator->trans('mautic.core.ipaddress'), |
||
593 | $translator->trans('mautic.form.result.thead.referrer'), |
||
594 | ]; |
||
595 | foreach ($fields as $f) { |
||
596 | if (in_array($f->getType(), $viewOnlyFields) || false === $f->getSaveResult()) { |
||
597 | continue; |
||
598 | } |
||
599 | $header[] = $f->getLabel(); |
||
600 | } |
||
601 | //free memory |
||
602 | unset($fields); |
||
603 | |||
604 | //write the row |
||
605 | $objPHPExcel->getActiveSheet()->fromArray($header, null, 'A1'); |
||
606 | |||
607 | //build the data rows |
||
608 | $count = 2; |
||
609 | foreach ($results as $k => $s) { |
||
610 | $row = [ |
||
611 | $s['id'], |
||
612 | $this->dateHelper->toFull($s['dateSubmitted'], 'UTC'), |
||
613 | $s['ipAddress'], |
||
614 | $s['referer'], |
||
615 | ]; |
||
616 | foreach ($s['results'] as $k2 => $r) { |
||
617 | if (in_array($r['type'], $viewOnlyFields)) { |
||
618 | continue; |
||
619 | } |
||
620 | $row[] = htmlspecialchars_decode($r['value'], ENT_QUOTES); |
||
621 | //free memory |
||
622 | unset($s['results'][$k2]); |
||
623 | } |
||
624 | |||
625 | $objPHPExcel->getActiveSheet()->fromArray($row, null, "A{$count}"); |
||
626 | |||
627 | //free memory |
||
628 | unset($row, $results[$k]); |
||
629 | |||
630 | //increment letter |
||
631 | ++$count; |
||
632 | } |
||
633 | |||
634 | $objWriter = IOFactory::createWriter($objPHPExcel, 'Xlsx'); |
||
635 | $objWriter->setPreCalculateFormulas(false); |
||
636 | |||
637 | $objWriter->save('php://output'); |
||
638 | } |
||
639 | ); |
||
640 | $response->headers->set('Content-Type', 'application/force-download'); |
||
641 | $response->headers->set('Content-Type', 'application/octet-stream'); |
||
642 | $response->headers->set('Content-Disposition', 'attachment; filename="'.$name.'.xlsx"'); |
||
643 | $response->headers->set('Expires', 0); |
||
644 | $response->headers->set('Cache-Control', 'must-revalidate'); |
||
645 | $response->headers->set('Pragma', 'public'); |
||
646 | |||
647 | return $response; |
||
648 | } |
||
649 | throw new \Exception('PHPSpreadsheet is required to export to Excel spreadsheets'); |
||
650 | default: |
||
651 | return new Response(); |
||
652 | } |
||
653 | } |
||
654 | |||
655 | /** |
||
656 | * Get line chart data of submissions. |
||
657 | * |
||
658 | * @param string $unit {@link php.net/manual/en/function.date.php#refsect1-function.date-parameters} |
||
659 | * @param string $dateFormat |
||
660 | * @param array $filter |
||
661 | * @param bool $canViewOthers |
||
662 | * |
||
663 | * @return array |
||
664 | */ |
||
665 | public function getSubmissionsLineChartData( |
||
666 | $unit, |
||
667 | \DateTime $dateFrom, |
||
668 | \DateTime $dateTo, |
||
669 | $dateFormat = null, |
||
670 | $filter = [], |
||
671 | $canViewOthers = true |
||
672 | ) { |
||
673 | $chart = new LineChart($unit, $dateFrom, $dateTo, $dateFormat); |
||
674 | $query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo); |
||
675 | $q = $query->prepareTimeDataQuery('form_submissions', 'date_submitted', $filter); |
||
676 | |||
677 | if (!$canViewOthers) { |
||
678 | $q->join('t', MAUTIC_TABLE_PREFIX.'forms', 'f', 'f.id = t.form_id') |
||
679 | ->andWhere('f.created_by = :userId') |
||
680 | ->setParameter('userId', $this->userHelper->getUser()->getId()); |
||
681 | } |
||
682 | |||
683 | $data = $query->loadAndBuildTimeData($q); |
||
684 | $chart->setDataset($this->translator->trans('mautic.form.submission.count'), $data); |
||
685 | |||
686 | return $chart->render(); |
||
687 | } |
||
688 | |||
689 | /** |
||
690 | * Get a list of top submission referrers. |
||
691 | * |
||
692 | * @param int $limit |
||
693 | * @param string $dateFrom |
||
694 | * @param string $dateTo |
||
695 | * @param array $filters |
||
696 | * @param bool $canViewOthers |
||
697 | * |
||
698 | * @return array |
||
699 | */ |
||
700 | public function getTopSubmissionReferrers($limit = 10, $dateFrom = null, $dateTo = null, $filters = [], $canViewOthers = true) |
||
701 | { |
||
702 | $q = $this->em->getConnection()->createQueryBuilder(); |
||
703 | $q->select('COUNT(DISTINCT t.id) AS submissions, t.referer') |
||
704 | ->from(MAUTIC_TABLE_PREFIX.'form_submissions', 't') |
||
705 | ->orderBy('submissions', 'DESC') |
||
706 | ->groupBy('t.referer') |
||
707 | ->setMaxResults($limit); |
||
708 | |||
709 | if (!$canViewOthers) { |
||
710 | $q->join('t', MAUTIC_TABLE_PREFIX.'forms', 'f', 'f.id = t.form_id') |
||
711 | ->andWhere('f.created_by = :userId') |
||
712 | ->setParameter('userId', $this->userHelper->getUser()->getId()); |
||
713 | } |
||
714 | |||
715 | $chartQuery = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo); |
||
716 | $chartQuery->applyFilters($q, $filters); |
||
717 | $chartQuery->applyDateFilters($q, 'date_submitted'); |
||
718 | |||
719 | return $q->execute()->fetchAll(); |
||
720 | } |
||
721 | |||
722 | /** |
||
723 | * Get a list of the most submisions per lead. |
||
724 | * |
||
725 | * @param int $limit |
||
726 | * @param string $dateFrom |
||
727 | * @param string $dateTo |
||
728 | * @param array $filters |
||
729 | * @param bool $canViewOthers |
||
730 | * |
||
731 | * @return array |
||
732 | */ |
||
733 | public function getTopSubmitters($limit = 10, $dateFrom = null, $dateTo = null, $filters = [], $canViewOthers = true) |
||
734 | { |
||
735 | $q = $this->em->getConnection()->createQueryBuilder(); |
||
736 | $q->select('COUNT(DISTINCT t.id) AS submissions, t.lead_id, l.firstname, l.lastname, l.email') |
||
737 | ->from(MAUTIC_TABLE_PREFIX.'form_submissions', 't') |
||
738 | ->join('t', MAUTIC_TABLE_PREFIX.'leads', 'l', 'l.id = t.lead_id') |
||
739 | ->orderBy('submissions', 'DESC') |
||
740 | ->groupBy('t.lead_id, l.firstname, l.lastname, l.email') |
||
741 | ->setMaxResults($limit); |
||
742 | |||
743 | if (!$canViewOthers) { |
||
744 | $q->join('t', MAUTIC_TABLE_PREFIX.'forms', 'f', 'f.id = t.form_id') |
||
745 | ->andWhere('f.created_by = :userId') |
||
746 | ->setParameter('userId', $this->userHelper->getUser()->getId()); |
||
747 | } |
||
748 | |||
749 | $chartQuery = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo); |
||
750 | $chartQuery->applyFilters($q, $filters); |
||
751 | $chartQuery->applyDateFilters($q, 'date_submitted'); |
||
752 | |||
753 | return $q->execute()->fetchAll(); |
||
754 | } |
||
755 | |||
756 | /** |
||
757 | * Execute a form submit action. |
||
758 | * |
||
759 | * @throws ValidationException |
||
760 | */ |
||
761 | protected function executeFormActions(SubmissionEvent $event): void |
||
762 | { |
||
763 | $actions = $event->getSubmission()->getForm()->getActions(); |
||
764 | $customComponents = $this->formModel->getCustomComponents(); |
||
765 | $availableActions = $customComponents['actions'] ?? []; |
||
766 | |||
767 | $actions->filter(function (Action $action) use ($availableActions) { |
||
768 | return array_key_exists($action->getType(), $availableActions); |
||
769 | })->map(function (Action $action) use ($event, $availableActions) { |
||
770 | $event->setAction($action); |
||
771 | $this->dispatcher->dispatch($availableActions[$action->getType()]['eventName'], $event); |
||
772 | }); |
||
773 | } |
||
774 | |||
775 | /** |
||
776 | * Create/update lead from form submit. |
||
777 | * |
||
778 | * @return Lead |
||
779 | * |
||
780 | * @throws ORMException |
||
781 | */ |
||
782 | protected function createLeadFromSubmit(Form $form, array $leadFieldMatches, $leadFields) |
||
783 | { |
||
784 | //set the mapped data |
||
785 | $inKioskMode = $form->isInKioskMode(); |
||
786 | $leadId = null; |
||
787 | $lead = new Lead(); |
||
788 | $currentFields = $leadFieldMatches; |
||
789 | $companyFields = $this->leadFieldModel->getFieldListWithProperties('company'); |
||
790 | |||
791 | if (!$inKioskMode) { |
||
792 | // Default to currently tracked lead |
||
793 | if ($currentLead = $this->contactTracker->getContact()) { |
||
794 | $lead = $currentLead; |
||
795 | $leadId = $lead->getId(); |
||
796 | $currentFields = $lead->getProfileFields(); |
||
797 | } |
||
798 | |||
799 | $this->logger->debug('FORM: Not in kiosk mode so using current contact ID #'.$leadId); |
||
800 | } else { |
||
801 | // Default to a new lead in kiosk mode |
||
802 | $lead->setNewlyCreated(true); |
||
803 | |||
804 | $this->logger->debug('FORM: In kiosk mode so assuming a new contact'); |
||
805 | } |
||
806 | |||
807 | $uniqueLeadFields = $this->leadFieldModel->getUniqueIdentifierFields(); |
||
808 | |||
809 | // Closure to get data and unique fields |
||
810 | $getData = function ($currentFields, $uniqueOnly = false) use ($leadFields, $uniqueLeadFields) { |
||
811 | $uniqueFieldsWithData = $data = []; |
||
812 | foreach ($leadFields as $alias => $properties) { |
||
813 | if (isset($currentFields[$alias])) { |
||
814 | $value = $currentFields[$alias]; |
||
815 | $data[$alias] = $value; |
||
816 | |||
817 | // make sure the value is actually there and the field is one of our uniques |
||
818 | if (!empty($value) && array_key_exists($alias, $uniqueLeadFields)) { |
||
819 | $uniqueFieldsWithData[$alias] = $value; |
||
820 | } |
||
821 | } |
||
822 | } |
||
823 | |||
824 | return ($uniqueOnly) ? $uniqueFieldsWithData : [$data, $uniqueFieldsWithData]; |
||
825 | }; |
||
826 | |||
827 | // Closure to get data and unique fields |
||
828 | $getCompanyData = function ($currentFields) use ($companyFields) { |
||
829 | $companyData = []; |
||
830 | // force add company contact field to company fields check |
||
831 | $companyFields = array_merge($companyFields, ['company'=> 'company']); |
||
832 | foreach ($companyFields as $alias => $properties) { |
||
833 | if (isset($currentFields[$alias])) { |
||
834 | $value = $currentFields[$alias]; |
||
835 | $companyData[$alias] = $value; |
||
836 | } |
||
837 | } |
||
838 | |||
839 | return $companyData; |
||
840 | }; |
||
841 | |||
842 | // Closure to help search for a conflict |
||
843 | $checkForIdentifierConflict = function ($fieldSet1, $fieldSet2) { |
||
844 | // Find fields in both sets |
||
845 | $potentialConflicts = array_keys( |
||
846 | array_intersect_key($fieldSet1, $fieldSet2) |
||
847 | ); |
||
848 | |||
849 | $this->logger->debug( |
||
850 | 'FORM: Potential conflicts '.implode(', ', array_keys($potentialConflicts)).' = '.implode(', ', $potentialConflicts) |
||
851 | ); |
||
852 | |||
853 | $conflicts = []; |
||
854 | foreach ($potentialConflicts as $field) { |
||
855 | if (!empty($fieldSet1[$field]) && !empty($fieldSet2[$field])) { |
||
856 | if (strtolower($fieldSet1[$field]) !== strtolower($fieldSet2[$field])) { |
||
857 | $conflicts[] = $field; |
||
858 | } |
||
859 | } |
||
860 | } |
||
861 | |||
862 | return [count($conflicts), $conflicts]; |
||
863 | }; |
||
864 | |||
865 | // Get data for the form submission |
||
866 | list($data, $uniqueFieldsWithData) = $getData($leadFieldMatches); |
||
867 | $this->logger->debug('FORM: Unique fields submitted include '.implode(', ', $uniqueFieldsWithData)); |
||
868 | |||
869 | // Check for duplicate lead |
||
870 | /** @var \Mautic\LeadBundle\Entity\Lead[] $leads */ |
||
871 | $leads = (!empty($uniqueFieldsWithData)) ? $this->em->getRepository('MauticLeadBundle:Lead')->getLeadsByUniqueFields( |
||
872 | $uniqueFieldsWithData, |
||
873 | $leadId |
||
874 | ) : []; |
||
875 | |||
876 | $uniqueFieldsCurrent = $getData($currentFields, true); |
||
877 | if (count($leads)) { |
||
878 | $this->logger->debug(count($leads).' found based on unique identifiers'); |
||
879 | |||
880 | /** @var \Mautic\LeadBundle\Entity\Lead $foundLead */ |
||
881 | $foundLead = $leads[0]; |
||
882 | |||
883 | $this->logger->debug('FORM: Testing contact ID# '.$foundLead->getId().' for conflicts'); |
||
884 | |||
885 | // Check for a conflict with the currently tracked lead |
||
886 | $foundLeadFields = $foundLead->getProfileFields(); |
||
887 | |||
888 | // Get unique identifier fields for the found lead then compare with the lead currently tracked |
||
889 | $uniqueFieldsFound = $getData($foundLeadFields, true); |
||
890 | list($hasConflict, $conflicts) = $checkForIdentifierConflict($uniqueFieldsFound, $uniqueFieldsCurrent); |
||
891 | |||
892 | if ($inKioskMode || $hasConflict || !$lead->getId()) { |
||
893 | // 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 |
||
894 | $lead = $foundLead; |
||
895 | |||
896 | if ($hasConflict) { |
||
897 | $this->logger->debug('FORM: Conflicts found in '.implode(', ', $conflicts).' so not merging'); |
||
898 | } else { |
||
899 | $this->logger->debug('FORM: In kiosk mode so not merging'); |
||
900 | } |
||
901 | } else { |
||
902 | $this->logger->debug('FORM: Merging contacts '.$lead->getId().' and '.$foundLead->getId()); |
||
903 | |||
904 | // Merge the found lead with currently tracked lead |
||
905 | $lead = $this->leadModel->mergeLeads($lead, $foundLead); |
||
0 ignored issues
–
show
|
|||
906 | } |
||
907 | |||
908 | // Update unique fields data for comparison with submitted data |
||
909 | $currentFields = $lead->getProfileFields(); |
||
910 | $uniqueFieldsCurrent = $getData($currentFields, true); |
||
911 | } |
||
912 | |||
913 | if (!$inKioskMode) { |
||
914 | // Check for conflicts with the submitted data and the currently tracked lead |
||
915 | list($hasConflict, $conflicts) = $checkForIdentifierConflict($uniqueFieldsWithData, $uniqueFieldsCurrent); |
||
916 | |||
917 | $this->logger->debug( |
||
918 | 'FORM: Current unique contact fields '.implode(', ', array_keys($uniqueFieldsCurrent)).' = '.implode(', ', $uniqueFieldsCurrent) |
||
919 | ); |
||
920 | |||
921 | $this->logger->debug( |
||
922 | 'FORM: Submitted unique contact fields '.implode(', ', array_keys($uniqueFieldsWithData)).' = '.implode(', ', $uniqueFieldsWithData) |
||
923 | ); |
||
924 | if ($hasConflict) { |
||
925 | // There's a conflict so create a new lead |
||
926 | $lead = new Lead(); |
||
927 | $lead->setNewlyCreated(true); |
||
928 | |||
929 | $this->logger->debug( |
||
930 | 'FORM: Conflicts found in '.implode(', ', $conflicts) |
||
931 | .' between current tracked contact and submitted data so assuming a new contact' |
||
932 | ); |
||
933 | } |
||
934 | } |
||
935 | |||
936 | //check for existing IP address |
||
937 | $ipAddress = $this->ipLookupHelper->getIpAddress(); |
||
938 | |||
939 | //no lead was found by a mapped email field so create a new one |
||
940 | if ($lead->isNewlyCreated()) { |
||
941 | if (!$inKioskMode) { |
||
942 | $lead->addIpAddress($ipAddress); |
||
943 | $this->logger->debug('FORM: Associating '.$ipAddress->getIpAddress().' to contact'); |
||
944 | } |
||
945 | } elseif (!$inKioskMode) { |
||
946 | $leadIpAddresses = $lead->getIpAddresses(); |
||
947 | if (!$leadIpAddresses->contains($ipAddress)) { |
||
948 | $lead->addIpAddress($ipAddress); |
||
949 | |||
950 | $this->logger->debug('FORM: Associating '.$ipAddress->getIpAddress().' to contact'); |
||
951 | } |
||
952 | } |
||
953 | |||
954 | //set the mapped fields |
||
955 | $this->leadModel->setFieldValues($lead, $data, false, true, true); |
||
956 | |||
957 | // last active time |
||
958 | $lead->setLastActive(new \DateTime()); |
||
959 | |||
960 | //create a new lead |
||
961 | $lead->setManipulator(new LeadManipulator( |
||
962 | 'form', |
||
963 | 'submission', |
||
964 | $form->getId(), |
||
965 | $form->getName() |
||
966 | )); |
||
967 | $this->leadModel->saveEntity($lead, false); |
||
968 | |||
969 | if (!$inKioskMode) { |
||
970 | // Set the current lead which will generate tracking cookies |
||
971 | $this->contactTracker->setTrackedContact($lead); |
||
972 | } else { |
||
973 | // Set system current lead which will still allow execution of events without generating tracking cookies |
||
974 | $this->contactTracker->setSystemContact($lead); |
||
975 | } |
||
976 | |||
977 | $companyFieldMatches = $getCompanyData($leadFieldMatches); |
||
978 | if (!empty($companyFieldMatches)) { |
||
979 | list($company, $leadAdded, $companyEntity) = IdentifyCompanyHelper::identifyLeadsCompany($companyFieldMatches, $lead, $this->companyModel); |
||
980 | if ($leadAdded) { |
||
981 | $lead->addCompanyChangeLogEntry('form', 'Identify Company', 'Lead added to the company, '.$company['companyname'], $company['id']); |
||
982 | } elseif ($companyEntity instanceof Company) { |
||
983 | $this->companyModel->setFieldValues($companyEntity, $companyFieldMatches); |
||
984 | $this->companyModel->saveEntity($companyEntity); |
||
985 | } |
||
986 | |||
987 | if (!empty($company) and $companyEntity instanceof Company) { |
||
988 | // Save after the lead in for new leads created through the API and maybe other places |
||
989 | $this->companyModel->addLeadToCompany($companyEntity, $lead); |
||
990 | $this->leadModel->setPrimaryCompany($companyEntity->getId(), $lead->getId()); |
||
991 | } |
||
992 | $this->em->clear(CompanyChangeLog::class); |
||
993 | } |
||
994 | |||
995 | return $lead; |
||
996 | } |
||
997 | |||
998 | /** |
||
999 | * Validates a field value. |
||
1000 | * |
||
1001 | * @param $value |
||
1002 | * |
||
1003 | * @return bool|string True if valid; otherwise string with invalid reason |
||
1004 | */ |
||
1005 | protected function validateFieldValue(Field $field, $value) |
||
1006 | { |
||
1007 | $standardValidation = $this->fieldHelper->validateFieldValue($field->getType(), $value, $field); |
||
1008 | if (!empty($standardValidation)) { |
||
1009 | return $standardValidation; |
||
1010 | } |
||
1011 | |||
1012 | $components = $this->formModel->getCustomComponents(); |
||
1013 | foreach ([$field->getType(), 'form'] as $type) { |
||
1014 | if (isset($components['validators'][$type])) { |
||
1015 | if (!is_array($components['validators'][$type])) { |
||
1016 | $components['validators'][$type] = [$components['validators'][$type]]; |
||
1017 | } |
||
1018 | foreach ($components['validators'][$type] as $validator) { |
||
1019 | if (!is_array($validator)) { |
||
1020 | $validator = ['eventName' => $validator]; |
||
1021 | } |
||
1022 | $event = $this->dispatcher->dispatch($validator['eventName'], new ValidationEvent($field, $value)); |
||
1023 | if (!$event->isValid()) { |
||
1024 | return $event->getInvalidReason(); |
||
1025 | } |
||
1026 | } |
||
1027 | } |
||
1028 | } |
||
1029 | |||
1030 | return true; |
||
1031 | } |
||
1032 | } |
||
1033 |
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.