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,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);
}
}