/** * @param MauticEvents\CommandListEvent $event */ public function onBuildCommandList(MauticEvents\CommandListEvent $event) { $security = $this->security; if ($security->isGranted('campaign:campaigns:view')) { $event->addCommands('mautic.campaign.campaigns', $this->campaignModel->getCommandList()); } }
/** * Compile events for the lead timeline * * @param LeadTimelineEvent $event */ public function onTimelineGenerate(LeadTimelineEvent $event) { // Set available event types $eventTypeKey = 'campaign.event'; $eventTypeName = $this->translator->trans('mautic.campaign.triggered'); $event->addEventType($eventTypeKey, $eventTypeName); // Decide if those events are filtered if (!$event->isApplicable($eventTypeKey)) { return; } $lead = $event->getLead(); /** @var \Mautic\CampaignBundle\Entity\LeadEventLogRepository $logRepository */ $logRepository = $this->em->getRepository('MauticCampaignBundle:LeadEventLog'); $logs = $logRepository->getLeadLogs($lead->getId(), $event->getQueryOptions()); $eventSettings = $this->campaignModel->getEvents(); // Add total number to counter $event->addToCounter($eventTypeKey, $logs); if (!$event->isEngagementCount()) { foreach ($logs['results'] as $log) { // Hide this from the time line all together if (!empty($log['metadata']['failed'])) { $event->subtractFromCounter($eventTypeKey); continue; } $template = !empty($eventSettings['action'][$log['type']]['timelineTemplate']) ? $eventSettings['action'][$log['type']]['timelineTemplate'] : 'MauticCampaignBundle:SubscribedEvents\\Timeline:index.html.php'; $event->addEvent(['event' => $eventTypeKey, 'eventLabel' => ['label' => $log['event_name'] . ' / ' . $log['campaign_name'], 'href' => $this->router->generate('mautic_campaign_action', ['objectAction' => 'view', 'objectId' => $log['campaign_id']])], 'eventType' => $eventTypeName, 'timestamp' => $log['dateTriggered'], 'extra' => ['log' => $log], 'contentTemplate' => $template, 'icon' => 'fa-clock-o']); } } }
/** * Set a widget detail when needed. * * @param WidgetDetailEvent $event */ public function onWidgetDetailGenerate(WidgetDetailEvent $event) { $this->checkPermissions($event); $canViewOthers = $event->hasPermission('campaign:campaigns:viewother'); if ($event->getType() == 'events.in.time') { $widget = $event->getWidget(); $params = $widget->getParams(); if (!$event->isCached()) { $event->setTemplateData(['chartType' => 'line', 'chartHeight' => $widget->getHeight() - 80, 'chartData' => $this->campaignEventModel->getEventLineChartData($params['timeUnit'], $params['dateFrom'], $params['dateTo'], $params['dateFormat'], $canViewOthers)]); } $event->setTemplate('MauticCoreBundle:Helper:chart.html.php'); $event->stopPropagation(); } if ($event->getType() == 'leads.added.in.time') { $widget = $event->getWidget(); $params = $widget->getParams(); if (!$event->isCached()) { $event->setTemplateData(['chartType' => 'line', 'chartHeight' => $widget->getHeight() - 80, 'chartData' => $this->campaignModel->getLeadsAddedLineChartData($params['timeUnit'], $params['dateFrom'], $params['dateTo'], $params['dateFormat'], $canViewOthers)]); } $event->setTemplate('MauticCoreBundle:Helper:chart.html.php'); $event->stopPropagation(); } }
/** * @param ReportBuilderEvent $event * @param array $columns */ private function injectAttributionReportData(ReportBuilderEvent $event, array $columns, $type) { $attributionColumns = ['log.campaign_id' => ['label' => 'mautic.lead.report.attribution.campaign_id', 'type' => 'int', 'link' => 'mautic_campaign_action'], 'log.date_triggered' => ['label' => 'mautic.lead.report.attribution.action_date', 'type' => 'datetime'], 'c.name' => ['alias' => 'campaign_name', 'label' => 'mautic.lead.report.attribution.campaign_name', 'type' => 'string'], 'l.stage_id' => ['label' => 'mautic.lead.report.attribution.stage_id', 'type' => 'int', 'link' => 'mautic_stage_action'], 's.name' => ['alias' => 'stage_name', 'label' => 'mautic.lead.report.attribution.stage_name', 'type' => 'string'], 'channel' => ['alias' => 'channel', 'formula' => 'SUBSTRING_INDEX(e.type, \'.\', 1)', 'label' => 'mautic.lead.report.attribution.channel', 'type' => 'string'], 'channel_action' => ['alias' => 'channel_action', 'formula' => 'SUBSTRING_INDEX(e.type, \'.\', -1)', 'label' => 'mautic.lead.report.attribution.channel_action', 'type' => 'string'], 'e.name' => ['alias' => 'action_name', 'label' => 'mautic.lead.report.attribution.action_name', 'type' => 'string']]; $filters = $columns = array_merge($columns, $event->getCategoryColumns('cat.'), $attributionColumns); // Setup available channels $availableChannels = $this->campaignModel->getEvents(); $channels = []; $channelActions = []; foreach ($availableChannels['decision'] as $channel => $decision) { $parts = explode('.', $channel); $channelName = $parts[0]; $channels[$channelName] = $this->translator->hasId('mautic.channel.' . $channelName) ? $this->translator->trans('mautic.channel.' . $channelName) : ucfirst($channelName); unset($parts[0]); $actionValue = implode('.', $parts); if ($this->translator->hasId('mautic.channel.action.' . $channel)) { $actionName = $this->translator->trans('mautic.channel.action.' . $channel); } elseif ($this->translator->hasId('mautic.campaign.' . $channel)) { $actionName = $this->translator->trans('mautic.campaign.' . $channel); } else { $actionName = $channelName . ': ' . $actionValue; } $channelActions[$actionValue] = $actionName; } $filters['channel'] = ['label' => 'mautic.lead.report.attribution.channel', 'type' => 'select', 'list' => $channels]; $filters['channel_action'] = ['label' => 'mautic.lead.report.attribution.channel_action', 'type' => 'select', 'list' => $channelActions]; $this->channelActions = $channelActions; $this->channels = $channels; unset($channelActions, $channels); // Setup available channels $campaigns = $this->campaignModel->getRepository()->getSimpleList(); $filters['log.campaign_id'] = ['label' => 'mautic.lead.report.attribution.filter.campaign', 'type' => 'select', 'list' => $campaigns]; unset($campaigns); // Setup stages list $userStages = $this->stageModel->getUserStages(); $stages = []; foreach ($userStages as $stage) { $stages[$stage['id']] = $stage['name']; } $filters['l.stage_id'] = ['label' => 'mautic.lead.report.attribution.filter.stage', 'type' => 'select', 'list' => $stages]; unset($stages); $context = "contact.attribution.{$type}"; $event->addGraph($context, 'pie', 'mautic.lead.graph.pie.attribution_stages')->addGraph($context, 'pie', 'mautic.lead.graph.pie.attribution_campaigns')->addGraph($context, 'pie', 'mautic.lead.graph.pie.attribution_actions')->addGraph($context, 'pie', 'mautic.lead.graph.pie.attribution_channels'); $data = ['display_name' => 'mautic.lead.report.attribution.' . $type, 'columns' => $columns, 'filters' => $filters]; $event->addTable($context, $data, 'contacts'); }
/** * @param $post * @param $server * @param Form $form * * @return bool|array */ public function saveSubmission($post, $server, Form $form, Request $request = null, $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 ($page != null) { $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 = []; /** @var Field $f */ foreach ($fields as $f) { $id = $f->getId(); $type = $f->getType(); $alias = $f->getAlias(); $value = isset($post[$alias]) ? $post[$alias] : ''; $fieldArray[$id] = ['id' => $id, 'type' => $type, 'alias' => $alias]; if ($type == 'captcha') { $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; } if ($f->isRequired() && empty($value)) { //field is required, but hidden from form because of 'ShowWhenValueExists' if ($f->getShowWhenValueExists() === false && !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 (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(['\\Mautic\\CoreBundle\\Helper\\InputHelper', $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'); } } // @deprecated - BC support; to be removed in 3.0 - be sure to remove support in FormBuilderEvent as well if (isset($params['valueConstraints']) && is_callable($params['valueConstraints'])) { $customErrors = call_user_func_array($params['valueConstraints'], [$f, $value]); if (!empty($customErrors)) { $validationErrors[$alias] = is_array($customErrors) ? implode('<br />', $customErrors) : $customErrors; } } } 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; } $leadField = $f->getLeadField(); if (!empty($leadField)) { $leadValue = $value; if (is_array($leadValue)) { // Multiselect lead fields store the values with bars $delimeter = 'multiselect' === $leadFields[$leadField]['type'] ? '|' : ', '; $leadValue = implode($delimeter, $leadValue); } $leadFieldMatches[$leadField] = $leadValue; } //convert array from checkbox groups and multiple selects if (is_array($value)) { $value = implode(', ', $value); } $tokens["{formfield={$alias}}"] = $value; //save the result if ($f->getSaveResult() !== false) { $results[$alias] = $value; } } // Set the results $submission->setResults($results); // Update the event $submissionEvent->setFields($fieldArray)->setTokens($tokens)->setResults($results)->setContactFieldMatches($leadFieldMatches); // @deprecated - BC support; to be removed in 3.0 - be sure to remove the validator option from addSubmitAction as well $this->validateActionCallbacks($submissionEvent, $validationErrors, $alias); //return errors if there any - this should be moved to right after foreach($fields) once validateActionCallbacks support is dropped if (!empty($validationErrors)) { return ['errors' => $validationErrors]; } // Create/update lead if (!empty($leadFieldMatches)) { $lead = $this->createLeadFromSubmit($form, $leadFieldMatches, $leadFields); $submission->setLead($lead); } // Get updated lead if applicable with tracking ID if ($form->isInKioskMode()) { $lead = $this->leadModel->getCurrentLead(); } else { list($lead, $trackingId, $generated) = $this->leadModel->getCurrentLead(true); //set tracking ID for stats purposes to determine unique hits $submission->setTrackingId($trackingId); } $submission->setLead($lead); // Save the submission $this->saveEntity($submission); // 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()]]; } if (!$form->isStandalone()) { // Find and add the lead to the associated campaigns $campaigns = $this->campaignModel->getCampaignsByForm($form); if (!empty($campaigns)) { foreach ($campaigns as $campaign) { $this->campaignModel->addLead($campaign, $lead); } } } if ($this->dispatcher->hasListeners(FormEvents::FORM_ON_SUBMIT)) { // Reset action config from executeFormActions() $submissionEvent->setActionConfig(null, []); // Dispatch to on submit listeners $this->dispatcher->dispatch(FormEvents::FORM_ON_SUBMIT, $submissionEvent); } //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; }
/** * Find and trigger the negative events, i.e. the events with a no decision path. * * @param Campaign $campaign * @param int $totalEventCount * @param int $limit * @param bool $max * @param OutputInterface $output * @param bool|false $returnCounts If true, returns array of counters * * @return int */ public function triggerNegativeEvents($campaign, &$totalEventCount = 0, $limit = 100, $max = false, OutputInterface $output = null, $returnCounts = false) { defined('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED') or define('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED', 1); $this->logger->debug('CAMPAIGN: Triggering negative events'); $campaignId = $campaign->getId(); $campaignName = $campaign->getName(); $repo = $this->getRepository(); $campaignRepo = $this->getCampaignRepository(); // Get events to avoid large number of joins $campaignEvents = $repo->getCampaignEvents($campaignId); // Get an array of events that are non-action based $nonActionEvents = []; $actionEvents = []; foreach ($campaignEvents as $id => $e) { if (!empty($e['decisionPath']) && !empty($e['parent_id']) && $campaignEvents[$e['parent_id']]['eventType'] != 'condition') { if ($e['decisionPath'] == 'no') { $nonActionEvents[$e['parent_id']][$id] = $e; } elseif ($e['decisionPath'] == 'yes') { $actionEvents[$e['parent_id']][] = $id; } } } $this->logger->debug('CAMPAIGN: Processing the children of the following events: ' . implode(', ', array_keys($nonActionEvents))); if (empty($nonActionEvents)) { // No non-action events associated with this campaign unset($campaignEvents); return 0; } // Get a count $leadCount = $campaignRepo->getCampaignLeadCount($campaignId); if ($output) { $output->writeln($this->translator->trans('mautic.campaign.trigger.lead_count_analyzed', ['%leads%' => $leadCount, '%batch%' => $limit])); } $start = $leadProcessedCount = $lastRoundPercentage = $executedEventCount = $evaluatedEventCount = $negativeExecutedCount = $negativeEvaluatedCount = 0; $nonActionEventCount = $leadCount * count($nonActionEvents); $eventSettings = $this->campaignModel->getEvents(); $maxCount = $max ? $max : $nonActionEventCount; // Try to save some memory gc_enable(); if ($leadCount) { if ($output) { $progress = ProgressBarHelper::init($output, $maxCount); $progress->start(); if ($max) { $progress->advance($totalEventCount); } } $sleepBatchCount = 0; $batchDebugCounter = 1; while ($start <= $leadCount) { $this->logger->debug('CAMPAIGN: Batch #' . $batchDebugCounter); // Get batched campaign ids $campaignLeads = $campaignRepo->getCampaignLeads($campaignId, $start, $limit, ['cl.lead_id, cl.date_added']); $campaignLeadIds = []; $campaignLeadDates = []; foreach ($campaignLeads as $r) { $campaignLeadIds[] = $r['lead_id']; $campaignLeadDates[$r['lead_id']] = $r['date_added']; } unset($campaignLeads); $this->logger->debug('CAMPAIGN: Processing the following contacts: ' . implode(', ', $campaignLeadIds)); foreach ($nonActionEvents as $parentId => $events) { // Just a check to ensure this is an appropriate action if ($campaignEvents[$parentId]['eventType'] == 'action') { $this->logger->debug('CAMPAIGN: Parent event ID #' . $parentId . ' is an action.'); continue; } // Get only leads who have had the action prior to the decision executed $grandParentId = $campaignEvents[$parentId]['parent_id']; // Get the lead log for this batch of leads limiting to those that have already triggered // the decision's parent and haven't executed this level in the path yet if ($grandParentId) { $this->logger->debug('CAMPAIGN: Checking for contacts based on grand parent execution.'); $leadLog = $repo->getEventLog($campaignId, $campaignLeadIds, [$grandParentId], array_keys($events), true); $applicableLeads = array_keys($leadLog); } else { $this->logger->debug('CAMPAIGN: Checking for contacts based on exclusion due to being at root level'); // The event has no grandparent (likely because the decision is first in the campaign) so find leads that HAVE // already executed the events in the root level and exclude them $havingEvents = isset($actionEvents[$parentId]) ? array_merge($actionEvents[$parentId], array_keys($events)) : array_keys($events); $leadLog = $repo->getEventLog($campaignId, $campaignLeadIds, $havingEvents); $unapplicableLeads = array_keys($leadLog); // Only use leads that are not applicable $applicableLeads = array_diff($campaignLeadIds, $unapplicableLeads); unset($unapplicableLeads); } if (empty($applicableLeads)) { $this->logger->debug('CAMPAIGN: No events are applicable'); continue; } $this->logger->debug('CAMPAIGN: These contacts have have not gone down the positive path: ' . implode(', ', $applicableLeads)); // Get the leads $leads = $this->leadModel->getEntities(['filter' => ['force' => [['column' => 'l.id', 'expr' => 'in', 'value' => $applicableLeads]]], 'orderBy' => 'l.id', 'orderByDir' => 'asc']); if (!count($leads)) { // Just a precaution in case non-existent leads are lingering in the campaign leads table $this->logger->debug('CAMPAIGN: No contact entities found.'); continue; } // Loop over the non-actions and determine if it has been processed for this lead $leadDebugCounter = 1; /** @var \Mautic\LeadBundle\Entity\Lead $lead */ foreach ($leads as $lead) { ++$negativeEvaluatedCount; // Set lead for listeners $this->leadModel->setSystemCurrentLead($lead); $this->logger->debug('CAMPAIGN: contact ID #' . $lead->getId() . '; #' . $leadDebugCounter . ' in batch #' . $batchDebugCounter); // Prevent path if lead has already gone down this path if (!isset($leadLog[$lead->getId()]) || !array_key_exists($parentId, $leadLog[$lead->getId()])) { // Get date to compare against $utcDateString = $grandParentId ? $leadLog[$lead->getId()][$grandParentId]['date_triggered'] : $campaignLeadDates[$lead->getId()]; // Convert to local DateTime $grandParentDate = (new DateTimeHelper($utcDateString))->getLocalDateTime(); // Non-decision has not taken place yet, so cycle over each associated action to see if timing is right $eventTiming = []; $executeAction = false; foreach ($events as $id => $e) { if ($sleepBatchCount == $limit) { // Keep CPU down $this->batchSleep(); $sleepBatchCount = 0; } else { ++$sleepBatchCount; } if (isset($leadLog[$lead->getId()]) && array_key_exists($id, $leadLog[$lead->getId()])) { $this->logger->debug('CAMPAIGN: Event (ID #' . $id . ') has already been executed'); unset($e); continue; } if (!isset($eventSettings[$e['eventType']][$e['type']])) { $this->logger->debug('CAMPAIGN: Event (ID #' . $id . ') no longer exists'); unset($e); continue; } // First get the timing for all the 'non-decision' actions $eventTiming[$id] = $this->checkEventTiming($e, $grandParentDate, true); if ($eventTiming[$id] === true) { // Includes events to be executed now then schedule the rest if applicable $executeAction = true; } unset($e); } if (!$executeAction) { $negativeEvaluatedCount += count($nonActionEvents); // Timing is not appropriate so move on to next lead unset($eventTiming); continue; } if ($max && $totalEventCount + count($nonActionEvents) >= $max) { // Hit the max or will hit the max while mid-process for the lead if ($output) { $progress->finish(); $output->writeln(''); } $counts = ['events' => $nonActionEventCount, 'evaluated' => $negativeEvaluatedCount, 'executed' => $negativeExecutedCount, 'totalEvaluated' => $evaluatedEventCount, 'totalExecuted' => $executedEventCount]; $this->logger->debug('CAMPAIGN: Counts - ' . var_export($counts, true)); return $returnCounts ? $counts : $executedEventCount; } $decisionLogged = false; // Execute or schedule events $this->logger->debug('CAMPAIGN: Processing the following events for contact ID# ' . $lead->getId() . ': ' . implode(', ', array_keys($eventTiming))); foreach ($eventTiming as $id => $eventTriggerDate) { // Set event $event = $events[$id]; $event['campaign'] = ['id' => $campaignId, 'name' => $campaignName]; // Set lead in case this is triggered by the system $this->leadModel->setSystemCurrentLead($lead); if ($this->executeEvent($event, $campaign, $lead, $eventSettings, false, null, $eventTriggerDate, false, $evaluatedEventCount, $executedEventCount, $totalEventCount)) { if (!$decisionLogged) { // Log the decision $log = $this->getLogEntity($parentId, $campaign, $lead, null, true); $log->setDateTriggered(new \DateTime()); $log->setNonActionPathTaken(true); $repo->saveEntity($log); $this->em->detach($log); unset($log); $decisionLogged = true; } ++$negativeExecutedCount; } unset($utcDateString, $grandParentDate); } } else { $this->logger->debug('CAMPAIGN: Decision has already been executed.'); } $currentCount = $max ? $totalEventCount : $negativeEvaluatedCount; if ($output && $currentCount < $maxCount) { $progress->setProgress($currentCount); } ++$leadDebugCounter; // Save RAM $this->em->detach($lead); unset($lead); } } // Next batch $start += $limit; // Save RAM $this->em->clear('Mautic\\LeadBundle\\Entity\\Lead'); $this->em->clear('Mautic\\UserBundle\\Entity\\User'); unset($leads, $campaignLeadIds, $leadLog); $currentCount = $max ? $totalEventCount : $negativeEvaluatedCount; if ($output && $currentCount < $maxCount) { $progress->setProgress($currentCount); } // Free some memory gc_collect_cycles(); ++$batchDebugCounter; } if ($output) { $progress->finish(); $output->writeln(''); } } $counts = ['events' => $nonActionEventCount, 'evaluated' => $negativeEvaluatedCount, 'executed' => $negativeExecutedCount, 'totalEvaluated' => $evaluatedEventCount, 'totalExecuted' => $executedEventCount]; $this->logger->debug('CAMPAIGN: Counts - ' . var_export($counts, true)); return $returnCounts ? $counts : $executedEventCount; }
/** * @param $post * @param $server * @param Form $form * * @return boolean|string false if no error was encountered; otherwise the error message */ public function saveSubmission($post, $server, Form $form) { $fieldHelper = new FormFieldHelper($this->translator); //everything matches up so let's save the results $submission = new Submission(); $submission->setDateSubmitted(new \DateTime()); $submission->setForm($form); $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, array('mauticError', 'mauticMessage')); $submission->setReferer($referer); $fields = $form->getFields(); $fieldArray = array(); $results = array(); $tokens = array(); $leadFieldMatches = array(); $validationErrors = array(); foreach ($fields as $f) { $id = $f->getId(); $type = $f->getType(); $alias = $f->getAlias(); $value = isset($post[$alias]) ? $post[$alias] : ''; $fieldArray[$id] = array('id' => $id, 'type' => $type, 'alias' => $alias); if (in_array($type, array('button', 'freetext'))) { //don't save items that don't have a value associated with it continue; } elseif ($type == 'captcha') { $captcha = $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; } if ($f->isRequired() && empty($value)) { //somehow the user got passed the JS validation $msg = $f->getValidationMessage(); if (empty($msg)) { $msg = $this->translator->trans('mautic.form.field.generic.validationfailed', array('%label%' => $f->getLabel()), 'validators'); } $validationErrors[$alias] = $msg; continue; } //clean and validate the input if ($f->isCustom()) { $params = $f->getCustomParameters(); if (!empty($value)) { if (isset($params['valueFilter'])) { if (is_string($params['inputFilter'] && method_exists('\\Mautic\\CoreBundle\\Helper\\InputHelper', $params['valueFilter']))) { $value = InputHelper::_($value, $params['valueFilter']); } elseif (is_callable($params['valueFilter'])) { $value = call_user_func_array($params['valueFilter'], array($f, $value)); } else { $value = InputHelper::_($value, 'clean'); } } else { $value = InputHelper::_($value, 'clean'); } } if (isset($params['valueConstraints']) && is_callable($params['valueConstraints'])) { $customErrors = call_user_func_array($params['valueConstraints'], array($f, $value)); if (!empty($customErrors)) { $validationErrors[$alias] = is_array($customErrors) ? implode('<br />', $customErrors) : $customErrors; } } } elseif (!empty($value)) { $filter = $fieldHelper->getFieldFilter($type); $value = InputHelper::_($value, $filter); $validation = $fieldHelper->validateFieldValue($type, $value); if (!empty($validation)) { $validationErrors[$alias] = is_array($validation) ? implode('<br />', $validation) : $validation; } } //convert array from checkbox groups and multiple selects if (is_array($value)) { $value = implode(", ", $value); } $tokens["{formfield={$alias}}"] = $value; //save the result if ($f->getSaveResult() !== false) { $results[$alias] = $value; } $leadField = $f->getLeadField(); if (!empty($leadField)) { $leadFieldMatches[$leadField] = $value; } } $submission->setResults($results); //execute submit actions $actions = $form->getActions(); //get post submit actions to make sure it still exists $components = $this->formModel->getCustomComponents(); $availableActions = $components['actions']; $args = array('post' => $post, 'server' => $server, 'factory' => $this->factory, 'submission' => $submission, 'fields' => $fieldArray, 'form' => $form, 'tokens' => $tokens); foreach ($actions as $action) { $key = $action->getType(); if (!isset($availableActions[$key])) { continue; } $settings = $availableActions[$key]; $args['action'] = $action; $args['config'] = $action->getProperties(); if (array_key_exists('validator', $settings)) { $callback = $settings['validator']; if (is_callable($callback)) { if (is_array($callback)) { $reflection = new \ReflectionMethod($callback[0], $callback[1]); } elseif (strpos($callback, '::') !== false) { $parts = explode('::', $callback); $reflection = new \ReflectionMethod($parts[0], $parts[1]); } else { $reflection = new \ReflectionMethod(null, $callback); } $pass = array(); foreach ($reflection->getParameters() as $param) { if (isset($args[$param->getName()])) { $pass[] = $args[$param->getName()]; } else { $pass[] = null; } } list($validated, $validatedMessage) = $reflection->invokeArgs($this, $pass); if (!$validated) { $validationErrors[$alias] = $validatedMessage; } } } } //return errors if (!empty($validationErrors)) { return array('errors' => $validationErrors); } //set the landing page the form was submitted from if applicable if (!empty($post['mauticpage'])) { $page = $this->pageModel->getEntity((int) $post['mauticpage']); if ($page != null) { $submission->setPage($page); } } // Add a feedback parameter $args['feedback'] = array(); // Create/update lead if (!empty($leadFieldMatches)) { $this->createLeadFromSubmit($form, $leadFieldMatches); } if ($form->isStandalone()) { // Now handle post submission actions foreach ($actions as $action) { $key = $action->getType(); if (!isset($availableActions[$key])) { continue; } $settings = $availableActions[$key]; $args['action'] = $action; $args['config'] = $action->getProperties(); // Set the lead each time in case an action updates it $args['lead'] = $this->leadModel->getCurrentLead(); $callback = $settings['callback']; if (is_callable($callback)) { if (is_array($callback)) { $reflection = new \ReflectionMethod($callback[0], $callback[1]); } elseif (strpos($callback, '::') !== false) { $parts = explode('::', $callback); $reflection = new \ReflectionMethod($parts[0], $parts[1]); } else { $reflection = new \ReflectionMethod(null, $callback); } $pass = array(); foreach ($reflection->getParameters() as $param) { if (isset($args[$param->getName()])) { $pass[] = $args[$param->getName()]; } else { $pass[] = null; } } $returned = $reflection->invokeArgs($this, $pass); $args['feedback'][$key] = $returned; } } } // Get updated lead with tracking ID if ($form->isInKioskMode()) { $lead = $this->leadModel->getCurrentLead(); } else { list($lead, $trackingId, $generated) = $this->leadModel->getCurrentLead(true); //set tracking ID for stats purposes to determine unique hits $submission->setTrackingId($trackingId); } $submission->setLead($lead); if (!$form->isStandalone()) { // Find and add the lead to the associated campaigns $campaigns = $this->campaignModel->getCampaignsByForm($form); if (!empty($campaigns)) { foreach ($campaigns as $campaign) { $this->campaignModel->addLead($campaign, $lead); } } } //save entity after the form submission events are fired in case a new lead is created $this->saveEntity($submission); if ($this->dispatcher->hasListeners(FormEvents::FORM_ON_SUBMIT)) { $event = new SubmissionEvent($submission, $post, $server); $this->dispatcher->dispatch(FormEvents::FORM_ON_SUBMIT, $event); } //last round of callback commands from the submit actions; first come first serve foreach ($args['feedback'] as $k => $data) { if (!empty($data['callback'])) { return array('callback' => $data); } } //made it to the end so return false that there was not an error return false; }