Initial commit: CloudOps infrastructure platform

This commit is contained in:
root
2026-04-09 19:58:57 +02:00
commit 1166a52f26
7762 changed files with 839452 additions and 0 deletions

View File

@@ -0,0 +1,144 @@
<?php
namespace Mautic\CampaignBundle\Controller;
use Doctrine\Persistence\ManagerRegistry;
use Mautic\CampaignBundle\Entity\LeadEventLog;
use Mautic\CampaignBundle\Model\EventLogModel;
use Mautic\CoreBundle\Controller\AjaxController as CommonAjaxController;
use Mautic\CoreBundle\Factory\ModelFactory;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\InputHelper;
use Mautic\CoreBundle\Helper\UserHelper;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\CoreBundle\Service\FlashBag;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\CoreBundle\Twig\Helper\DateHelper;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
class AjaxController extends CommonAjaxController
{
public function __construct(
private DateHelper $dateHelper,
ManagerRegistry $doctrine,
ModelFactory $modelFactory,
UserHelper $userHelper,
CoreParametersHelper $coreParametersHelper,
EventDispatcherInterface $dispatcher,
Translator $translator,
FlashBag $flashBag,
RequestStack $requestStack,
CorePermissions $security,
) {
parent::__construct($doctrine, $modelFactory, $userHelper, $coreParametersHelper, $dispatcher, $translator, $flashBag, $requestStack, $security);
}
public function updateConnectionsAction(Request $request): \Symfony\Component\HttpFoundation\JsonResponse
{
$session = $request->getSession();
$campaignId = InputHelper::clean($request->query->get('campaignId'));
$canvasSettings = $request->request->all()['canvasSettings'] ?? [];
if (empty($campaignId)) {
$dataArray = ['success' => 0];
} else {
$session->set('mautic.campaign.'.$campaignId.'.events.canvassettings', $canvasSettings);
$dataArray = ['success' => 1];
}
return $this->sendJsonResponse($dataArray);
}
public function updateScheduledCampaignEventAction(Request $request): \Symfony\Component\HttpFoundation\JsonResponse
{
$eventId = (int) $request->request->get('eventId');
$contactId = (int) $request->request->get('contactId');
$newDate = InputHelper::clean($request->request->get('date'));
$originalDate = InputHelper::clean($request->request->get('originalDate'));
$dataArray = ['success' => 0, 'date' => $originalDate];
if (!empty($eventId) && !empty($contactId) && !empty($newDate)) {
if ($log = $this->getContactEventLog($eventId, $contactId)) {
$newDate = new \DateTime($newDate);
if ($newDate >= new \DateTime()) {
$log->setTriggerDate($newDate);
/** @var EventLogModel $logModel */
$logModel = $this->getModel('campaign.event_log');
$logModel->saveEntity($log);
$dataArray = [
'success' => 1,
'date' => $newDate->format('Y-m-d H:i:s'),
];
}
}
}
// Format the date to match the view
$dataArray['formattedDate'] = $this->dateHelper->toFull($dataArray['date']);
return $this->sendJsonResponse($dataArray);
}
public function cancelScheduledCampaignEventAction(Request $request): \Symfony\Component\HttpFoundation\JsonResponse
{
$dataArray = ['success' => 0];
$eventId = (int) $request->request->get('eventId');
$contactId = (int) $request->request->get('contactId');
if (!empty($eventId) && !empty($contactId)) {
if ($log = $this->getContactEventLog($eventId, $contactId)) {
$log->setIsScheduled(false);
/** @var EventLogModel $logModel */
$logModel = $this->getModel('campaign.event_log');
$metadata = $log->getMetadata();
$metadata['errors'] = $this->translator->trans(
'mautic.campaign.event.cancelled.time',
['%date%' => $log->getTriggerDate()->format('Y-m-d H:i:s')]
);
$log->setMetadata($metadata);
$logModel->getRepository()->saveEntity($log);
$dataArray = ['success' => 1];
}
}
return $this->sendJsonResponse($dataArray);
}
/**
* @return LeadEventLog|null
*/
protected function getContactEventLog($eventId, $contactId)
{
$contact = $this->getModel('lead')->getEntity($contactId);
if ($contact) {
if ($this->security->hasEntityAccess('lead:leads:editown', 'lead:leads:editother', $contact->getPermissionUser())) {
/** @var EventLogModel $logModel */
$logModel = $this->getModel('campaign.event_log');
/** @var LeadEventLog $log */
$log = $logModel->getRepository()
->findOneBy(
[
'lead' => $contactId,
'event' => $eventId,
],
['dateTriggered' => 'desc']
);
if ($log && ($log->getTriggerDate() > new \DateTime())) {
return $log;
}
}
}
return null;
}
}

View File

@@ -0,0 +1,463 @@
<?php
namespace Mautic\CampaignBundle\Controller\Api;
use Doctrine\Persistence\ManagerRegistry;
use Mautic\ApiBundle\Controller\CommonApiController;
use Mautic\ApiBundle\Helper\EntityResultHelper;
use Mautic\CampaignBundle\Entity\Campaign;
use Mautic\CampaignBundle\Entity\Event;
use Mautic\CampaignBundle\Membership\MembershipManager;
use Mautic\CampaignBundle\Model\CampaignModel;
use Mautic\CampaignBundle\Model\EventModel;
use Mautic\CoreBundle\Event\EntityExportEvent;
use Mautic\CoreBundle\Event\EntityImportEvent;
use Mautic\CoreBundle\Factory\ModelFactory;
use Mautic\CoreBundle\Helper\AppVersion;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\ImportHelper;
use Mautic\CoreBundle\Helper\InputHelper;
use Mautic\CoreBundle\Helper\UserHelper;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\LeadBundle\Controller\LeadAccessTrait;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* @extends CommonApiController<Campaign>
*/
class CampaignApiController extends CommonApiController
{
use LeadAccessTrait;
/**
* @var CampaignModel|null
*/
protected $model;
public function __construct(
CorePermissions $security,
Translator $translator,
EntityResultHelper $entityResultHelper,
RouterInterface $router,
FormFactoryInterface $formFactory,
AppVersion $appVersion,
private RequestStack $requestStack,
private MembershipManager $membershipManager,
ManagerRegistry $doctrine,
ModelFactory $modelFactory,
EventDispatcherInterface $dispatcher,
CoreParametersHelper $coreParametersHelper,
private ValidatorInterface $validator,
private EventModel $eventModel,
) {
$campaignModel = $modelFactory->getModel('campaign');
\assert($campaignModel instanceof CampaignModel);
$this->model = $campaignModel;
$this->entityClass = Campaign::class;
$this->entityNameOne = 'campaign';
$this->entityNameMulti = 'campaigns';
$this->permissionBase = 'campaign:campaigns';
$this->serializerGroups = [
'campaignDetails',
'campaignEventDetails',
'categoryList',
'publishDetails',
'leadListList',
'formList',
];
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
}
/**
* Adds a lead to a campaign.
*
* @param int $id Campaign ID
* @param int $leadId Lead ID
*
* @return Response
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function addLeadAction($id, $leadId)
{
$entity = $this->model->getEntity($id);
if (null !== $entity) {
$leadModel = $this->getModel('lead');
$lead = $leadModel->getEntity($leadId);
if (null == $lead) {
return $this->notFound();
} elseif (!$this->security->hasEntityAccess('lead:leads:editown', 'lead:leads:editother', $lead->getOwner())) {
return $this->accessDenied();
}
$this->membershipManager->addContact($lead, $entity);
$view = $this->view(['success' => 1], Response::HTTP_OK);
return $this->handleView($view);
}
return $this->notFound();
}
/**
* Removes given lead from a campaign.
*
* @param int $id Campaign ID
* @param int $leadId Lead ID
*
* @return Response
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function removeLeadAction($id, $leadId)
{
$entity = $this->model->getEntity($id);
if (null !== $entity) {
$lead = $this->checkLeadAccess($leadId, 'edit');
if ($lead instanceof Response) {
return $lead;
}
$this->membershipManager->removeContact($lead, $entity);
$view = $this->view(['success' => 1], Response::HTTP_OK);
return $this->handleView($view);
}
return $this->notFound();
}
/**
* @param Campaign &$entity
* @param string $action
*/
protected function preSaveEntity(&$entity, $form, $parameters, $action = 'edit')
{
$method = $this->requestStack->getCurrentRequest()->getMethod();
if ('POST' === $method || 'PUT' === $method) {
if (empty($parameters['events'])) {
$msg = $this->translator->trans('mautic.campaign.form.events.notempty', [], 'validators');
return $this->returnError($msg, Response::HTTP_BAD_REQUEST);
} elseif (empty($parameters['lists']) && empty($parameters['forms'])) {
$msg = $this->translator->trans('mautic.campaign.form.sources.notempty', [], 'validators');
return $this->returnError($msg, Response::HTTP_BAD_REQUEST);
}
}
$deletedSources = ['lists' => [], 'forms' => []];
$deletedEvents = [];
$currentSources = [
'lists' => isset($parameters['lists']) ? $this->modifyCampaignEventArray($parameters['lists']) : [],
'forms' => isset($parameters['forms']) ? $this->modifyCampaignEventArray($parameters['forms']) : [],
];
// delete events and sources which does not exist in the PUT request
if ('PUT' === $method) {
$requestEventIds = [];
$requestSegmentIds = [];
$requestFormIds = [];
foreach ($parameters['events'] as $key => $requestEvent) {
if (!isset($requestEvent['id'])) {
return $this->returnError('$campaign[events]['.$key.']["id"] is missing', Response::HTTP_BAD_REQUEST);
}
$requestEventIds[] = $requestEvent['id'];
}
foreach ($entity->getEvents() as $currentEvent) {
if (!in_array($currentEvent->getId(), $requestEventIds)) {
$deletedEvents[] = $currentEvent->getId();
}
}
if (isset($parameters['lists'])) {
foreach ($parameters['lists'] as $requestSegment) {
if (!isset($requestSegment['id'])) {
return $this->returnError('$campaign[lists]['.$key.']["id"] is missing', Response::HTTP_BAD_REQUEST);
}
$requestSegmentIds[] = $requestSegment['id'];
}
}
foreach ($entity->getLists() as $currentSegment) {
if (!in_array($currentSegment->getId(), $requestSegmentIds)) {
$deletedSources['lists'][$currentSegment->getId()] = 'ignore';
}
}
if (isset($parameters['forms'])) {
foreach ($parameters['forms'] as $requestForm) {
if (!isset($requestForm['id'])) {
return $this->returnError('$campaign[forms]['.$key.']["id"] is missing', Response::HTTP_BAD_REQUEST);
}
$requestFormIds[] = $requestForm['id'];
}
}
foreach ($entity->getForms() as $currentForm) {
if (!in_array($currentForm->getId(), $requestFormIds)) {
$deletedSources['forms'][$currentForm->getId()] = 'ignore';
}
}
}
// Set lead sources
$this->model->setLeadSources($entity, $currentSources, $deletedSources);
// Build and set Event entities
if (isset($parameters['events']) && isset($parameters['canvasSettings'])) {
$this->model->setEvents($entity, $parameters['events'], $parameters['canvasSettings'], $deletedEvents);
}
/** @var array<ConstraintViolationListInterface<ConstraintViolationInterface>> $eventViolations */
$eventViolations = array_filter(
array_map(
fn (Event $event) => $this->validator->validate($event),
$entity->getEvents()->toArray()
),
fn ($error) => $error->count() > 0
);
if (count($eventViolations) > 0) {
$errors = [];
foreach ($eventViolations as $violationList) {
foreach ($violationList as $violation) {
\assert($violation instanceof ConstraintViolationInterface);
$errors[] = [
'code' => $violation->getCode(),
'message' => $violation->getMessage(),
'details' => $violation->getPropertyPath(),
'type' => 'validation',
];
}
}
$view = $this->view(['errors' => $errors], Response::HTTP_UNPROCESSABLE_ENTITY);
return $this->handleView($view);
}
// Persist to the database before building connection so that IDs are available
$this->model->saveEntity($entity);
// Update canvas settings with new event IDs then save
if (isset($parameters['canvasSettings'])) {
$this->model->setCanvasSettings($entity, $parameters['canvasSettings']);
}
if (Request::METHOD_PUT === $method && !empty($deletedEvents)) {
$this->eventModel->deleteEvents($entity->getEvents()->toArray(), $deletedEvents);
}
}
/**
* Change the array structure.
*
* @param array $events
*/
public function modifyCampaignEventArray($events): array
{
$updatedEvents = [];
if ($events && is_array($events)) {
foreach ($events as $event) {
if (!empty($event['id'])) {
$updatedEvents[$event['id']] = 'ignore';
}
}
}
return $updatedEvents;
}
/**
* Obtains a list of campaign contacts.
*
* @return Response
*/
public function getContactsAction(Request $request, $id)
{
$entity = $this->model->getEntity($id);
if (null === $entity) {
return $this->notFound();
}
if (!$this->checkEntityAccess($entity)) {
return $this->accessDenied();
}
$where = InputHelper::clean($request->query->get('where') ?? []);
$order = InputHelper::clean($request->query->get('order') ?? []);
$start = (int) $request->query->get('start', 0);
$limit = (int) $request->query->get('limit', 100);
$where[] = [
'col' => 'campaign_id',
'expr' => 'eq',
'val' => $id,
];
$where[] = [
'col' => 'manually_removed',
'expr' => 'eq',
'val' => 0,
];
return $this->forward(
'Mautic\CoreBundle\Controller\Api\StatsApiController::listAction',
[
'table' => 'campaign_leads',
'itemsName' => 'contacts',
'order' => $order,
'where' => $where,
'start' => $start,
'limit' => $limit,
]
);
}
public function cloneCampaignAction($campaignId)
{
if (empty($campaignId) || false == intval($campaignId)) {
return $this->notFound();
}
$original = $this->model->getEntity($campaignId);
if (empty($original)) {
return $this->notFound();
}
$entity = clone $original;
if (!$this->checkEntityAccess($entity, 'create')) {
return $this->accessDenied();
}
$this->model->saveEntity($entity);
$headers = [];
// return the newly created entities location if applicable
$route = 'mautic_api_campaigns_getone';
$headers['Location'] = $this->generateUrl(
$route,
array_merge(['id' => $entity->getId()], $this->routeParams),
true
);
$view = $this->view([$this->entityNameOne => $entity], Response::HTTP_OK, $headers);
$this->setSerializationContext($view);
return $this->handleView($view);
}
/**
* Get a list of events.
*/
public function exportCampaignAction(Request $request, int $campaignId): Response
{
// Check if the campaign exists
$campaign = $this->model->getEntity($campaignId);
if (!$campaign) {
return $this->notFound();
}
// Check if user has permission to export campaigns
if (!$this->security->isGranted('campaign:export:enable', 'MATCH_ONE')) {
return $this->accessDenied();
}
// Dispatch event to collect campaign data for export
$event = new EntityExportEvent(Campaign::ENTITY_NAME, $campaignId);
$this->dispatcher->dispatch($event);
$data = $event->getEntities();
// Prepare response
$view = $this->view([$data], Response::HTTP_OK);
$this->setSerializationContext($view);
return $this->handleView($view);
}
public function importCampaignAction(Request $request, UserHelper $userHelper, ImportHelper $importHelper): Response
{
// Check if user has permission to import campaigns
if (!$this->security->isGranted('campaign:imports:create')) {
return $this->accessDenied();
}
// Decode request JSON
$data = json_decode($request->getContent(), true);
if (!$data || !isset($data[0][Campaign::ENTITY_NAME])) {
$files = $request->files->all();
if (1 !== count($files)) {
return $this->handleView(
$this->view(['error' => $this->translator->trans('mautic.campaign.api.import.incorrect_zip_file', [], 'messages')], Response::HTTP_BAD_REQUEST)
);
}
$uploadedFile = array_values($files)[0];
if (!$uploadedFile->isValid()) {
return $this->handleView(
$this->view(['error' => $this->translator->trans('mautic.campaign.api.import.upload_failed', [], 'messages')], Response::HTTP_BAD_REQUEST)
);
}
if ('zip' !== strtolower($uploadedFile->getClientOriginalExtension())) {
return $this->handleView(
$this->view(['error' => $this->translator->trans('mautic.campaign.api.import.incorrect_upload_file_format', [], 'messages')], Response::HTTP_BAD_REQUEST)
);
}
$zipPath = $uploadedFile->getPathname();
if (!file_exists($zipPath)) {
return $this->handleView(
$this->view(['error' => $this->translator->trans('mautic.campaign.api.import.uploaded_file_no_exist', [], 'messages')], Response::HTTP_INTERNAL_SERVER_ERROR)
);
}
try {
$data = $importHelper->readZipFile($zipPath);
} catch (\RuntimeException $e) {
unlink($zipPath);
return $this->handleView(
$this->view(['error' => $e->getMessage()], Response::HTTP_BAD_REQUEST)
);
}
}
$importHelper->recursiveRemoveEmailaddress($data);
$userId = $userHelper->getUser()->getId();
foreach ($data as $entity) {
$event = new EntityImportEvent(Campaign::ENTITY_NAME, $entity, $userId);
$this->dispatcher->dispatch($event);
}
$view = $this->view([$this->translator->trans('mautic.campaign.campaign.import.finished', [], 'messages')], Response::HTTP_CREATED);
$this->setSerializationContext($view);
return $this->handleView($view);
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Mautic\CampaignBundle\Controller\Api;
use Doctrine\Persistence\ManagerRegistry;
use Mautic\ApiBundle\Controller\CommonApiController;
use Mautic\ApiBundle\Helper\EntityResultHelper;
use Mautic\ApiBundle\Serializer\Exclusion\FieldExclusionStrategy;
use Mautic\CampaignBundle\Entity\Event;
use Mautic\CampaignBundle\Model\EventModel;
use Mautic\CoreBundle\Entity\FormEntity;
use Mautic\CoreBundle\Factory\ModelFactory;
use Mautic\CoreBundle\Helper\AppVersion;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\LeadBundle\Controller\LeadAccessTrait;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\RouterInterface;
/**
* @extends CommonApiController<Event>
*/
class EventApiController extends CommonApiController
{
use LeadAccessTrait;
public function __construct(CorePermissions $security, Translator $translator, EntityResultHelper $entityResultHelper, RouterInterface $router, FormFactoryInterface $formFactory, AppVersion $appVersion, RequestStack $requestStack, ManagerRegistry $doctrine, ModelFactory $modelFactory, EventDispatcherInterface $dispatcher, CoreParametersHelper $coreParametersHelper)
{
$campaignEventModel = $modelFactory->getModel('campaign.event');
\assert($campaignEventModel instanceof EventModel);
$this->model = $campaignEventModel;
$this->entityClass = Event::class;
$this->entityNameOne = 'event';
$this->entityNameMulti = 'events';
$this->serializerGroups = ['campaignEventStandaloneDetails', 'campaignList'];
$this->parentChildrenLevelDepth = 1;
// Don't include campaign in children/parent arrays
$this->addExclusionStrategy(new FieldExclusionStrategy(['campaign'], 1));
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
}
/**
* @param Event|FormEntity $entity
*/
protected function checkEntityAccess($entity, $action = 'view')
{
// Use the campaign for permission checks
return parent::checkEntityAccess($entity->getCampaign(), $action);
}
}

View File

@@ -0,0 +1,276 @@
<?php
namespace Mautic\CampaignBundle\Controller\Api;
use Doctrine\Persistence\ManagerRegistry;
use FOS\RestBundle\View\View;
use Mautic\ApiBundle\Controller\FetchCommonApiController;
use Mautic\ApiBundle\Helper\EntityResultHelper;
use Mautic\ApiBundle\Serializer\Exclusion\FieldInclusionStrategy;
use Mautic\CampaignBundle\Entity\Campaign;
use Mautic\CampaignBundle\Entity\Event;
use Mautic\CampaignBundle\Entity\LeadEventLog;
use Mautic\CampaignBundle\Model\EventLogModel;
use Mautic\CampaignBundle\Model\EventModel;
use Mautic\CoreBundle\Factory\ModelFactory;
use Mautic\CoreBundle\Helper\AppVersion;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\UserHelper;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\LeadBundle\Controller\LeadAccessTrait;
use Mautic\LeadBundle\Entity\Lead;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
/**
* @extends FetchCommonApiController<LeadEventLog>
*/
class EventLogApiController extends FetchCommonApiController
{
use LeadAccessTrait;
private const LOG_SERIALIZATION = 30;
/**
* @var Campaign
*/
protected $campaign;
/**
* @var Lead
*/
protected $contact;
/**
* @var EventLogModel|null
*/
protected $model;
public function __construct(
CorePermissions $security,
Translator $translator,
EntityResultHelper $entityResultHelper,
AppVersion $appVersion,
RequestStack $requestStack,
ManagerRegistry $doctrine,
ModelFactory $modelFactory,
EventDispatcherInterface $dispatcher,
CoreParametersHelper $coreParametersHelper,
) {
$campaignEventLogModel = $modelFactory->getModel('campaign.event_log');
\assert($campaignEventLogModel instanceof EventLogModel);
$this->model = $campaignEventLogModel;
$this->entityClass = LeadEventLog::class;
$this->entityNameOne = 'event';
$this->entityNameMulti = 'events';
$this->parentChildrenLevelDepth = 1;
$this->serializerGroups = [
'campaignList',
'ipAddressList',
self::LOG_SERIALIZATION => 'campaignEventLogDetails',
];
// Only include the id of the parent
$this->addExclusionStrategy(new FieldInclusionStrategy(['id'], 1, 'parent'));
parent::__construct($security, $translator, $entityResultHelper, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
}
/**
* @return Response
*/
public function getEntitiesAction(Request $request, UserHelper $userHelper)
{
$this->serializerGroups[self::LOG_SERIALIZATION] = 'campaignEventStandaloneLogDetails';
$this->serializerGroups[] = 'campaignEventStandaloneList';
$this->serializerGroups[] = 'leadBasicList';
return parent::getEntitiesAction($request, $userHelper);
}
/**
* Get a list of events.
*
* @return Response
*/
public function getContactEventsAction(Request $request, UserHelper $userHelper, $contactId, $campaignId = null)
{
// Ensure contact exists and user has access
$contact = $this->checkLeadAccess($contactId, 'view');
if ($contact instanceof Response) {
return $contact;
}
// Ensure campaign exists and user has access
if (!empty($campaignId)) {
$campaign = $this->getModel('campaign')->getEntity($campaignId);
if (null == $campaign || !$campaign->getId()) {
return $this->notFound();
}
if (!$this->checkEntityAccess($campaign)) {
return $this->accessDenied();
}
// Check that contact is part of the campaign
$membership = $campaign->getContactMembership($contact);
if (0 === count($membership)) {
return $this->returnError(
$this->translator->trans(
'mautic.campaign.error.contact_not_in_campaign',
['%campaign%' => $campaignId, '%contact%' => $contactId]
),
Response::HTTP_CONFLICT
);
}
$this->campaign = $campaign;
$this->serializerGroups[] = 'campaignEventWithLogsList';
$this->serializerGroups[] = 'campaignLeadList';
} else {
unset($this->serializerGroups[self::LOG_SERIALIZATION]);
$this->serializerGroups[] = 'campaignEventStandaloneList';
$this->serializerGroups[] = 'campaignEventStandaloneLogDetails';
}
$this->contact = $contact;
$this->extraGetEntitiesArguments = [
'contact_id' => $contactId,
'campaign_id' => $campaignId,
];
return $this->getEntitiesAction($request, $userHelper);
}
/**
* @return Response
*/
public function editContactEventAction(Request $request, $eventId, $contactId)
{
$parameters = $request->request->all();
// Ensure contact exists and user has access
$contact = $this->checkLeadAccess($contactId, 'edit');
if ($contact instanceof Response) {
return $contact;
}
/** @var EventModel $eventModel */
$eventModel = $this->getModel('campaign.event');
/** @var Event $event */
$event = $eventModel->getEntity($eventId);
if (null === $event || !$event->getId()) {
return $this->notFound();
}
// Ensure campaign edit access
$campaign = $event->getCampaign();
if (!$this->checkEntityAccess($campaign, 'edit')) {
return $this->accessDenied();
}
$result = $this->model->updateContactEvent($event, $contact, $parameters);
if (is_string($result)) {
return $this->returnError($result, Response::HTTP_CONFLICT);
} else {
[$log, $created] = $result;
}
$event->addContactLog($log);
$view = $this->view(
[
$this->entityNameOne => $event,
],
($created) ? Response::HTTP_CREATED : Response::HTTP_OK
);
$this->serializerGroups[] = 'campaignEventWithLogsDetails';
$this->serializerGroups[] = 'campaignBasicList';
$this->setSerializationContext($view);
return $this->handleView($view);
}
/**
* @return array|Response
*/
public function editEventsAction(Request $request)
{
$parameters = $request->request->all();
$valid = $this->validateBatchPayload($parameters);
if ($valid instanceof Response) {
return $valid;
}
$events = $this->getBatchEntities($parameters, $errors, false, 'eventId', $this->getModel('campaign.event'), false);
$contacts = $this->getBatchEntities($parameters, $errors, false, 'contactId', $this->getModel('lead'), false);
$this->inBatchMode = true;
$errors = [];
foreach ($parameters as $key => $params) {
if (!isset($params['eventId']) || !isset($params['contactId']) || !isset($events[$params['eventId']])
|| !isset($contacts[$params['contactId']])
) {
$errors[$key] = $this->notFound('mautic.campaign.error.edit_events.request_invalid');
continue;
}
$event = $events[$params['eventId']];
// Ensure contact exists and user has access
$contact = $this->checkLeadAccess($contacts[$params['contactId']], 'edit');
if ($contact instanceof Response) {
$errors[$key] = $contact->getContent();
continue;
}
// Ensure campaign edit access
$campaign = $event->getCampaign();
if (!$this->checkEntityAccess($campaign, 'edit')) {
$errors[$key] = $this->accessDenied();
continue;
}
$result = $this->model->updateContactEvent($event, $contact, $params);
if (is_string($result)) {
$errors[$key] = $this->returnError($result, Response::HTTP_CONFLICT);
} else {
[$log, $created] = $result;
$event->addContactLog($log);
}
}
$payload = [
$this->entityNameMulti => $events,
];
if (!empty($errors)) {
$payload['errors'] = $errors;
}
$view = $this->view($payload, Response::HTTP_OK);
$this->serializerGroups[] = 'campaignEventWithLogsList';
$this->setSerializationContext($view);
return $this->handleView($view);
}
protected function view($data = null, ?int $statusCode = null, array $headers = []): View
{
if ($this->campaign) {
$data['campaign'] = $this->campaign;
if ($this->contact) {
[$data['membership'], $ignore] = $this->prepareEntitiesForView($this->campaign->getContactMembership($this->contact));
}
}
return parent::view($data, $statusCode, $headers);
}
}

View File

@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace Mautic\CampaignBundle\Controller;
use Doctrine\DBAL\Exception;
use Mautic\CampaignBundle\Entity\Campaign;
use Mautic\CampaignBundle\Model\CampaignModel;
use Mautic\CoreBundle\Helper\MapHelper;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class CampaignMapStatsController extends AbstractController
{
public const MAP_OPTIONS = [
'contacts' => [
'label' => 'mautic.lead.leads',
'unit' => 'Contact',
],
'read_count' => [
'label' => 'mautic.email.read',
'unit' => 'Read',
],
'clicked_through_count'=> [
'label' => 'mautic.email.click',
'unit' => 'Click',
],
];
public const LEGEND_TEXT = 'Total: %total (%withCountry with country)';
public function __construct(private CampaignModel $model)
{
}
/**
* @return array<string, array<int, array<string, int|string>>>
*
* @throws Exception
*/
public function getData(Campaign $entity, \DateTimeImmutable $dateFromObject, \DateTimeImmutable $dateToObject): array
{
return $this->model->getCountryStats($entity, $dateFromObject, $dateToObject);
}
public function hasAccess(CorePermissions $security, Campaign $entity): bool
{
return $security->hasEntityAccess(
'email:emails:viewown',
'email:emails:viewother',
$entity->getCreatedBy()
);
}
/**
* @return array<string,array<string, string>>
*/
public function getMapOptions(Campaign $entity): array
{
if ($entity->isEmailCampaign()) {
return self::MAP_OPTIONS;
}
$key = array_key_first(self::MAP_OPTIONS);
return [$key => self::MAP_OPTIONS[$key]];
}
public function getMapOptionsTitle(): string
{
return '';
}
/**
* @throws \Exception
*/
public function viewAction(
CorePermissions $security,
int $objectId,
string $dateFrom = '',
string $dateTo = '',
): Response {
$entity = $this->model->getEntity($objectId);
if (empty($entity) || !$this->hasAccess($security, $entity)) {
throw new AccessDeniedHttpException();
}
$statsCountries = $this->getData($entity, new \DateTimeImmutable($dateFrom), new \DateTimeImmutable($dateTo));
$mapData = MapHelper::buildMapData($statsCountries, $this->getMapOptions($entity), self::LEGEND_TEXT);
return $this->render(
'@MauticCore/Helper/map.html.twig',
[
'data' => $mapData[0]['data'],
'height' => 315,
'optionsEnabled' => true,
'optionsTitle' => $this->getMapOptionsTitle(),
'options' => $mapData,
'legendEnabled' => true,
'statUnit' => $mapData[0]['unit'],
]
);
}
}

View File

@@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace Mautic\CampaignBundle\Controller;
use Mautic\CampaignBundle\Model\CampaignModel;
use Mautic\CoreBundle\Helper\Chart\BarChart;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\DateTimeHelper;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\EmailBundle\Stats\EmailPeriodMetrics;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
class CampaignMetricsController extends AbstractController
{
public function __construct(
private Translator $translator,
private CoreParametersHelper $coreParametersHelper,
) {
}
public function emailWeekdaysAction(
EmailPeriodMetrics $emailPeriodMetrics,
CampaignModel $model,
int $objectId,
string $dateFrom = '',
string $dateTo = '',
): Response {
$entity = $model->getEntity($objectId);
$eventsIds = $entity->getEmailSendEvents()->getKeys();
$dateFromObject = new \DateTimeImmutable($dateFrom);
$dateToObject = new \DateTimeImmutable($dateTo);
$dateTimeHelper = new DateTimeHelper();
$defaultTimezoneOffset = $dateTimeHelper->getLocalDateTime()->format('Z');
$stats = $emailPeriodMetrics->emailMetricsPerWeekdayByCampaignEvents($eventsIds, $dateFromObject, $dateToObject, $defaultTimezoneOffset);
$chart = new BarChart([
$this->translator->trans('mautic.core.date.monday'),
$this->translator->trans('mautic.core.date.tuesday'),
$this->translator->trans('mautic.core.date.wednesday'),
$this->translator->trans('mautic.core.date.thursday'),
$this->translator->trans('mautic.core.date.friday'),
$this->translator->trans('mautic.core.date.saturday'),
$this->translator->trans('mautic.core.date.sunday'),
]);
$chart->setDataset($this->translator->trans('mautic.email.sent'), array_column($stats, 'sent_count'));
$chart->setDataset($this->translator->trans('mautic.email.read'), array_column($stats, 'read_count'));
$chart->setDataset($this->translator->trans('mautic.email.click'), array_column($stats, 'hit_count'));
return $this->render(
'@MauticCore/Helper/chart.html.twig',
[
'chartData' => $chart->render(),
'chartType' => 'bar',
'chartHeight' => 300,
]
);
}
public function emailHoursAction(
EmailPeriodMetrics $emailPeriodMetrics,
CampaignModel $model,
int $objectId,
string $dateFrom = '',
string $dateTo = '',
): Response {
$entity = $model->getEntity($objectId);
$eventsIds = $entity->getEmailSendEvents()->getKeys();
$dateFromObject = new \DateTimeImmutable($dateFrom);
$dateToObject = new \DateTimeImmutable($dateTo);
$dateTimeHelper = new DateTimeHelper();
$defaultTimezoneOffset = $dateTimeHelper->getLocalDateTime()->format('Z');
$stats = $emailPeriodMetrics->emailMetricsPerHourByCampaignEvents($eventsIds, $dateFromObject, $dateToObject, $defaultTimezoneOffset);
$hoursRange = range(0, 23);
$labels = [];
$timeFormat = $this->coreParametersHelper->get('date_format_timeonly');
foreach ($hoursRange as $hour) {
$startTime = (new \DateTime())->setTime($hour, 0);
$endTime = (new \DateTime())->setTime(($hour + 1) % 24, 0);
$labels[] = $startTime->format($timeFormat).' - '.$endTime->format($timeFormat);
}
$chart = new BarChart($labels);
$chart->setDataset($this->translator->trans('mautic.email.sent'), array_column($stats, 'sent_count'));
$chart->setDataset($this->translator->trans('mautic.email.read'), array_column($stats, 'read_count'));
$chart->setDataset($this->translator->trans('mautic.email.click'), array_column($stats, 'hit_count'));
return $this->render(
'@MauticCore/Helper/chart.html.twig',
[
'chartData' => $chart->render(),
'chartType' => 'hour',
'chartHeight' => 300,
]
);
}
}

View File

@@ -0,0 +1,656 @@
<?php
namespace Mautic\CampaignBundle\Controller;
use Doctrine\Persistence\ManagerRegistry;
use Mautic\CampaignBundle\Entity\Event;
use Mautic\CampaignBundle\EventCollector\EventCollector;
use Mautic\CampaignBundle\Form\Type\EventType;
use Mautic\CampaignBundle\Model\CampaignModel;
use Mautic\CoreBundle\Controller\FormController as CommonFormController;
use Mautic\CoreBundle\Factory\ModelFactory;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\DateTimeHelper;
use Mautic\CoreBundle\Helper\UserHelper;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\CoreBundle\Service\FlashBag;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\CoreBundle\Twig\Helper\DateHelper;
use Mautic\FormBundle\Helper\FormFieldHelper;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
class EventController extends CommonFormController
{
/**
* @var string[]
*/
private array $supportedEventTypes = [
Event::TYPE_DECISION,
Event::TYPE_ACTION,
Event::TYPE_CONDITION,
];
public function __construct(
FormFactoryInterface $formFactory,
FormFieldHelper $fieldHelper,
private EventCollector $eventCollector,
private DateHelper $dateHelper,
ManagerRegistry $doctrine,
ModelFactory $modelFactory,
UserHelper $userHelper,
CoreParametersHelper $coreParametersHelper,
EventDispatcherInterface $dispatcher,
Translator $translator,
FlashBag $flashBag,
RequestStack $requestStack,
CorePermissions $security,
private CampaignModel $campaignModel,
) {
parent::__construct($formFactory, $fieldHelper, $doctrine, $modelFactory, $userHelper, $coreParametersHelper, $dispatcher, $translator, $flashBag, $requestStack, $security);
}
/**
* @var array<string, array<string, mixed>>
*/
private array $modifiedEvents = [];
/**
* @var array<int, string>
*/
private array $deletedEvents = [];
/**
* Generates new form and processes post data.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
*/
public function newAction(Request $request)
{
$success = 0;
$valid = $cancelled = false;
$this->setCampaignElements($request->request);
if ('1' === $request->request->get('submit')) {
$event = $request->request->all()['campaignevent'] ?? [];
$type = $event['type'];
$eventType = $event['eventType'];
$campaignId = $event['campaignId'];
$event['triggerDate'] = (!empty($event['triggerDate'])) ? (new DateTimeHelper($event['triggerDate']))->getDateTime() : null;
} else {
$type = $request->query->get('type');
$eventType = $request->query->get('eventType');
$campaignId = $request->query->get('campaignId');
$anchorName = $request->query->get('anchor', '');
$event = [
'type' => $type,
'eventType' => $eventType,
'campaignId' => $campaignId,
'anchor' => $anchorName,
'anchorEventType' => $request->query->get('anchorEventType', ''),
];
}
// set the eventType key for events
if (!in_array($eventType, $this->supportedEventTypes)) {
return $this->modalAccessDenied();
}
// ajax only for form fields
if (!$type
|| !$request->isXmlHttpRequest()
|| !$this->security->isGranted(
[
'campaign:campaigns:edit',
'campaign:campaigns:create',
],
'MATCH_ONE'
)
) {
return $this->modalAccessDenied();
}
// fire the builder event
$events = $this->eventCollector->getEventsArray();
$form = $this->formFactory->create(
EventType::class,
$event,
[
'action' => $this->generateUrl('mautic_campaignevent_action', ['objectAction' => 'new']),
'settings' => $events[$eventType][$type],
]
);
$event['settings'] = $events[$eventType][$type];
$form->get('campaignId')->setData($campaignId);
// Check for a submitted form and process it
if ('1' === $request->request->get('submit')) {
if (!$cancelled = $this->isFormCancelled($form)) {
if ($valid = $this->isFormValid($form)) {
$success = 1;
// form is valid so process the data
$keyId = 'new'.bin2hex(random_bytes(32));
// save the properties to return with request
$modifiedEvents = $this->getModifiedEvents();
$formData = $form->getData();
$event = array_merge($event, $formData);
$event['id'] = $event['tempId'] = $keyId;
if (empty($event['name'])) {
// set it to the event default
$event['name'] = $this->translator->trans($event['settings']['label']);
}
$modifiedEvents[$keyId] = $event;
$this->modifiedEvents = $modifiedEvents;
} else {
$success = 0;
}
}
}
$viewParams = ['type' => $type];
if ($cancelled || $valid) {
$closeModal = true;
} else {
$closeModal = false;
if (isset($event['settings']['formTheme'])) {
$viewParams['formTheme'] = $event['settings']['formTheme'];
}
$viewParams['form'] = $form->createView();
$viewParams['eventHeader'] = $this->translator->trans($event['settings']['label']);
$viewParams['eventDescription'] = (!empty($event['settings']['description'])) ? $this->translator->trans(
$event['settings']['description']
) : '';
}
$viewParams['hideTriggerMode'] = isset($event['settings']['hideTriggerMode']) && $event['settings']['hideTriggerMode'];
$passthroughVars = [
'mauticContent' => 'campaignEvent',
'success' => $success,
'formSubmitted' => $form->isSubmitted(),
'route' => false,
];
if (1 === $success && !empty($modifiedEvents)) {
$passthroughVars['modifiedEvents'] = $modifiedEvents;
}
if (!empty($keyId)) {
$passthroughVars = array_merge($passthroughVars, $this->eventViewVars($event, $campaignId, 'new'));
}
if ($closeModal) {
// just close the modal
$passthroughVars['closeModal'] = 1;
return new JsonResponse($passthroughVars);
} else {
return $this->ajaxAction(
$request,
[
'contentTemplate' => '@MauticCampaign/Event/form.html.twig',
'viewParameters' => $viewParams,
'passthroughVars' => $passthroughVars,
]
);
}
}
/**
* Generates edit form and processes post data.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
*/
public function editAction(Request $request, $objectId)
{
$valid = $cancelled = false;
$method = $request->getMethod();
$campaignEvent = $request->request->all()['campaignevent'] ?? [];
$campaignId = 'POST' === $method && !empty($campaignEvent['campaignId'])
? $campaignEvent['campaignId']
: $request->query->get('campaignId');
$this->setCampaignElements($request->request);
$event = $this->modifiedEvents[$objectId] ?? [];
if (empty($event)) {
$eventEntity = $this->getModel('campaign.event')->getEntity($objectId);
if (null === $eventEntity) {
return $this->modalAccessDenied();
}
$event = $eventEntity->convertToArray();
}
if ('1' === $request->request->get('submit')) {
$event = array_merge($event, [
'anchor' => $campaignEvent['anchor'] ?? '',
'anchorEventType' => $campaignEvent['anchorEventType'] ?? '',
]);
} else {
if (!isset($event['anchor']) && !empty($event['decisionPath'])) {
// Used to generate label
$event['anchor'] = $event['decisionPath'];
}
if ($request->query->has('anchor')) {
// Override the anchor
$event['anchor'] = $request->get('anchor');
}
if ($request->query->has('anchorEventType')) {
// Override the anchorEventType
$event['anchorEventType'] = $request->get('anchorEventType');
}
}
/*
* If we don't have an event, don't support the event type, this is not an
* AJAX request, or we are not granted campaign edit/create, deny access.
*/
if (empty($event)
|| empty($event['eventType'])
|| !in_array($event['eventType'], $this->supportedEventTypes)
|| !isset($event['type'])
|| !$request->isXmlHttpRequest()
|| !$this->security->isGranted(
[
'campaign:campaigns:edit',
'campaign:campaigns:create',
],
'MATCH_ONE'
)
) {
return $this->modalAccessDenied();
}
/**
* Fire the CampaignBuilderEvent event to get all events.
*
* We can directly dereference the return value here to get
* the supported events for this type because we already made
* sure that we're accessing a supported event type above.
*
* Method getEventsArray() returns translated labels & descriptions
*/
$supportedEvents = $this->eventCollector->getEventsArray()[$event['eventType']];
$form = $this->formFactory->create(
EventType::class,
(array) $event,
[
'action' => $this->generateUrl('mautic_campaignevent_action', ['objectAction' => 'edit', 'objectId' => $objectId]),
'settings' => $supportedEvents[$event['type']],
]
);
$event['settings'] = $supportedEvents[$event['type']];
$form->get('campaignId')->setData($campaignId);
$modifiedEvents = $this->getModifiedEvents();
// Check for a submitted form and process it
if ('1' === $request->request->get('submit')) {
if (!$cancelled = $this->isFormCancelled($form)) {
if ($valid = $this->isFormValid($form)) {
$formData = $form->getData();
$event = array_merge($event, $formData);
// Set the name to the event default if not known.
if (empty($event['name'])) {
$event['name'] = $event['settings']['label'];
}
$modifiedEvents[$objectId] = $event;
}
}
}
$viewParams = [
'type' => $event['type'],
'hideTriggerMode' => isset($event['settings']['hideTriggerMode']) && $event['settings']['hideTriggerMode'],
];
$passthroughVars = [
'mauticContent' => 'campaignEvent',
'success' => !$cancelled && $valid,
'formSubmitted' => $form->isSubmitted(),
'route' => false,
'modifiedEvents'=> $modifiedEvents,
'eventId' => $event['id'] ?? '',
'event' => $event,
];
if (!$cancelled && !$valid) {
if (isset($event['settings']['formTheme'])) {
$viewParams['formTheme'] = $event['settings']['formTheme'];
}
$viewParams = array_merge($viewParams, [
'form' => $form->createView(),
'eventHeader' => $event['settings']['label'],
'eventDescription' => $event['settings']['description'],
]);
return $this->ajaxAction(
$request,
[
'contentTemplate' => '@MauticCampaign/Event/form.html.twig',
'viewParameters' => $viewParams,
'passthroughVars' => $passthroughVars,
]
);
}
if (!$cancelled && $valid) {
$passthroughVars = array_merge($passthroughVars, $this->eventViewVars($event, $campaignId, 'edit'));
}
// Just close the modal
$passthroughVars['closeModal'] = 1;
return new JsonResponse($passthroughVars);
}
/**
* Deletes the entity.
*
* @return JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse
*/
public function deleteAction(Request $request, $objectId)
{
$this->setCampaignElements($request->request);
$modifiedEvents = $this->getModifiedEvents();
$deletedEvents = $this->deletedEvents;
// ajax only for form fields
if (!$request->isXmlHttpRequest()
|| !$this->security->isGranted(
[
'campaign:campaigns:edit',
'campaign:campaigns:create',
],
'MATCH_ONE'
)
) {
return $this->accessDenied();
}
$event = (array_key_exists($objectId, $modifiedEvents)) ? $modifiedEvents[$objectId] : null;
if ('POST' == $request->getMethod() && null !== $event) {
$events = $this->eventCollector->getEventsArray();
$event['settings'] = $events[$event['eventType']][$event['type']];
// Add the field to the delete list
if (!in_array($objectId, $deletedEvents)) {
// If event is new don't add to deleted list
if (!str_contains($objectId, 'new')) {
$deletedEvents[] = $objectId;
}
// Always remove from modified list if deleted
if (isset($modifiedEvents[$objectId])) {
unset($modifiedEvents[$objectId]);
}
}
$dataArray = [
'mauticContent' => 'campaignEvent',
'success' => 1,
'route' => false,
'eventId' => $objectId,
'deleted' => 1,
'event' => $event,
'deletedEvents' => $deletedEvents,
];
} else {
$dataArray = ['success' => 0];
}
return new JsonResponse($dataArray);
}
/**
* Undeletes the entity.
*
* @return JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse
*/
public function undeleteAction(Request $request, $objectId)
{
$campaignId = $request->query->get('campaignId');
$this->setCampaignElements($request->request);
$modifiedEvents = $this->getModifiedEvents();
$deletedEvents = $this->deletedEvents;
// ajax only for form fields
if (!$request->isXmlHttpRequest()
|| !$this->security->isGranted(
[
'campaign:campaigns:edit',
'campaign:campaigns:create',
],
'MATCH_ONE'
)
) {
return $this->accessDenied();
}
$event = (array_key_exists($objectId, $modifiedEvents)) ? $modifiedEvents[$objectId] : null;
if ('POST' == $request->getMethod() && null !== $event) {
$events = $this->eventCollector->getEventsArray();
$event['settings'] = $events[$event['eventType']][$event['type']];
// add the field to the delete list
if (in_array($objectId, $deletedEvents)) {
$key = array_search($objectId, $deletedEvents);
unset($deletedEvents[$key]);
}
$template = (empty($event['settings']['template'])) ? '@MauticCampaign/Event/_generic.html.twig'
: $event['settings']['template'];
// prevent undefined errors
$entity = new Event();
$blank = $entity->convertToArray();
$event = array_merge($blank, $event);
$dataArray = [
'mauticContent' => 'campaignEvent',
'success' => 1,
'route' => false,
'eventId' => $objectId,
'eventHtml' => $this->renderView(
$template,
[
'event' => $event,
'id' => $objectId,
'campaignId' => $campaignId,
]
),
'deletedEvents' => $deletedEvents,
];
} else {
$dataArray = ['success' => 0];
}
return new JsonResponse($dataArray);
}
public function cloneAction(Request $request, string $objectId): JsonResponse
{
$campaignId = $request->query->get('campaignId');
$session = $request->getSession();
$this->setCampaignElements($request->request);
$modifiedEvents = $this->getModifiedEvents();
$campaign = $this->campaignModel->getEntity($campaignId);
// ajax only for form fields
if (!$request->isXmlHttpRequest()
|| !$this->security->isGranted(
[
'campaign:campaigns:edit',
'campaign:campaigns:create',
],
'MATCH_ONE'
)
) {
return $this->accessDenied();
}
$event = (array_key_exists($objectId, $modifiedEvents)) ? $modifiedEvents[$objectId] : null;
if ('POST' == $request->getMethod() && null !== $event) {
$keyId = 'new'.hash('sha1', uniqid((string) mt_rand()));
$event['id'] = $event['tempId'] = $keyId;
$session->set('mautic.campaign.events.clone.storage', $event);
$dataArray = [
'success' => 1,
'mauticContent' => 'campaignEventClone',
'route' => false,
'eventId' => $objectId,
'eventName' => $event['name'],
'eventType' => $event['eventType'],
'type' => $event['type'],
'campaignId' => $campaign ? $campaign->getId() : $campaignId,
'campaignName' => $campaign ? $campaign->getName() : $this->translator->trans('mautic.campaign.event.clone.new.campaign'),
];
} else {
$dataArray = ['success' => 0];
}
return new JsonResponse($dataArray);
}
public function insertAction(Request $request): JsonResponse
{
$campaignId = $request->query->get('campaignId');
$session = $request->getSession();
$this->setCampaignElements($request->request);
$event = $session->get('mautic.campaign.events.clone.storage');
if (empty($event)) {
return new JsonResponse([
'error' => $this->translator->trans('mautic.campaign.event.clone.request.missing'),
], 400);
}
$session->remove('mautic.campaign.events.clone.storage');
$keyId = 'new'.hash('sha1', uniqid((string) mt_rand()));
$event['id'] = $event['tempId'] = $keyId;
$modifiedEvents[$keyId] = $event;
$this->modifiedEvents = $modifiedEvents;
$passThroughVars = [
'mauticContent' => 'campaignEvent',
'clearCloneStorage' => true,
'success' => 1,
'route' => false,
];
$passThroughVars = array_merge($passThroughVars, $this->eventViewVars($event, $campaignId, 'insert'));
return new JsonResponse($passThroughVars);
}
/**
* @param array<string, mixed> $event
*
* @return array<string, mixed>
*/
private function eventViewVars(
array $event,
string $campaignId,
string $action,
): array {
// Merge default event properties with provided event data
$event = array_merge((new Event())->convertToArray(), $event);
// Determine the template
$template = $event['settings']['template'] ?? '@MauticCampaign/Event/_generic.html.twig';
// Prepare common template variables
$templateVars = [
'event' => $event,
'id' => $event['id'],
'campaignId' => $campaignId,
];
if ('edit' === $action) {
$templateVars['update'] = true;
}
// Render the template and store it in the appropriate variable
$passThroughKey = ('edit' === $action) ? 'updateHtml' : 'eventHtml';
$passThroughVars[$passThroughKey] = $this->renderView($template, $templateVars);
// Pass through event-related variables
$passThroughVars += [
'event' => $event,
'eventId' => $event['id'],
'eventType' => $event['eventType'],
];
// Handle trigger mode interval
if (Event::TRIGGER_MODE_INTERVAL === $event['triggerMode']) {
$label = 'mautic.campaign.connection.trigger.interval.label';
if (Event::PATH_INACTION === $event['anchor']) {
$label .= '_inaction';
}
$passThroughVars['label'] = $this->translator->trans(
$label,
[
'%number%' => $event['triggerInterval'],
'%unit%' => $this->translator->trans(
'mautic.campaign.event.intervalunit.'.$event['triggerIntervalUnit'],
['%count%' => $event['triggerInterval']]
),
]
);
}
// Handle trigger mode date
if (Event::TRIGGER_MODE_DATE === $event['triggerMode']) {
$label = 'mautic.campaign.connection.trigger.date.label';
if (Event::PATH_INACTION === $event['anchor']) {
$label .= '_inaction';
}
$passThroughVars['label'] = $this->translator->trans(
$label,
[
'%full%' => $this->dateHelper->toFull($event['triggerDate']),
'%time%' => $this->dateHelper->toTime($event['triggerDate']),
'%date%' => $this->dateHelper->toShort($event['triggerDate']),
]
);
}
return $passThroughVars;
}
private function setCampaignElements(ParameterBag $request): void
{
if ($request->get('modifiedEvents')) {
$this->modifiedEvents = json_decode($request->get('modifiedEvents'), true);
}
if ($request->get('deletedEvents')) {
$this->deletedEvents = json_decode($request->get('deletedEvents'), true);
}
}
/**
* @return array<string, array<string, mixed>>
*/
private function getModifiedEvents(): array
{
return $this->modifiedEvents;
}
}

View File

@@ -0,0 +1,483 @@
<?php
declare(strict_types=1);
namespace Mautic\CampaignBundle\Controller;
use Doctrine\Persistence\ManagerRegistry;
use Mautic\CampaignBundle\Entity\Campaign;
use Mautic\CampaignBundle\Entity\Event;
use Mautic\CampaignBundle\Form\Type\CampaignImportType;
use Mautic\CoreBundle\Controller\AbstractFormController;
use Mautic\CoreBundle\Event\EntityImportAnalyzeEvent;
use Mautic\CoreBundle\Event\EntityImportEvent;
use Mautic\CoreBundle\Event\EntityImportUndoEvent;
use Mautic\CoreBundle\Factory\ModelFactory;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\DateTimeHelper;
use Mautic\CoreBundle\Helper\ImportHelper;
use Mautic\CoreBundle\Helper\PathsHelper;
use Mautic\CoreBundle\Helper\UserHelper;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\CoreBundle\Service\FlashBag;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\FormBundle\Entity\Action;
use Mautic\FormBundle\Entity\Field;
use Mautic\FormBundle\Entity\Form;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
final class ImportController extends AbstractFormController
{
// Steps of the import
public const STEP_UPLOAD_ZIP = 1;
public const STEP_PROGRESS_BAR = 2;
public const STEP_IMPORT_FROM_ZIP = 3;
public function __construct(
ManagerRegistry $doctrine,
CoreParametersHelper $coreParametersHelper,
ModelFactory $modelFactory,
private UserHelper $userHelper,
EventDispatcherInterface $dispatcher,
Translator $translator,
FlashBag $flashBag,
private RequestStack $requestStack,
CorePermissions $security,
private LoggerInterface $logger,
private PathsHelper $pathsHelper,
private FormFactoryInterface $formFactory,
) {
parent::__construct($doctrine, $modelFactory, $userHelper, $coreParametersHelper, $dispatcher, $translator, $flashBag, $requestStack, $security);
}
public function newAction(): Response
{
if (!$this->security->isGranted('campaign:imports:create')) {
return $this->accessDenied();
}
$session = $this->requestStack->getSession();
$filePath = $session->get('mautic.campaign.import.file');
if ($filePath && file_exists($filePath)) {
@unlink($filePath);
$this->logger->info("Removed leftover import file on refresh: {$filePath}");
}
$this->resetImport();
$form = $this->formFactory->create(CampaignImportType::class, [], [
'action' => $this->generateUrl('mautic_campaign_import_action', ['objectAction' => 'upload']),
]);
return $this->delegateView([
'viewParameters' => [
'form' => $form->createView(),
'mauticContent' => 'campaignImport',
],
'contentTemplate' => '@MauticCampaign/Import/import.html.twig',
]);
}
public function uploadAction(Request $request): Response
{
if (!$this->security->isGranted('campaign:imports:create')) {
return $this->accessDenied();
}
$fullPath = $this->pathsHelper->getImportCampaignsPath().'/'.$this->getImportFileName();
$fileName = $this->getImportFileName();
$importDir = $this->pathsHelper->getImportCampaignsPath();
$form = $this->formFactory->create(CampaignImportType::class, [], [
'action' => $this->generateUrl('mautic_campaign_import_action', ['objectAction' => 'upload']),
]);
// Handle cancel action
if ($this->isFormCancelled($form)) {
$this->resetImport();
$this->removeImportFile($fullPath);
$this->logger->log(LogLevel::WARNING, "Import for file {$fullPath} was canceled.");
return $this->newAction();
}
// Validate form before processing
if (!$this->isFormValid($form)) {
$this->logger->error('No file uploaded.');
$form->addError(new FormError($this->translator->trans('mautic.campaign.import.incorrectfile', [], 'validators')));
} else {
// Retrieve uploaded file
$fileData = $request->files->get('campaign_import')['campaignFile'] ?? null;
if (!$fileData) {
$this->logger->error('No file uploaded.');
$form->addError(new FormError($this->translator->trans('mautic.campaign.import.nofile', [], 'validators')));
} else {
// Set progress to 0 before import starts
$this->requestStack->getSession()->set('mautic.campaign.import.step', self::STEP_PROGRESS_BAR);
$this->requestStack->getSession()->set('mautic.campaign.import.progress', 0);
$this->requestStack->getSession()->remove('mautic.campaign.import.summary');
try {
// Ensure the import directory exists
(new Filesystem())->mkdir($importDir, 0755);
// Remove existing file if it exists
if (file_exists($fullPath)) {
if (!unlink($fullPath)) {
$this->logger->error("Failed to delete existing file before new upload: {$fullPath}");
}
}
// Move uploaded file
$fileData->move($importDir, $fileName);
// Update session with the new file and progress reset
$this->requestStack->getSession()->set('mautic.campaign.import.file', $fullPath);
$this->logger->info("File successfully uploaded: {$fullPath}");
return $this->redirectToRoute('mautic_campaign_import_action', ['objectAction' => 'progress']);
} catch (FileException $e) {
$this->logger->error('File upload failed: '.$e->getMessage());
$form->addError(new FormError(
$this->translator->trans(
str_contains($e->getMessage(), 'upload_max_filesize')
? 'mautic.lead.import.filetoolarge'
: 'mautic.lead.import.filenotreadable',
[],
'validators'
)
));
}
}
}
return $this->delegateView([
'viewParameters' => [
'mauticContent' => 'campaignImport',
'form' => $form->createView(),
],
'contentTemplate' => '@MauticCampaign/Import/import.html.twig',
]);
}
/**
* Cancels import by removing the uploaded file.
*/
public function cancelAction(): Response
{
if (!$this->security->isGranted('campaign:imports:create')) {
return $this->accessDenied();
}
$filePath = $this->requestStack->getSession()->get('mautic.campaign.import.file');
if (is_string($filePath)) {
$this->removeImportFile($filePath);
}
$this->resetImport();
$this->addFlashMessage('mautic.campaign.notice.import.canceled', [], FlashBag::LEVEL_NOTICE);
return $this->redirectToRoute('mautic_campaign_import_action', ['objectAction' => 'new']);
}
private function resetImport(): void
{
$this->requestStack->getSession()->set('mautic.campaign.import.file', null);
$this->requestStack->getSession()->set('mautic.campaign.import.step', self::STEP_UPLOAD_ZIP);
$this->requestStack->getSession()->set('mautic.campaign.import.progress', 0);
$this->requestStack->getSession()->remove('mautic.campaign.import.analyzeSummary');
}
private function removeImportFile(string $filepath): void
{
if (file_exists($filepath) && is_readable($filepath)) {
unlink($filepath);
$this->logger->log(LogLevel::WARNING, "File {$filepath} was removed.");
}
}
/**
* Generates unique import directory name inside the cache dir if not stored in the session.
* If it exists in the session, returns that one.
*/
private function getImportFileName(): string
{
$session = $this->requestStack->getSession();
$fileName = $session->get('mautic.campaign.import.file');
if ($fileName && !str_contains($fileName, '/')) {
return $fileName;
}
$uniqueId = bin2hex(random_bytes(8));
$fileName = sprintf('%s_%s.zip', (new DateTimeHelper())->toUtcString('YmdHis'), $uniqueId);
$session->set('mautic.campaign.import.file', $fileName);
return $fileName;
}
public function progressAction(ImportHelper $importHelper): Response
{
$session = $this->requestStack->getSession();
$session->get('mautic.campaign.import.progress', 0);
$step = $session->get('mautic.campaign.import.step', self::STEP_PROGRESS_BAR);
$fullPath = $session->get('mautic.campaign.import.file');
// If there's no valid file, show an error
if (!$fullPath || !file_exists($fullPath)) {
if (self::STEP_UPLOAD_ZIP !== $step) {
$this->addFlashMessage('mautic.campaign.import.nofile', [], FlashBag::LEVEL_ERROR, 'validators');
}
$this->resetImport();
return $this->redirectToRoute('mautic_campaign_import_action', ['objectAction' => 'new']);
}
if (self::STEP_PROGRESS_BAR === $step) {
$analyzeSummary = $this->analyzeData($importHelper, $fullPath);
if (empty($analyzeSummary)) {
$this->addFlashMessage('mautic.campaign.import.nofile', [], FlashBag::LEVEL_ERROR, 'validators');
$this->removeImportFile($fullPath);
$this->resetImport();
return $this->redirectToRoute('mautic_campaign_import_action', ['objectAction' => 'new']);
}
$session->set('mautic.campaign.import.step', self::STEP_IMPORT_FROM_ZIP);
$session->set('mautic.campaign.import.analyzeSummary', $analyzeSummary);
return $this->delegateView([
'viewParameters' => [
'importProgress' => 50,
'analyzeSummary' => $analyzeSummary,
'mauticContent' => 'campaignImport',
],
'contentTemplate' => '@MauticCampaign/Import/progress.html.twig',
]);
} else {
try {
$fileData = $importHelper->readZipFile($fullPath);
$userId = $this->userHelper->getUser()->getId();
$importSummary = [];
$importActions = $this->requestStack->getCurrentRequest()->get('importAction', []);
$importHelper->recursiveRemoveEmailaddress($fileData);
// Loop through importActions and clean UUIDs for 'create' actions
foreach ($fileData as &$group) {
foreach ($importActions as $entityType => $entities) {
if (in_array($entityType, [Event::ENTITY_NAME, Field::ENTITY_NAME, Action::ENTITY_NAME], true)) {
continue;
}
if (!isset($group[$entityType])) {
continue;
}
foreach ($entities as $entityUuid => $action) {
if ('create' !== $action) {
continue;
}
foreach ($group[$entityType] as &$item) {
if (isset($item['uuid']) && (int) $item['uuid'] === (int) $entityUuid) {
if (Campaign::ENTITY_NAME == $entityType) {
foreach ($group[Event::ENTITY_NAME] as &$eventItem) {
$eventItem['uuid'] = '';
}
}
if (Form::ENTITY_NAME == $entityType) {
if (isset($group[Field::ENTITY_NAME])) {
foreach ($group[Field::ENTITY_NAME] as &$fieldItem) {
$fieldItem['uuid'] = '';
}
}
if (isset($group[Action::ENTITY_NAME])) {
foreach ($group[Action::ENTITY_NAME] as &$actionItem) {
$actionItem['uuid'] = '';
}
}
}
$item['uuid'] = '';
break;
}
}
}
}
}
foreach ($fileData as $entity) {
$event = new EntityImportEvent(Campaign::ENTITY_NAME, $entity, $userId);
$this->dispatcher->dispatch($event);
$summary = $event->getStatus();
if (!empty($summary)) {
$importSummary[] = $summary;
}
}
foreach ($importSummary as $summary) {
foreach ([EntityImportEvent::NEW, EntityImportEvent::UPDATE] as $status) {
if (!isset($summary[$status][Campaign::ENTITY_NAME])) {
continue;
}
$campaignData = $summary[$status][Campaign::ENTITY_NAME];
$campaignName = $campaignData['names'][0] ?? 'Unknown';
$campaignId = $campaignData['ids'][0] ?? 0;
$this->addFlashMessage(
'mautic.campaign.notice.import.finished',
[
'%id%' => $campaignId,
'%name%' => htmlspecialchars($campaignName, ENT_QUOTES, 'UTF-8'),
]
);
}
}
$this->removeImportFile($fullPath);
$session->set('mautic.campaign.import.summary', $importSummary);
$this->resetImport();
} catch (\RuntimeException $e) {
$this->logger->error($e->getMessage());
$this->addFlashMessage('mautic.campaign.import.nofile', [], FlashBag::LEVEL_ERROR, 'validators');
$this->removeImportFile($fullPath);
$importSummary = [
EntityImportEvent::ERRORS => [$e->getMessage()],
];
}
return $this->delegateView([
'viewParameters' => [
'importProgress' => 100,
'importSummary' => $importSummary,
'mauticContent' => 'campaignImport',
],
'contentTemplate' => '@MauticCampaign/Import/progress.html.twig',
]);
}
}
/**
* @return array<int|string, array<string, mixed>>
*/
private function analyzeData(ImportHelper $importHelper, string $fullPath): array
{
try {
$fileData = $importHelper->readZipFile($fullPath);
} catch (\RuntimeException $e) {
$this->logger->error($e->getMessage());
$this->removeImportFile($fullPath);
return [
[
'errors' => [
'messages' => [$e->getMessage()],
],
],
];
}
$allData = [];
foreach ($fileData as $entityData) {
$mergedSummary = [];
foreach ($entityData as $key => $data) {
if (empty($data)) {
continue;
}
$event = new EntityImportAnalyzeEvent($key, $data);
$this->dispatcher->dispatch($event);
$summary = $event->getSummary();
foreach ($summary as $status => $entities) {
if ('errors' === $status) {
// Accumulate errors into a flat array
$mergedSummary['errors'] = array_merge(
$mergedSummary['errors'] ?? [],
is_array($entities) ? $entities : [$entities]
);
continue;
}
foreach ($entities as $entityName => $info) {
if (!isset($mergedSummary[$status][$entityName])) {
$mergedSummary[$status][$entityName] = [
'names' => [],
'uuids' => [],
];
}
$mergedSummary[$status][$entityName]['names'] = array_merge(
$mergedSummary[$status][$entityName]['names'],
$info['names'] ?? []
);
$mergedSummary[$status][$entityName]['uuids'] = array_merge(
$mergedSummary[$status][$entityName]['uuids'],
$info['uuids'] ?? []
);
}
}
}
if (!empty($mergedSummary)) {
$allData[] = $mergedSummary;
}
}
return $allData;
}
public function undoAction(): JsonResponse
{
if (!$this->security->isGranted('campaign:imports:delete')) {
return $this->accessDenied();
}
$session = $this->requestStack->getSession();
$importSummaries = $session->get('mautic.campaign.import.summary', []);
$hasUndoData = false;
foreach ($importSummaries as $summary) {
$updates = $summary[EntityImportEvent::UPDATE] ?? [];
$newItems = $summary[EntityImportEvent::NEW] ?? [];
// Only trigger undo if there are no updates and we have new items
if (empty($updates) && !empty($newItems)) {
foreach ($newItems as $entityType => $data) {
if (!empty($data['ids'])) {
$undoEvent = new EntityImportUndoEvent($entityType, $data);
$this->dispatcher->dispatch($undoEvent);
$hasUndoData = true;
}
}
}
}
if ($hasUndoData) {
$this->logger->info('Undo import triggered for Campaign.');
$this->addFlashMessage('mautic.campaign.notice.import.undo');
} else {
$this->addFlashMessage('mautic.campaign.notice.import.undo_no_data');
}
return new JsonResponse(['flashes' => $this->getFlashContent()]);
}
}

View File

@@ -0,0 +1,293 @@
<?php
namespace Mautic\CampaignBundle\Controller;
use Mautic\CampaignBundle\Form\Type\CampaignLeadSourceType;
use Mautic\CampaignBundle\Model\CampaignModel;
use Mautic\CoreBundle\Controller\FormController as CommonFormController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class SourceController extends CommonFormController
{
/**
* @var string[]
*/
private array $supportedSourceTypes = ['lists', 'forms'];
/**
* @var mixed
*/
private $modifiedSources = [];
/**
* @param int $objectId
*
* @return Response
*/
public function newAction(Request $request, $objectId = 0)
{
$success = 0;
$valid = $cancelled = false;
$this->setCampaignElements($request->request);
if ('1' === $request->request->get('submit')) {
$source = $request->request->all()['campaign_leadsource'] ?? [];
$sourceType = $source['sourceType'];
} else {
$sourceType = $request->query->get('sourceType');
$source = [
'sourceType' => $sourceType,
];
}
// set the sourceType key for sources
if (!in_array($sourceType, $this->supportedSourceTypes)) {
return $this->modalAccessDenied();
}
// ajax only for form fields
if (!$request->isXmlHttpRequest()
|| !$this->security->isGranted(
[
'campaign:campaigns:edit',
'campaign:campaigns:create',
],
'MATCH_ONE'
)
) {
return $this->modalAccessDenied();
}
$campaignModel = $this->getModel('campaign');
\assert($campaignModel instanceof CampaignModel);
$sourceList = $campaignModel->getSourceLists($sourceType);
$form = $this->formFactory->create(
CampaignLeadSourceType::class,
$source,
[
'action' => $this->generateUrl('mautic_campaignsource_action', ['objectAction' => 'new', 'objectId' => $objectId]),
'source_choices' => $sourceList,
]
);
$modifiedSources = $this->modifiedSources;
// Check for a submitted form and process it
if ('1' === $request->request->get('submit')) {
if (!$cancelled = $this->isFormCancelled($form)) {
if ($valid = $this->isFormValid($form)) {
$success = 1;
$modifiedSources[$sourceType] = array_flip($form[$sourceType]->getData());
} else {
$success = 0;
}
}
}
$passthroughVars = [
'mauticContent' => 'campaignSource',
'success' => $success,
'route' => false,
];
if (1 === $success && !empty($modifiedSources)) {
$passthroughVars['modifiedSources'] = $modifiedSources;
}
if ($cancelled || $valid) {
if ($valid) {
$passthroughVars['sourceHtml'] = $this->renderView(
'@MauticCampaign/Source/_index.html.twig',
[
'sourceType' => $sourceType,
'campaignId' => $objectId,
'names' => implode(', ', array_intersect_key($sourceList, $modifiedSources[$sourceType])),
]
);
$passthroughVars['sourceType'] = $sourceType;
}
// just close the modal
$passthroughVars['closeModal'] = 1;
return new JsonResponse($passthroughVars);
} else {
$viewParams = [
'sourceType' => $sourceType,
'form' => $form->createView(),
];
return $this->ajaxAction(
$request,
[
'contentTemplate' => '@MauticCampaign/Source/form.html.twig',
'viewParameters' => $viewParams,
'passthroughVars' => $passthroughVars,
]
);
}
}
/**
* @return Response
*/
public function editAction(Request $request, $objectId)
{
$this->setCampaignElements($request->request);
$modifiedSources = $this->modifiedSources;
if ('1' === $request->request->get('submit')) {
$source = $request->request->all()['campaign_leadsource'] ?? [];
$sourceType = $source['sourceType'];
} else {
$sourceType = $request->query->get('sourceType');
$source = [
'sourceType' => $sourceType,
$sourceType => array_flip($modifiedSources[$sourceType]),
];
}
$success = 0;
$valid = $cancelled = false;
if (!in_array($sourceType, $this->supportedSourceTypes)) {
return $this->modalAccessDenied();
}
// ajax only for form fields
if (!$request->isXmlHttpRequest()
|| !$this->security->isGranted(
[
'campaign:campaigns:edit',
'campaign:campaigns:create',
],
'MATCH_ONE'
)
) {
return $this->modalAccessDenied();
}
$campaignModel = $this->getModel('campaign');
\assert($campaignModel instanceof CampaignModel);
$sourceList = $campaignModel->getSourceLists($sourceType);
$form = $this->formFactory->create(
CampaignLeadSourceType::class,
$source,
[
'action' => $this->generateUrl('mautic_campaignsource_action', ['objectAction' => 'edit', 'objectId' => $objectId]),
'source_choices' => $sourceList,
]
);
// Check for a submitted form and process it
if ('1' === $request->request->get('submit')) {
if (!$cancelled = $this->isFormCancelled($form)) {
if ($valid = $this->isFormValid($form)) {
$success = 1;
// save the properties to session
$modifiedSources[$sourceType] = array_flip($form[$sourceType]->getData());
} else {
$success = 0;
}
}
}
$passthroughVars = [
'mauticContent' => 'campaignSource',
'success' => $success,
'route' => false,
];
if (1 === $success && !empty($modifiedSources)) {
$passthroughVars['modifiedSources'] = $modifiedSources;
}
if ($cancelled || $valid) {
if ($valid) {
$passthroughVars['updateHtml'] = $this->renderView(
'@MauticCampaign/Source/_index.html.twig',
[
'sourceType' => $sourceType,
'campaignId' => $objectId,
'names' => implode(', ', array_intersect_key($sourceList, $modifiedSources[$sourceType])),
'update' => true,
]
);
$passthroughVars['sourceType'] = $sourceType;
}
// just close the modal
$passthroughVars['closeModal'] = 1;
return new JsonResponse($passthroughVars);
} else {
$viewParams = [
'sourceType' => $sourceType,
'form' => $form->createView(),
];
return $this->ajaxAction(
$request,
[
'contentTemplate' => '@MauticCampaign/Source/form.html.twig',
'viewParameters' => $viewParams,
'passthroughVars' => $passthroughVars,
]
);
}
}
/**
* Deletes the entity.
*
* @return JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse
*/
public function deleteAction(Request $request, $objectId)
{
$this->setCampaignElements($request->request);
$modifiedSources = $this->modifiedSources;
$sourceType = $request->get('sourceType');
// ajax only for form fields
if (!$request->isXmlHttpRequest()
|| !$this->security->isGranted(
[
'campaign:campaigns:edit',
'campaign:campaigns:create',
],
'MATCH_ONE'
)
) {
return $this->accessDenied();
}
if ('POST' == $request->getMethod()) {
// Add the field to the delete list
if (isset($modifiedSources[$sourceType])) {
unset($modifiedSources[$sourceType]);
}
$dataArray = [
'mauticContent' => 'campaignSource',
'success' => 1,
'route' => false,
'sourceType' => $sourceType,
'deleted' => 1,
'modifiedSources' => $modifiedSources,
];
} else {
$dataArray = ['success' => 0];
}
return new JsonResponse($dataArray);
}
private function setCampaignElements(ParameterBag $request): void
{
if ($request->get('modifiedSources')) {
$this->modifiedSources = json_decode($request->get('modifiedSources'), true);
}
}
}