Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,845 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller;
|
||||
|
||||
use Mautic\CampaignBundle\Membership\MembershipManager;
|
||||
use Mautic\CampaignBundle\Model\CampaignModel;
|
||||
use Mautic\CoreBundle\Controller\AjaxController as CommonAjaxController;
|
||||
use Mautic\CoreBundle\Controller\AjaxLookupControllerTrait;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
use Mautic\CoreBundle\Helper\Tree\JsPlumbFormatter;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Service\FlashBag;
|
||||
use Mautic\EmailBundle\Helper\MailHelper;
|
||||
use Mautic\EmailBundle\Model\EmailModel;
|
||||
use Mautic\LeadBundle\Entity\DoNotContact;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Entity\UtmTag;
|
||||
use Mautic\LeadBundle\Event\ListTypeaheadEvent;
|
||||
use Mautic\LeadBundle\Form\Type\FieldType;
|
||||
use Mautic\LeadBundle\Form\Type\FilterPropertiesType;
|
||||
use Mautic\LeadBundle\Helper\FormFieldHelper;
|
||||
use Mautic\LeadBundle\Model\CompanyModel;
|
||||
use Mautic\LeadBundle\Model\DoNotContact as DoNotContactModel;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\LeadBundle\Model\ListModel;
|
||||
use Mautic\LeadBundle\Provider\FormAdjustmentsProviderInterface;
|
||||
use Mautic\LeadBundle\Segment\SegmentFilterIconTrait;
|
||||
use Mautic\LeadBundle\Segment\Stat\SegmentCampaignShare;
|
||||
use Mautic\LeadBundle\Services\ContactColumnsDictionary;
|
||||
use Mautic\LeadBundle\Services\SegmentDependencyTreeFactory;
|
||||
use Mautic\PluginBundle\Helper\IntegrationHelper;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class AjaxController extends CommonAjaxController
|
||||
{
|
||||
use AjaxLookupControllerTrait;
|
||||
use SegmentFilterIconTrait;
|
||||
|
||||
public function userListAction(Request $request): JsonResponse
|
||||
{
|
||||
$filter = InputHelper::clean($request->query->get('filter'));
|
||||
$leadModel = $this->getModel('lead.lead');
|
||||
\assert($leadModel instanceof LeadModel);
|
||||
$results = $leadModel->getLookupResults('user', $filter);
|
||||
$dataArray = [];
|
||||
foreach ($results as $r) {
|
||||
$name = $r['firstName'].' '.$r['lastName'];
|
||||
$dataArray[] = [
|
||||
'label' => $name,
|
||||
'value' => $r['id'],
|
||||
];
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
public function contactListAction(Request $request, LeadModel $model, CorePermissions $corePermissions): JsonResponse
|
||||
{
|
||||
$filter['string'] = InputHelper::clean($request->query->get('filter'));
|
||||
|
||||
// Do not show other's contacts if do not have permission.
|
||||
if (!$corePermissions->isGranted(['lead:leads:viewother'], 'MATCH_ONE')) {
|
||||
$filter['force'] = ' '.$this->translator->trans('mautic.core.searchcommand.ismine');
|
||||
}
|
||||
|
||||
$results = $model->getLookupResults('contact', $filter);
|
||||
|
||||
$results['success'] = 1;
|
||||
|
||||
return $this->sendJsonResponse($results);
|
||||
}
|
||||
|
||||
public function getLeadIdsByFieldValueAction(Request $request, LeadModel $leadModel): JsonResponse
|
||||
{
|
||||
$field = InputHelper::clean($request->query->get('field'));
|
||||
$value = InputHelper::clean($request->query->all()['value'] ?? '');
|
||||
$ignore = (int) $request->query->get('ignore');
|
||||
$dataArray = ['items' => []];
|
||||
|
||||
if ($field && $value) {
|
||||
$repo = $leadModel->getRepository();
|
||||
$leads = $repo->getLeadsByFieldValue($field, $value, $ignore);
|
||||
$dataArray['existsMessage'] = $this->translator->trans('mautic.lead.exists.by.field').': ';
|
||||
|
||||
foreach ($leads as $lead) {
|
||||
$fields = $repo->getFieldValues($lead->getId());
|
||||
$lead->setFields($fields);
|
||||
$name = $lead->getName();
|
||||
|
||||
if (!$name) {
|
||||
$name = $lead->getEmail();
|
||||
}
|
||||
|
||||
if (!$name) {
|
||||
$name = $this->translator->trans('mautic.lead.lead.anonymous');
|
||||
}
|
||||
|
||||
$leadLink = $this->generateUrl('mautic_contact_action', ['objectAction' => 'view', 'objectId' => $lead->getId()]);
|
||||
|
||||
$dataArray['items'][] = [
|
||||
'name' => $name,
|
||||
'id' => $lead->getId(),
|
||||
'link' => $leadLink,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
public function fieldListAction(Request $request): JsonResponse
|
||||
{
|
||||
$filter = InputHelper::clean($request->query->get('filter'));
|
||||
$fieldAlias = InputHelper::alphanum($request->query->get('field'), false, null, ['_']);
|
||||
$event = new ListTypeaheadEvent($fieldAlias, $filter);
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
return $this->sendJsonResponse($event->getDataArray());
|
||||
}
|
||||
|
||||
public function loadSegmentFilterFormAction(
|
||||
Request $request,
|
||||
FormFactoryInterface $formFactory,
|
||||
FormAdjustmentsProviderInterface $formAdjustmentsProvider,
|
||||
ListModel $listModel,
|
||||
): JsonResponse {
|
||||
$fieldAlias = InputHelper::clean($request->request->get('fieldAlias'));
|
||||
$fieldObject = InputHelper::clean($request->request->get('fieldObject'));
|
||||
$operator = InputHelper::clean($request->request->get('operator'));
|
||||
$search = InputHelper::clean($request->request->get('search'));
|
||||
$filterNum = (int) $request->request->get('filterNum');
|
||||
|
||||
$form = $formFactory->createNamed('RENAME', FilterPropertiesType::class);
|
||||
|
||||
if ($fieldAlias && $operator) {
|
||||
$formAdjustmentsProvider->adjustForm(
|
||||
$form,
|
||||
$fieldAlias,
|
||||
$fieldObject,
|
||||
$operator,
|
||||
$listModel->getChoiceFields($search)[$fieldObject][$fieldAlias]
|
||||
);
|
||||
}
|
||||
|
||||
$formHtml = $this->renderView(
|
||||
'@MauticLead/List/filterpropform.html.twig',
|
||||
[
|
||||
// 'form' => $this->setFormTheme($form, '@MauticLead/List/filterpropform.html.twig', []),
|
||||
'form' => $form->createView(),
|
||||
]
|
||||
);
|
||||
|
||||
$formHtml = str_replace('id="RENAME', "id=\"leadlist_filters_{$filterNum}_properties", $formHtml);
|
||||
$formHtml = str_replace('name="RENAME', "name=\"leadlist[filters][{$filterNum}][properties]", $formHtml);
|
||||
|
||||
return $this->sendJsonResponse(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'form' => $formHtml,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the cache and gets returns updated HTML.
|
||||
*/
|
||||
public function updateSocialProfileAction(Request $request, IntegrationHelper $integrationHelper): JsonResponse
|
||||
{
|
||||
$dataArray = ['success' => 0];
|
||||
$network = InputHelper::clean($request->request->get('network'));
|
||||
$leadId = InputHelper::clean($request->request->get('lead'));
|
||||
|
||||
if (!empty($leadId)) {
|
||||
// find the lead
|
||||
$model = $this->getModel('lead.lead');
|
||||
$lead = $model->getEntity($leadId);
|
||||
|
||||
if (null !== $lead && $this->security->hasEntityAccess('lead:leads:editown', 'lead:leads:editown', $lead->getPermissionUser())) {
|
||||
$leadFields = $lead->getFields();
|
||||
$socialProfiles = $integrationHelper->getUserProfiles($lead, $leadFields, true, $network);
|
||||
$socialProfileUrls = $integrationHelper->getSocialProfileUrlRegex(false);
|
||||
$integrations = [];
|
||||
$socialCount = count($socialProfiles);
|
||||
if (empty($network) || empty($socialCount)) {
|
||||
$dataArray['completeProfile'] = $this->renderView(
|
||||
'@MauticLead/Social/index.html.twig',
|
||||
[
|
||||
'socialProfiles' => $socialProfiles,
|
||||
'lead' => $lead,
|
||||
'socialProfileUrls' => $socialProfileUrls,
|
||||
]
|
||||
);
|
||||
$dataArray['socialCount'] = $socialCount;
|
||||
} else {
|
||||
foreach ($socialProfiles as $name => $details) {
|
||||
if ($integrationObject = $integrationHelper->getIntegrationObject($name)) {
|
||||
if ($template = $integrationObject->getSocialProfileTemplate()) {
|
||||
$integrations[$name]['newContent'] = $this->renderView(
|
||||
$template,
|
||||
[
|
||||
'lead' => $lead,
|
||||
'details' => $details,
|
||||
'integrationName' => $name,
|
||||
'socialProfileUrls' => $socialProfileUrls,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
$dataArray['profiles'] = $integrations;
|
||||
}
|
||||
|
||||
$dataArray['success'] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the cache for a network.
|
||||
*/
|
||||
public function clearSocialProfileAction(Request $request, IntegrationHelper $helper): JsonResponse
|
||||
{
|
||||
$dataArray = ['success' => 0];
|
||||
$network = InputHelper::clean($request->request->get('network'));
|
||||
$leadId = InputHelper::clean($request->request->get('lead'));
|
||||
|
||||
if (!empty($leadId)) {
|
||||
// find the lead
|
||||
$model = $this->getModel('lead.lead');
|
||||
$lead = $model->getEntity($leadId);
|
||||
|
||||
if (null !== $lead && $this->security->hasEntityAccess('lead:leads:editown', 'lead:leads:editown', $lead->getPermissionUser())) {
|
||||
$dataArray['success'] = 1;
|
||||
$socialProfiles = $helper->clearIntegrationCache($lead, $network);
|
||||
$socialCount = count($socialProfiles);
|
||||
|
||||
if (empty($socialCount)) {
|
||||
$dataArray['completeProfile'] = $this->renderView(
|
||||
'@MauticLead/Social/index.html.twig',
|
||||
[
|
||||
'socialProfiles' => $socialProfiles,
|
||||
'lead' => $lead,
|
||||
'socialProfileUrls' => $helper->getSocialProfileUrlRegex(false),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$dataArray['socialCount'] = $socialCount;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
protected function toggleLeadListAction(Request $request): JsonResponse
|
||||
{
|
||||
$dataArray = ['success' => 0];
|
||||
$leadId = (int) $request->request->get('leadId');
|
||||
$listId = (int) $request->request->get('listId');
|
||||
$action = InputHelper::clean($request->request->get('listAction'));
|
||||
|
||||
if (!empty($leadId) && !empty($listId) && in_array($action, ['remove', 'add'])) {
|
||||
$leadModel = $this->getModel('lead');
|
||||
$listModel = $this->getModel('lead.list');
|
||||
|
||||
$lead = $leadModel->getEntity($leadId);
|
||||
$list = $listModel->getEntity($listId);
|
||||
|
||||
if (null !== $lead && null !== $list) {
|
||||
$class = 'add' == $action ? 'addToLists' : 'removeFromLists';
|
||||
$leadModel->$class($lead, $list);
|
||||
$dataArray['success'] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
public function togglePreferredLeadChannelAction(Request $request, LeadModel $leadModel, DoNotContactModel $doNotContact): JsonResponse
|
||||
{
|
||||
$dataArray = ['success' => 0];
|
||||
$leadId = (int) $request->request->get('leadId');
|
||||
$channel = InputHelper::clean($request->request->get('channel'));
|
||||
$action = InputHelper::clean($request->request->get('channelAction'));
|
||||
|
||||
if (!empty($leadId) && !empty($channel) && in_array($action, ['remove', 'add'])) {
|
||||
$lead = $leadModel->getEntity($leadId);
|
||||
|
||||
if (null !== $lead) {
|
||||
if ('remove' === $action) {
|
||||
$doNotContact->addDncForContact($leadId, $channel, DoNotContact::MANUAL, 'user');
|
||||
} elseif ('add' === $action) {
|
||||
$doNotContact->removeDncForContact($leadId, $channel);
|
||||
$this->addFlashMessage('mautic.lead.event.donotcontact_channel_contactable', ['%channel%' => $channel], FlashBag::LEVEL_SUCCESS);
|
||||
$dataArray['flashes'] = $this->getFlashContent();
|
||||
}
|
||||
$dataArray['success'] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
public function toggleLeadCampaignAction(Request $request, MembershipManager $membershipManager, LeadModel $leadModel, CampaignModel $campaignModel): JsonResponse
|
||||
{
|
||||
$dataArray = ['success' => 0];
|
||||
$leadId = (int) $request->request->get('leadId');
|
||||
$campaignId = (int) $request->request->get('campaignId');
|
||||
$action = InputHelper::clean($request->request->get('campaignAction'));
|
||||
|
||||
if (empty($leadId) || empty($campaignId) || !in_array($action, ['remove', 'add'])) {
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
$lead = $leadModel->getEntity($leadId);
|
||||
$campaign = $campaignModel->getEntity($campaignId);
|
||||
|
||||
if (null === $lead || null === $campaign) {
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
if ('add' === $action) {
|
||||
$membershipManager->addContact($lead, $campaign);
|
||||
}
|
||||
|
||||
if ('remove' === $action) {
|
||||
$membershipManager->removeContact($lead, $campaign);
|
||||
}
|
||||
|
||||
$dataArray['success'] = 1;
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
public function toggleCompanyLeadAction(Request $request, LeadModel $leadModel, CompanyModel $companyModel): JsonResponse
|
||||
{
|
||||
$dataArray = ['success' => 0];
|
||||
$leadId = (int) $request->request->get('leadId');
|
||||
$companyId = (int) $request->request->get('companyId');
|
||||
$action = InputHelper::clean($request->request->get('companyAction'));
|
||||
|
||||
if (!empty($leadId) && !empty($companyId) && in_array($action, ['remove', 'add'])) {
|
||||
$lead = $leadModel->getEntity($leadId);
|
||||
$company = $companyModel->getEntity($companyId);
|
||||
|
||||
if (null !== $lead && null !== $company) {
|
||||
$class = 'add' == $action ? 'addLeadToCompany' : 'removeLeadFromCompany';
|
||||
$companyModel->$class($company, $lead);
|
||||
$dataArray['success'] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
public function getImportProgressAction(Request $request): JsonResponse
|
||||
{
|
||||
$dataArray = ['success' => 1];
|
||||
|
||||
if ($this->security->isGranted('lead:leads:create')) {
|
||||
$session = $request->getSession();
|
||||
$dataArray['progress'] = $session->get('mautic.lead.import.progress', [0, 0]);
|
||||
$dataArray['percent'] = ($dataArray['progress'][1]) ? ceil(($dataArray['progress'][0] / $dataArray['progress'][1]) * 100) : 100;
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
public function removeBounceStatusAction(Request $request, DoNotContactModel $doNotContact, EmailModel $emailModel): JsonResponse
|
||||
{
|
||||
$dataArray = ['success' => 0];
|
||||
$dncId = $request->request->get('id');
|
||||
$channel = $request->request->get('channel', 'email');
|
||||
|
||||
if (!empty($dncId)) {
|
||||
/** @var DoNotContact $dnc */
|
||||
$dnc = $this->doctrine->getManager()->getRepository(DoNotContact::class)->findOneBy(
|
||||
[
|
||||
'id' => $dncId,
|
||||
]
|
||||
);
|
||||
|
||||
$lead = $dnc->getLead();
|
||||
if ($lead) {
|
||||
// Use lead model to trigger listeners
|
||||
$doNotContact->removeDncForContact($lead->getId(), $channel);
|
||||
$this->addFlashMessage('mautic.lead.event.donotcontact_channel_contactable', ['%channel%' => $channel], FlashBag::LEVEL_SUCCESS);
|
||||
$dataArray['flashes'] = $this->getFlashContent();
|
||||
} else {
|
||||
$emailModel->getRepository()->deleteDoNotEmailEntry($dncId);
|
||||
}
|
||||
|
||||
$dataArray['success'] = 1;
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the rows for new leads.
|
||||
*
|
||||
* @return array|JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse
|
||||
*/
|
||||
public function getNewLeadsAction(Request $request, ContactColumnsDictionary $contactColumnsDictionary, LeadModel $model)
|
||||
{
|
||||
$dataArray = ['success' => 0];
|
||||
$maxId = $request->get('maxId');
|
||||
|
||||
if (!empty($maxId)) {
|
||||
// set some permissions
|
||||
$permissions = $this->security->isGranted(
|
||||
[
|
||||
'lead:leads:viewown',
|
||||
'lead:leads:viewother',
|
||||
'lead:leads:create',
|
||||
'lead:leads:editown',
|
||||
'lead:leads:editother',
|
||||
'lead:leads:deleteown',
|
||||
'lead:leads:deleteother',
|
||||
],
|
||||
'RETURN_ARRAY'
|
||||
);
|
||||
|
||||
if (!$permissions['lead:leads:viewown'] && !$permissions['lead:leads:viewother']) {
|
||||
return $this->accessDenied(true);
|
||||
}
|
||||
|
||||
$session = $request->getSession();
|
||||
$search = $session->get('mautic.lead.filter', '');
|
||||
$filter = ['string' => $search, 'force' => []];
|
||||
$translator = $this->translator;
|
||||
$anonymous = $translator->trans('mautic.lead.lead.searchcommand.isanonymous');
|
||||
$mine = $translator->trans('mautic.core.searchcommand.ismine');
|
||||
$indexMode = $session->get('mautic.lead.indexmode', 'list');
|
||||
|
||||
$session->set('mautic.lead.indexmode', $indexMode);
|
||||
|
||||
// (strpos($search, "$isCommand:$anonymous") === false && strpos($search, "$listCommand:") === false)) ||
|
||||
if ('list' != $indexMode) {
|
||||
// remove anonymous leads unless requested to prevent clutter
|
||||
$filter['force'][] = "!$anonymous";
|
||||
}
|
||||
|
||||
if (!$permissions['lead:leads:viewother']) {
|
||||
$filter['force'][] = $mine;
|
||||
}
|
||||
|
||||
$filter['force'][] = [
|
||||
'column' => 'l.id',
|
||||
'expr' => 'gt',
|
||||
'value' => $maxId,
|
||||
];
|
||||
|
||||
$results = $model->getEntities(
|
||||
[
|
||||
'filter' => $filter,
|
||||
'withTotalCount' => true,
|
||||
]
|
||||
);
|
||||
$count = $results['count'];
|
||||
|
||||
if (!empty($count)) {
|
||||
// Get the max ID of the latest lead added
|
||||
$maxLeadId = $model->getRepository()->getMaxLeadId();
|
||||
|
||||
// We need the EmailRepository to check if a lead is flagged as do not contact
|
||||
/** @var \Mautic\EmailBundle\Entity\EmailRepository $emailRepo */
|
||||
$emailRepo = $this->getModel('email')->getRepository();
|
||||
$indexMode = $request->get('view', $session->get('mautic.lead.indexmode', 'list'));
|
||||
$template = ('list' == $indexMode) ? 'list_rows' : 'grid_cards';
|
||||
$dataArray['leads'] = $this->render(
|
||||
"@MauticLead/Lead/{$template}.html.twig",
|
||||
[
|
||||
'items' => $results['results'],
|
||||
'noContactList' => $emailRepo->getDoNotEmailList(array_keys($results['results'])),
|
||||
'permissions' => $permissions,
|
||||
'security' => $this->security,
|
||||
'highlight' => true,
|
||||
'currentList' => null,
|
||||
'columns' => $contactColumnsDictionary->getColumns(),
|
||||
]
|
||||
)->getContent();
|
||||
$dataArray['indexMode'] = $indexMode;
|
||||
$dataArray['maxId'] = $maxLeadId;
|
||||
$dataArray['success'] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
public function getEmailTemplateAction(Request $request, EmailModel $model, MailHelper $mailHelper): JsonResponse
|
||||
{
|
||||
$data = ['success' => 1, 'body' => '', 'subject' => ''];
|
||||
$emailId = $request->query->get('template');
|
||||
|
||||
/** @var \Mautic\EmailBundle\Entity\Email $email */
|
||||
$email = $model->getEntity($emailId);
|
||||
|
||||
if (null !== $email
|
||||
&& $this->security->hasEntityAccess(
|
||||
'email:emails:viewown',
|
||||
'email:emails:viewother',
|
||||
$email->getCreatedBy()
|
||||
)
|
||||
) {
|
||||
$mailHelper->setEmail($email, true, [], true);
|
||||
|
||||
$data['body'] = $mailHelper->getBody();
|
||||
$data['subject'] = $mailHelper->getSubject();
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($data);
|
||||
}
|
||||
|
||||
public function updateLeadTagsAction(Request $request, LeadModel $leadModel): JsonResponse
|
||||
{
|
||||
$post = $request->request->all()['lead_tags'] ?? [];
|
||||
$lead = $leadModel->getEntity((int) $post['id']);
|
||||
$updatedTags = (!empty($post['tags']) && is_array($post['tags'])) ? $post['tags'] : [];
|
||||
$data = ['success' => 0];
|
||||
|
||||
if (null !== $lead && $this->security->hasEntityAccess('lead:leads:editown', 'lead:leads:editother', $lead->getPermissionUser())) {
|
||||
$leadModel->setTags($lead, $updatedTags, true);
|
||||
|
||||
/** @var \Doctrine\ORM\PersistentCollection $leadTags */
|
||||
$leadTags = $lead->getTags();
|
||||
$leadTagKeys = $leadTags->getKeys();
|
||||
|
||||
// Get an updated list of tags
|
||||
$tags = $leadModel->getTagRepository()->getSimpleList(null, [], 'tag');
|
||||
$tagOptions = '';
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
$selected = (in_array($tag['label'], $leadTagKeys)) ? ' selected="selected"' : '';
|
||||
$tagOptions .= '<option'.$selected.' value="'.$tag['value'].'">'.$tag['label'].'</option>';
|
||||
}
|
||||
|
||||
$data['success'] = 1;
|
||||
$data['tags'] = $tagOptions;
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($data);
|
||||
}
|
||||
|
||||
public function addLeadTagsAction(Request $request, LeadModel $leadModel): JsonResponse
|
||||
{
|
||||
$tags = $request->request->get('tags');
|
||||
$tags = json_decode($tags, true);
|
||||
|
||||
if (is_array($tags)) {
|
||||
$newTags = [];
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if (!is_numeric($tag)) {
|
||||
$newTags[] = $leadModel->getTagRepository()->getTagByNameOrCreateNewOne($tag);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($newTags)) {
|
||||
$leadModel->getTagRepository()->saveEntities($newTags);
|
||||
}
|
||||
|
||||
// Get an updated list of tags
|
||||
$allTags = $leadModel->getTagRepository()->getSimpleList(null, [], 'tag');
|
||||
$tagOptions = '';
|
||||
|
||||
foreach ($allTags as $tag) {
|
||||
$selected = (in_array($tag['value'], $tags) || in_array($tag['label'], $tags)) ? ' selected="selected"' : '';
|
||||
$tagOptions .= '<option'.$selected.' value="'.$tag['value'].'">'.$tag['label'].'</option>';
|
||||
}
|
||||
|
||||
$data = [
|
||||
'success' => 1,
|
||||
'tags' => $tagOptions,
|
||||
];
|
||||
} else {
|
||||
$data = ['success' => 0];
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($data);
|
||||
}
|
||||
|
||||
public function addLeadUtmTagsAction(Request $request, LeadModel $leadModel): JsonResponse
|
||||
{
|
||||
$utmTags = $request->request->get('utmtags');
|
||||
$utmTags = json_decode($utmTags, true);
|
||||
|
||||
if (is_array($utmTags)) {
|
||||
$newUtmTags = [];
|
||||
foreach ($utmTags as $utmTag) {
|
||||
if (!is_numeric($utmTag)) {
|
||||
// New tag
|
||||
$utmTagEntity = new UtmTag();
|
||||
$utmTagEntity->setUtmTag(InputHelper::clean($utmTag));
|
||||
$newUtmTags[] = $utmTagEntity;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($newUtmTags)) {
|
||||
$leadModel->getUtmTagRepository()->saveEntities($newUtmTags);
|
||||
}
|
||||
|
||||
// Get an updated list of tags
|
||||
$allUtmTags = $leadModel->getUtmTagRepository()->getSimpleList(null, [], 'utmtag');
|
||||
$utmTagOptions = '';
|
||||
|
||||
foreach ($allUtmTags as $utmTag) {
|
||||
$selected = (in_array($utmTag['value'], $utmTags) || in_array($utmTag['label'], $utmTags)) ? ' selected="selected"' : '';
|
||||
$utmTagOptions .= '<option'.$selected.' value="'.$utmTag['value'].'">'.$utmTag['label'].'</option>';
|
||||
}
|
||||
|
||||
$data = [
|
||||
'success' => 1,
|
||||
'tags' => $utmTagOptions,
|
||||
];
|
||||
} else {
|
||||
$data = ['success' => 0];
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($data);
|
||||
}
|
||||
|
||||
public function reorderAction(Request $request, FieldModel $model): JsonResponse
|
||||
{
|
||||
$dataArray = ['success' => 0];
|
||||
$order = InputHelper::clean($request->request->get('field'));
|
||||
$page = (int) $request->get('page');
|
||||
$limit = (int) $request->get('limit');
|
||||
|
||||
if (!empty($order)) {
|
||||
$startAt = ($page > 1) ? ($page * $limit) + 1 : 1;
|
||||
$model->reorderFieldsByList($order, $startAt);
|
||||
$dataArray['success'] = 1;
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
public function updateLeadFieldValuesAction(Request $request, LeadModel $leadModel): JsonResponse
|
||||
{
|
||||
$alias = InputHelper::clean($request->request->get('alias'));
|
||||
$operator = InputHelper::clean($request->request->get('operator'));
|
||||
$changed = InputHelper::clean($request->request->get('changed'));
|
||||
$dataArray = ['success' => 0, 'options' => null, 'optionsAttr' => [], 'operators' => null, 'disabled' => false];
|
||||
$leadField = $this->getModel('lead.field')->getRepository()->findOneBy(['alias' => $alias]);
|
||||
|
||||
if ($leadField) {
|
||||
$options = null;
|
||||
$leadFieldType = $leadField->getType();
|
||||
|
||||
$properties = $leadField->getProperties();
|
||||
if (!empty($properties['list'])) {
|
||||
// Lookup/Select options
|
||||
$options = FormFieldHelper::parseList($properties['list']);
|
||||
} elseif (!empty($properties) && 'boolean' == $leadFieldType) {
|
||||
// Boolean options
|
||||
$options = [
|
||||
0 => $properties['no'],
|
||||
1 => $properties['yes'],
|
||||
];
|
||||
} else {
|
||||
switch ($leadFieldType) {
|
||||
case 'country':
|
||||
$options = FormFieldHelper::getCountryChoices();
|
||||
break;
|
||||
case 'region':
|
||||
$options = FormFieldHelper::getRegionChoices();
|
||||
break;
|
||||
case 'timezone':
|
||||
$options = FormFieldHelper::getTimezonesChoices();
|
||||
break;
|
||||
case 'locale':
|
||||
$options = array_flip(FormFieldHelper::getLocaleChoices());
|
||||
break;
|
||||
case 'date':
|
||||
case 'datetime':
|
||||
if ('date' == $operator) {
|
||||
$fieldHelper = new FormFieldHelper();
|
||||
$fieldHelper->setTranslator($this->translator);
|
||||
$options = $fieldHelper->getDateChoices();
|
||||
$options = array_merge(
|
||||
[
|
||||
'custom' => $this->translator->trans('mautic.campaign.event.timed.choice.custom'),
|
||||
],
|
||||
$options
|
||||
);
|
||||
|
||||
$dataArray['optionsAttr']['custom'] = [
|
||||
'data-custom' => 1,
|
||||
];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
$options = (!empty($properties)) ? $properties : [];
|
||||
}
|
||||
}
|
||||
|
||||
$dataArray['fieldType'] = $leadFieldType;
|
||||
$dataArray['options'] = $options;
|
||||
|
||||
if ('field' === $changed) {
|
||||
$dataArray['operators'] = $leadModel->getOperatorsForFieldType($leadFieldType, ['date']);
|
||||
foreach ($dataArray['operators'] as $value => $label) {
|
||||
$dataArray['operators'][$value] = $this->translator->trans($label);
|
||||
}
|
||||
$operator = array_key_first($dataArray['operators']);
|
||||
}
|
||||
|
||||
$disabled = false;
|
||||
switch ($operator) {
|
||||
case 'empty':
|
||||
case '!empty':
|
||||
$disabled = true;
|
||||
$dataArray['options'] = null;
|
||||
break;
|
||||
case 'regexp':
|
||||
case '!regexp':
|
||||
$dataArray['options'] = null;
|
||||
break;
|
||||
}
|
||||
$dataArray['disabled'] = $disabled;
|
||||
}
|
||||
|
||||
$dataArray['success'] = 1;
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
public function setAsPrimaryCompanyAction(Request $request, LeadModel $leadModel): JsonResponse
|
||||
{
|
||||
$dataArray['success'] = 1;
|
||||
$companyId = InputHelper::clean($request->request->get('companyId'));
|
||||
$leadId = InputHelper::clean($request->request->get('leadId'));
|
||||
|
||||
$primaryCompany = $leadModel->setPrimaryCompany($companyId, $leadId);
|
||||
|
||||
$dataArray = array_merge($dataArray, $primaryCompany);
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
public function getCampaignShareStatsAction(Request $request, SegmentCampaignShare $segmentCampaignShareService): JsonResponse
|
||||
{
|
||||
$ids = $request->query->all()['ids'] ?? [];
|
||||
$entityid = $request->query->get('entityId');
|
||||
|
||||
$data = $segmentCampaignShareService->getCampaignsSegmentShare((int) $entityid, $ids);
|
||||
|
||||
$data = [
|
||||
'success' => 1,
|
||||
'stats' => $data,
|
||||
];
|
||||
|
||||
return new JsonResponse($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getLeadCountAction(Request $request, ListModel $model): JsonResponse
|
||||
{
|
||||
$id = (int) InputHelper::clean($request->get('id'));
|
||||
|
||||
$leadList = $model->getEntity($id);
|
||||
if (!$leadList) {
|
||||
return new JsonResponse($this->prepareJsonResponse(0, false), Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$leadCounts = $model->getSegmentContactCount([$id]);
|
||||
$leadCount = $leadCounts[$id];
|
||||
|
||||
return new JsonResponse(
|
||||
$this->prepareJsonResponse(
|
||||
$leadCount,
|
||||
$leadList->needsRebuild(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function getSegmentDependencyTreeAction(Request $request, SegmentDependencyTreeFactory $segmentDependencyTreeFactory, ListModel $model): JsonResponse
|
||||
{
|
||||
$id = (int) $request->get('id');
|
||||
$segment = $model->getEntity($id);
|
||||
|
||||
if (!$segment) {
|
||||
return new JsonResponse(['message' => "Segment {$id} could not be found."], Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$parentNode = $segmentDependencyTreeFactory->buildTree($segment);
|
||||
$formatter = new JsPlumbFormatter();
|
||||
|
||||
return new JsonResponse($formatter->format($parentNode));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function prepareJsonResponse(int $leadCount, bool $needsRebuild): array
|
||||
{
|
||||
return [
|
||||
'html' => $this->translator->trans(
|
||||
$needsRebuild ? 'mautic.lead.list.building' : 'mautic.lead.list.viewleads_count',
|
||||
['%count%' => $leadCount]
|
||||
),
|
||||
'className' => sprintf('label %s col-count', $needsRebuild ? 'label-info' : 'label-gray'),
|
||||
'leadCount' => $leadCount,
|
||||
];
|
||||
}
|
||||
|
||||
public function removeTagFromLeadAction(Request $request, LeadModel $leadModel): JsonResponse
|
||||
{
|
||||
$leadId = (int) $request->request->get('leadId');
|
||||
$tagId = (int) $request->request->get('tagId');
|
||||
|
||||
if (!empty($leadId) && !empty($tagId)) {
|
||||
$leadModel->removeTagFromLead($leadId, $tagId);
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse([]);
|
||||
}
|
||||
|
||||
public function updateLeadFieldOrderChoiceListAction(Request $request): Response
|
||||
{
|
||||
$object = InputHelper::clean($request->request->get('object'));
|
||||
$group = InputHelper::clean($request->request->get('group'));
|
||||
$field = new LeadField();
|
||||
$field->setObject($object);
|
||||
$field->setGroup($group);
|
||||
$form = $this->createForm(FieldType::class, $field);
|
||||
|
||||
return $this->render(
|
||||
'@MauticLead/Field/_field_order.html.twig',
|
||||
[
|
||||
'form' => $form->createView(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller\Api;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ApiBundle\Controller\CommonApiController;
|
||||
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
||||
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 Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Helper\IdentifyCompanyHelper;
|
||||
use Mautic\LeadBundle\Model\CompanyModel;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonApiController<Company>
|
||||
*/
|
||||
class CompanyApiController extends CommonApiController
|
||||
{
|
||||
use CustomFieldsApiControllerTrait;
|
||||
use LeadAccessTrait;
|
||||
|
||||
/**
|
||||
* @var CompanyModel|null
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
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)
|
||||
{
|
||||
$companyModel = $modelFactory->getModel('lead.company');
|
||||
\assert($companyModel instanceof CompanyModel);
|
||||
|
||||
$this->model = $companyModel;
|
||||
$this->entityClass = Company::class;
|
||||
$this->entityNameOne = 'company';
|
||||
$this->entityNameMulti = 'companies';
|
||||
$this->serializerGroups[] = 'companyDetails';
|
||||
|
||||
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
|
||||
}
|
||||
|
||||
public function getNewEntity(array $params)
|
||||
{
|
||||
$leadCompanyModel = $this->getModel('lead.company');
|
||||
\assert($leadCompanyModel instanceof CompanyModel);
|
||||
[$company, $companyEntities] = IdentifyCompanyHelper::findCompany($params, $leadCompanyModel);
|
||||
if (count($companyEntities)) {
|
||||
return $this->model->getEntity($company['id']);
|
||||
}
|
||||
|
||||
return $this->model->getEntity();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Lead &$entity
|
||||
* @param string $action
|
||||
*/
|
||||
protected function preSaveEntity(&$entity, $form, $parameters, $action = 'edit')
|
||||
{
|
||||
$this->setCustomFieldValues($entity, $form, $parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a contact to a company.
|
||||
*
|
||||
* @param int $companyId Company ID
|
||||
* @param int $contactId Contact ID
|
||||
*
|
||||
* @return Response
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
|
||||
*/
|
||||
public function addContactAction($companyId, $contactId)
|
||||
{
|
||||
$company = $this->model->getEntity($companyId);
|
||||
$view = $this->view(['success' => 1], Response::HTTP_OK);
|
||||
|
||||
if (null === $company) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
$contact = $this->checkLeadAccess($contactId, 'edit');
|
||||
if ($contact instanceof Response) {
|
||||
return $contact;
|
||||
}
|
||||
|
||||
$this->model->addLeadToCompany($company, $contact);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes given contact from a company.
|
||||
*
|
||||
* @param int $companyId List ID
|
||||
* @param int $contactId Lead ID
|
||||
*
|
||||
* @return Response
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
|
||||
*/
|
||||
public function removeContactAction($companyId, $contactId)
|
||||
{
|
||||
$company = $this->model->getEntity($companyId);
|
||||
$view = $this->view(['success' => 1], Response::HTTP_OK);
|
||||
|
||||
if (null === $company) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
$contactModel = $this->getModel('lead');
|
||||
$contact = $contactModel->getEntity($contactId);
|
||||
|
||||
// Does the contact exist and the user has permission to edit
|
||||
if (null === $contact) {
|
||||
return $this->notFound();
|
||||
} elseif (!$this->security->hasEntityAccess('lead:leads:editown', 'lead:leads:editother', $contact->getPermissionUser())) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$this->model->removeLeadFromCompany($company, $contact);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller\Api;
|
||||
|
||||
use Doctrine\ORM\Tools\Pagination\Paginator;
|
||||
use Mautic\CoreBundle\Cache\ResultCacheOptions;
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Entity\CustomFieldEntityInterface;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
use Symfony\Component\Form\Form;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
trait CustomFieldsApiControllerTrait
|
||||
{
|
||||
private ?RequestStack $requestStack = null;
|
||||
|
||||
/**
|
||||
* @var mixed[]
|
||||
*/
|
||||
private $fieldCache = [];
|
||||
|
||||
/**
|
||||
* Remove IpAddress and lastActive as it'll be handled outside the form.
|
||||
*
|
||||
* @param mixed[] $parameters
|
||||
* @param Lead|Company $entity
|
||||
* @param string $action
|
||||
*
|
||||
* @return mixed|void
|
||||
*/
|
||||
protected function prepareParametersForBinding(Request $request, $parameters, $entity, $action)
|
||||
{
|
||||
if ('company' === $this->entityNameOne) {
|
||||
$object = 'company';
|
||||
} else {
|
||||
$object = 'lead';
|
||||
unset($parameters['lastActive'], $parameters['tags'], $parameters['ipAddress']);
|
||||
}
|
||||
|
||||
if (in_array($request->getMethod(), ['POST', 'PUT'])) {
|
||||
// If a new contact or PUT update (complete representation of the objectd), set empty fields to field defaults if the parameter
|
||||
// is not defined in the request
|
||||
|
||||
/** @var FieldModel $fieldModel */
|
||||
$fieldModel = $this->getModel('lead.field');
|
||||
$fields = $fieldModel->getFieldListWithProperties($object);
|
||||
|
||||
foreach ($fields as $alias => $field) {
|
||||
// Set the default value if the parameter is not included in the request, there is no value for the given entity, and a default is defined
|
||||
$currentValue = $entity->getFieldValue($alias);
|
||||
if (!isset($parameters[$alias]) && ('' === $currentValue || null == $currentValue) && '' !== $field['defaultValue'] && null !== $field['defaultValue']) {
|
||||
$parameters[$alias] = $field['defaultValue'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten fields into an 'all' key for dev convenience.
|
||||
*/
|
||||
protected function preSerializeEntity(object $entity, string $action = 'view'): void
|
||||
{
|
||||
if ($entity instanceof CustomFieldEntityInterface) {
|
||||
$fields = $entity->getFields();
|
||||
$fields['all'] = $entity->getProfileFields();
|
||||
|
||||
// Temporary hack to address numbers being type casted to float which broke some API implementations because M2 used to return
|
||||
// these as strings and values are normalized in a dozen differneet ways throughout LeadModel::setFieldValues methods and became
|
||||
// too risky to hotfix
|
||||
$fields = $this->fixNumbers($fields);
|
||||
|
||||
$entity->setFields($fields);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $fields
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
private function fixNumbers(array $fields): array
|
||||
{
|
||||
$numberFields = [];
|
||||
foreach ($fields as $group => $groupFields) {
|
||||
if ('all' === $group) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($groupFields as $field => $fieldDefinition) {
|
||||
if ('points' === $field) {
|
||||
// Points were always a number in M2
|
||||
$numberFields[$field] = (int) $fields[$group][$field]['value'];
|
||||
}
|
||||
|
||||
if ('number' !== $fieldDefinition['type'] || null === $fields[$group][$field]['value']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Some requests don't seem to have properties unserialized by default (even in M2)
|
||||
if (!isset($fieldDefinition['properties'])) {
|
||||
$fieldDefinition['properties'] = [];
|
||||
}
|
||||
$properties = is_string($fieldDefinition['properties']) ? unserialize($fieldDefinition['properties']) : $fieldDefinition['properties'];
|
||||
|
||||
$fields[$group][$field]['value'] = empty($properties['scale']) ? (int) $fields[$group][$field]['value']
|
||||
: (float) $fields[$group][$field]['value'];
|
||||
$fields[$group][$field]['normalizedValue'] = empty($properties['scale']) ? (int) $fields[$group][$field]['normalizedValue']
|
||||
: (float) $fields[$group][$field]['normalizedValue'];
|
||||
|
||||
$numberFields[$field] = $fields[$group][$field]['value'];
|
||||
}
|
||||
}
|
||||
|
||||
// Fix "all" fields
|
||||
$fields['all'] = array_merge($fields['all'], $numberFields);
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function getEntityFormOptions(): array
|
||||
{
|
||||
$object = ('company' === $this->entityNameOne) ? 'company' : 'lead';
|
||||
|
||||
if (!empty($this->fieldCache[$object])) {
|
||||
return $this->fieldCache[$object];
|
||||
}
|
||||
|
||||
$model = $this->getModel('lead.field');
|
||||
\assert($model instanceof FieldModel);
|
||||
|
||||
$fields = $model->getEntities(
|
||||
[
|
||||
'filter' => [
|
||||
'force' => [
|
||||
[
|
||||
'column' => 'f.isPublished',
|
||||
'expr' => 'eq',
|
||||
'value' => true,
|
||||
],
|
||||
[
|
||||
'column' => 'f.object',
|
||||
'expr' => 'eq',
|
||||
'value' => $object,
|
||||
],
|
||||
],
|
||||
],
|
||||
'hydration_mode' => 'HYDRATE_ARRAY',
|
||||
'result_cache' => new ResultCacheOptions(LeadField::CACHE_NAMESPACE),
|
||||
]
|
||||
);
|
||||
\assert($fields instanceof Paginator);
|
||||
|
||||
$this->fieldCache[$object] = ['fields' => $fields->getIterator()];
|
||||
|
||||
return $this->fieldCache[$object];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Lead|Company $entity
|
||||
* @param Form $form
|
||||
* @param mixed[] $parameters
|
||||
* @param bool $isPostOrPatch
|
||||
*
|
||||
* @return bool|void
|
||||
*/
|
||||
protected function setCustomFieldValues($entity, $form, $parameters, $isPostOrPatch = false)
|
||||
{
|
||||
// set the custom field values
|
||||
// pull the data from the form in order to apply the form's formatting
|
||||
foreach ($form as $f) {
|
||||
$parameters[$f->getName()] = $f->getData();
|
||||
}
|
||||
|
||||
if ($isPostOrPatch) {
|
||||
// Don't overwrite the contacts accumulated points
|
||||
if (isset($parameters['points']) && empty($parameters['points'])) {
|
||||
unset($parameters['points']);
|
||||
}
|
||||
|
||||
// When merging a contact because of a unique identifier match in POST /api/contacts//new or PATCH /api/contacts//edit all 0 values must be unset because
|
||||
// we have to assume 0 was not meant to overwrite an existing value. Other empty values will be caught by LeadModel::setFieldValues
|
||||
$parameters = array_filter(
|
||||
$parameters,
|
||||
function ($value): bool {
|
||||
if (is_numeric($value)) {
|
||||
return 0 !== (int) $value;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
$overwriteWithBlank = !$isPostOrPatch;
|
||||
if (isset($parameters['overwriteWithBlank']) && !empty($parameters['overwriteWithBlank'])) {
|
||||
$overwriteWithBlank = true;
|
||||
unset($parameters['overwriteWithBlank']);
|
||||
}
|
||||
|
||||
$this->model->setFieldValues($entity, $parameters, $overwriteWithBlank);
|
||||
}
|
||||
|
||||
#[\Symfony\Contracts\Service\Attribute\Required]
|
||||
public function setRequestStack(RequestStack $requestStack): void
|
||||
{
|
||||
$this->requestStack = $requestStack;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller\Api;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ApiBundle\Controller\CommonApiController;
|
||||
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
||||
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 Mautic\LeadBundle\Entity\LeadDevice;
|
||||
use Mautic\LeadBundle\Model\DeviceModel;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonApiController<LeadDevice>
|
||||
*/
|
||||
class DeviceApiController 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)
|
||||
{
|
||||
$leadDeviceModel = $modelFactory->getModel('lead.device');
|
||||
\assert($leadDeviceModel instanceof DeviceModel);
|
||||
|
||||
$this->model = $leadDeviceModel;
|
||||
$this->entityClass = LeadDevice::class;
|
||||
$this->entityNameOne = 'device';
|
||||
$this->entityNameMulti = 'devices';
|
||||
|
||||
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param LeadDevice &$entity
|
||||
* @param string $action
|
||||
*/
|
||||
protected function preSaveEntity(&$entity, $form, $parameters, $action = 'edit')
|
||||
{
|
||||
$lead = null;
|
||||
if (!empty($parameters['lead'])) {
|
||||
$lead = $parameters['lead'];
|
||||
} elseif (!empty($parameters['contact'])) {
|
||||
$lead = $parameters['contact'];
|
||||
}
|
||||
if ($lead) {
|
||||
$lead = $this->checkLeadAccess($lead, $action);
|
||||
|
||||
if ($lead instanceof Response) {
|
||||
return $lead;
|
||||
}
|
||||
|
||||
$entity->setLead($lead);
|
||||
unset($parameters['lead'], $parameters['contact']);
|
||||
} elseif ('new' === $action) {
|
||||
return $this->returnError('contact ID is mandatory', Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param LeadDevice|FormEntity $entity
|
||||
*/
|
||||
protected function checkEntityAccess($entity, $action = 'view')
|
||||
{
|
||||
return parent::checkEntityAccess($entity->getLead(), $action);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller\Api;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ApiBundle\Controller\CommonApiController;
|
||||
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
||||
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\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Field\Exception\AbortColumnCreateException;
|
||||
use Mautic\LeadBundle\Field\Exception\AbortColumnUpdateException;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
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;
|
||||
|
||||
/**
|
||||
* @extends CommonApiController<LeadField>
|
||||
*/
|
||||
class FieldApiController extends CommonApiController
|
||||
{
|
||||
/**
|
||||
* Can have value of 'contact' or 'company'.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $fieldObject;
|
||||
|
||||
/**
|
||||
* @var FieldModel|null
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
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, FieldModel $fieldModel)
|
||||
{
|
||||
$request = $requestStack->getCurrentRequest();
|
||||
\assert(null !== $request);
|
||||
|
||||
$this->model = $fieldModel;
|
||||
$this->fieldObject = $request->get('object');
|
||||
$this->entityClass = LeadField::class;
|
||||
$this->entityNameOne = 'field';
|
||||
$this->entityNameMulti = 'fields';
|
||||
$this->routeParams = ['object' => $this->fieldObject];
|
||||
|
||||
if ('contact' === $this->fieldObject) {
|
||||
$this->fieldObject = 'lead';
|
||||
}
|
||||
|
||||
$repo = $this->model->getRepository();
|
||||
$tableAlias = $repo->getTableAlias();
|
||||
$this->listFilters[] = [
|
||||
'column' => $tableAlias.'.object',
|
||||
'expr' => 'eq',
|
||||
'value' => $this->fieldObject,
|
||||
];
|
||||
|
||||
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
|
||||
}
|
||||
|
||||
protected function saveEntity($entity, int $statusCode): int
|
||||
{
|
||||
try {
|
||||
return parent::saveEntity($entity, $statusCode);
|
||||
} catch (AbortColumnCreateException) {
|
||||
// Field has been queued
|
||||
return Response::HTTP_ACCEPTED;
|
||||
} catch (AbortColumnUpdateException) {
|
||||
// Field has been queued
|
||||
return Response::HTTP_OK;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes and returns an array of where statements from the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getWhereFromRequest(Request $request)
|
||||
{
|
||||
$where = parent::getWhereFromRequest($request);
|
||||
|
||||
$where[] = [
|
||||
'col' => 'object',
|
||||
'expr' => 'eq',
|
||||
'val' => $this->fieldObject,
|
||||
];
|
||||
|
||||
return $where;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed|void
|
||||
*/
|
||||
protected function prepareParametersForBinding(Request $request, $parameters, $entity, $action)
|
||||
{
|
||||
$parameters['object'] = $this->fieldObject;
|
||||
// Workaround for mispelled isUniqueIdentifer.
|
||||
if (isset($parameters['isUniqueIdentifier'])) {
|
||||
$parameters['isUniqueIdentifer'] = $parameters['isUniqueIdentifier'];
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param LeadField &$entity
|
||||
* @param string $action
|
||||
*/
|
||||
protected function preSaveEntity(&$entity, $form, $parameters, $action = 'edit')
|
||||
{
|
||||
if (isset($parameters['properties'])) {
|
||||
$result = $this->model->setFieldProperties($entity, $parameters['properties']);
|
||||
|
||||
if (true !== $result) {
|
||||
return $this->returnError($this->translator->trans($result, [], 'validators'), Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,736 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller\Api;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ApiBundle\Controller\CommonApiController;
|
||||
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
||||
use Mautic\CoreBundle\Entity\IpAddress;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\AppVersion;
|
||||
use Mautic\CoreBundle\Helper\ArrayHelper;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Controller\FrequencyRuleTrait;
|
||||
use Mautic\LeadBundle\Controller\LeadDetailsTrait;
|
||||
use Mautic\LeadBundle\DataObject\LeadManipulator;
|
||||
use Mautic\LeadBundle\Deduplicate\ContactMerger;
|
||||
use Mautic\LeadBundle\Deduplicate\Exception\SameContactException;
|
||||
use Mautic\LeadBundle\Entity\DoNotContact;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\DoNotContact as DoNotContactModel;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonApiController<Lead>
|
||||
*/
|
||||
class LeadApiController extends CommonApiController
|
||||
{
|
||||
use CustomFieldsApiControllerTrait;
|
||||
use FrequencyRuleTrait;
|
||||
use LeadDetailsTrait;
|
||||
|
||||
public const MODEL_ID = 'lead.lead';
|
||||
|
||||
/**
|
||||
* @var LeadModel|null
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
private DoNotContactModel $doNotContactModel;
|
||||
|
||||
public function __construct(
|
||||
CorePermissions $security,
|
||||
Translator $translator,
|
||||
EntityResultHelper $entityResultHelper,
|
||||
RouterInterface $router,
|
||||
FormFactoryInterface $formFactory,
|
||||
DoNotContactModel $doNotContactModel,
|
||||
AppVersion $appVersion,
|
||||
private ContactMerger $contactMerger,
|
||||
private UserHelper $userHelper,
|
||||
private IpLookupHelper $ipLookupHelper,
|
||||
RequestStack $requestStack,
|
||||
ManagerRegistry $doctrine,
|
||||
ModelFactory $modelFactory,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
) {
|
||||
$this->doNotContactModel = $doNotContactModel;
|
||||
|
||||
$leadModel = $modelFactory->getModel(self::MODEL_ID);
|
||||
\assert($leadModel instanceof LeadModel);
|
||||
$this->model = $leadModel;
|
||||
$this->entityClass = Lead::class;
|
||||
$this->entityNameOne = 'contact';
|
||||
$this->entityNameMulti = 'contacts';
|
||||
$this->serializerGroups = ['leadDetails', 'frequencyRulesList', 'doNotContactList', 'userList', 'stageList', 'publishDetails', 'ipAddress', 'tagList', 'utmtagsList'];
|
||||
|
||||
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of users for lead owner edits.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function getOwnersAction(Request $request)
|
||||
{
|
||||
if (!$this->security->isGranted(
|
||||
['lead:leads:create', 'lead:leads:editown', 'lead:leads:editother'],
|
||||
'MATCH_ONE'
|
||||
)
|
||||
) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$filter = $request->query->get('filter', null);
|
||||
$limit = $request->query->get('limit', null);
|
||||
$start = $request->query->get('start', null);
|
||||
$users = $this->model->getLookupResults('user', $filter, $limit, $start);
|
||||
$view = $this->view($users, Response::HTTP_OK);
|
||||
$context = $view->getContext()->setGroups(['userList']);
|
||||
$view->setContext($context);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
protected function getTotalCountTtl(): ?int
|
||||
{
|
||||
return $this->coreParametersHelper->get('contact_api_count_cache_ttl', 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of custom fields.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function getFieldsAction()
|
||||
{
|
||||
if (!$this->security->isGranted(['lead:leads:editown', 'lead:leads:editother'], 'MATCH_ONE')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$fields = $this->getModel('lead.field')->getEntities(
|
||||
[
|
||||
'filter' => [
|
||||
'force' => [
|
||||
[
|
||||
'column' => 'f.isPublished',
|
||||
'expr' => 'eq',
|
||||
'value' => true,
|
||||
'object' => 'lead',
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$view = $this->view($fields, Response::HTTP_OK);
|
||||
$context = $view->getContext()->setGroups(['leadFieldList']);
|
||||
$view->setContext($context);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of notes on a specific lead.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function getNotesAction(Request $request, $id)
|
||||
{
|
||||
$entity = $this->model->getEntity($id);
|
||||
|
||||
if (null === $entity) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
if (!$this->security->hasEntityAccess('lead:leads:viewown', 'lead:leads:viewother', $entity->getPermissionUser())) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$results = $this->getModel('lead.note')->getEntities(
|
||||
[
|
||||
'start' => $request->query->get('start', '0'),
|
||||
'limit' => $request->query->get('limit', $this->coreParametersHelper->get('default_pagelimit')),
|
||||
'filter' => [
|
||||
'string' => $request->query->get('search', ''),
|
||||
'force' => [
|
||||
[
|
||||
'column' => 'n.lead',
|
||||
'expr' => 'eq',
|
||||
'value' => $entity,
|
||||
],
|
||||
],
|
||||
],
|
||||
'orderBy' => $request->query->get('orderBy', 'n.dateAdded'),
|
||||
'orderByDir' => $request->query->get('orderByDir', 'DESC'),
|
||||
]
|
||||
);
|
||||
|
||||
[$notes, $count] = $this->prepareEntitiesForView($results);
|
||||
|
||||
$view = $this->view(
|
||||
[
|
||||
'total' => $count,
|
||||
'notes' => $notes,
|
||||
],
|
||||
Response::HTTP_OK
|
||||
);
|
||||
|
||||
$context = $view->getContext()->setGroups(['leadNoteDetails']);
|
||||
$view->setContext($context);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of devices on a specific lead.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function getDevicesAction(Request $request, $id)
|
||||
{
|
||||
$entity = $this->model->getEntity($id);
|
||||
|
||||
if (null === $entity) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
if (!$this->security->hasEntityAccess('lead:leads:viewown', 'lead:leads:viewother', $entity->getPermissionUser())) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$results = $this->getModel('lead.device')->getEntities(
|
||||
[
|
||||
'start' => $request->query->get('start', '0'),
|
||||
'limit' => $request->query->get('limit', $this->coreParametersHelper->get('default_pagelimit')),
|
||||
'filter' => [
|
||||
'string' => $request->query->get('search', ''),
|
||||
'force' => [
|
||||
[
|
||||
'column' => 'd.lead',
|
||||
'expr' => 'eq',
|
||||
'value' => $entity,
|
||||
],
|
||||
],
|
||||
],
|
||||
'orderBy' => $request->query->get('orderBy', 'd.dateAdded'),
|
||||
'orderByDir' => $request->query->get('orderByDir', 'DESC'),
|
||||
]
|
||||
);
|
||||
|
||||
[$devices, $count] = $this->prepareEntitiesForView($results);
|
||||
|
||||
$view = $this->view(
|
||||
[
|
||||
'total' => $count,
|
||||
'devices' => $devices,
|
||||
],
|
||||
Response::HTTP_OK
|
||||
);
|
||||
|
||||
$context = $view->getContext()->setGroups(['leadDeviceDetails']);
|
||||
$view->setContext($context);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of contact segments the contact is in.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function getListsAction($id)
|
||||
{
|
||||
$entity = $this->model->getEntity($id);
|
||||
if (null !== $entity) {
|
||||
if (!$this->security->hasEntityAccess('lead:leads:viewown', 'lead:leads:viewother', $entity->getPermissionUser())) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$lists = $this->model->getLists($entity, true, true);
|
||||
|
||||
foreach ($lists as &$l) {
|
||||
unset($l['leads'][0]['leadlist_id']);
|
||||
unset($l['leads'][0]['lead_id']);
|
||||
|
||||
$l = array_merge($l, $l['leads'][0]);
|
||||
|
||||
unset($l['leads']);
|
||||
}
|
||||
|
||||
$view = $this->view(
|
||||
[
|
||||
'total' => count($lists),
|
||||
'lists' => $lists,
|
||||
],
|
||||
Response::HTTP_OK
|
||||
);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of contact companies the contact is in.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function getCompaniesAction($id)
|
||||
{
|
||||
$entity = $this->model->getEntity($id);
|
||||
|
||||
if (null === $entity) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
if (!$this->security->hasEntityAccess('lead:leads:viewown', 'lead:leads:viewother', $entity->getPermissionUser())) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$companies = $this->model->getCompanies($entity);
|
||||
|
||||
$view = $this->view(
|
||||
[
|
||||
'total' => count($companies),
|
||||
'companies' => $companies,
|
||||
],
|
||||
Response::HTTP_OK
|
||||
);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of campaigns the lead is part of.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function getCampaignsAction($id)
|
||||
{
|
||||
$entity = $this->model->getEntity($id);
|
||||
if (null !== $entity) {
|
||||
if (!$this->security->hasEntityAccess('lead:leads:viewown', 'lead:leads:viewother', $entity->getPermissionUser())) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
/** @var \Mautic\CampaignBundle\Model\CampaignModel $campaignModel */
|
||||
$campaignModel = $this->getModel('campaign');
|
||||
$campaigns = $campaignModel->getLeadCampaigns($entity, true);
|
||||
|
||||
foreach ($campaigns as &$c) {
|
||||
if (!empty($c['lists'])) {
|
||||
$c['listMembership'] = array_keys($c['lists']);
|
||||
unset($c['lists']);
|
||||
}
|
||||
|
||||
unset($c['leads'][0]['campaign_id']);
|
||||
unset($c['leads'][0]['lead_id']);
|
||||
|
||||
$c = array_merge($c, $c['leads'][0]);
|
||||
|
||||
unset($c['leads']);
|
||||
}
|
||||
|
||||
$view = $this->view(
|
||||
[
|
||||
'total' => count($campaigns),
|
||||
'campaigns' => $campaigns,
|
||||
],
|
||||
Response::HTTP_OK
|
||||
);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of contact events.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function getActivityAction(Request $request, $id)
|
||||
{
|
||||
$entity = $this->model->getEntity($id);
|
||||
|
||||
if (null === $entity) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
if (!$this->checkEntityAccess($entity)) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
return $this->getAllActivityAction($request, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of contact events.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function getAllActivityAction(Request $request, $lead = null)
|
||||
{
|
||||
$canViewOwn = $this->security->isGranted('lead:leads:viewown');
|
||||
$canViewOthers = $this->security->isGranted('lead:leads:viewother');
|
||||
|
||||
if (!$canViewOthers && !$canViewOwn) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$filters = $this->sanitizeEventFilter(InputHelper::clean($request->query->all()['filters'] ?? $request->request->all()['filters'] ?? []));
|
||||
$limit = (int) $request->get('limit', 25);
|
||||
$page = (int) $request->get('page', 1);
|
||||
$order = InputHelper::clean($request->get('order', ['timestamp', 'DESC']));
|
||||
|
||||
[$events, $serializerGroups] = $this->model->getEngagements($lead, $filters, $order, $page, $limit, false);
|
||||
|
||||
$view = $this->view($events);
|
||||
$context = $view->getContext()->setGroups($serializerGroups);
|
||||
$view->setContext($context);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a DNC to the contact.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function addDncAction(Request $request, $id, $channel)
|
||||
{
|
||||
$entity = $this->model->getEntity((int) $id);
|
||||
|
||||
if (null === $entity) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
if (!$this->checkEntityAccess($entity, 'edit')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$channelId = (int) $request->request->get('channelId');
|
||||
if ($channelId) {
|
||||
$channel = [$channel => $channelId];
|
||||
}
|
||||
|
||||
// If no reason is set, default to 3 (manual)
|
||||
$reason = (int) $request->request->get('reason', DoNotContact::MANUAL);
|
||||
|
||||
// If a reason is set, but it's empty or 0, show an error.
|
||||
if (0 === $reason) {
|
||||
return $this->returnError(
|
||||
'Invalid reason code given',
|
||||
Response::HTTP_BAD_REQUEST,
|
||||
['Reason code needs to be an integer and higher than 0.']
|
||||
);
|
||||
}
|
||||
|
||||
$comments = InputHelper::clean($request->request->get('comments'));
|
||||
|
||||
$doNotContact = $this->doNotContactModel;
|
||||
$doNotContact->addDncForContact($entity->getId(), $channel, $reason, $comments);
|
||||
$view = $this->view([$this->entityNameOne => $entity]);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a DNC from the contact.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function removeDncAction($id, $channel)
|
||||
{
|
||||
$doNotContact = $this->doNotContactModel;
|
||||
|
||||
$entity = $this->model->getEntity((int) $id);
|
||||
|
||||
if (null === $entity) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
if (!$this->checkEntityAccess($entity, 'edit')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$result = $doNotContact->removeDncForContact($entity->getId(), $channel);
|
||||
$view = $this->view(
|
||||
[
|
||||
'recordFound' => $result,
|
||||
$this->entityNameOne => $entity,
|
||||
]
|
||||
);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add/Remove a UTM Tagset to/from the contact.
|
||||
*
|
||||
* @param int $id
|
||||
* @param string $method
|
||||
* @param array<mixed>|int $data
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
protected function applyUtmTagsAction($id, $method, $data)
|
||||
{
|
||||
$entity = $this->model->getEntity((int) $id);
|
||||
|
||||
if (null === $entity) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
if (!$this->checkEntityAccess($entity, 'edit')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
// calls add/remove method as appropriate
|
||||
$result = $this->model->$method($entity, $data);
|
||||
|
||||
if (false === $result) {
|
||||
return $this->badRequest();
|
||||
}
|
||||
|
||||
if ('removeUtmTags' == $method) {
|
||||
$view = $this->view(
|
||||
[
|
||||
'recordFound' => $result,
|
||||
$this->entityNameOne => $entity,
|
||||
]
|
||||
);
|
||||
} else {
|
||||
$view = $this->view([$this->entityNameOne => $entity]);
|
||||
}
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a UTM Tagset to the contact.
|
||||
*
|
||||
* @param int $id
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function addUtmTagsAction(Request $request, $id)
|
||||
{
|
||||
return $this->applyUtmTagsAction($id, 'addUTMTags', $request->request->all());
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a UTM Tagset for the contact.
|
||||
*
|
||||
* @param int $id
|
||||
* @param int $utmid
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function removeUtmTagsAction($id, $utmid)
|
||||
{
|
||||
return $this->applyUtmTagsAction($id, 'removeUtmTags', (int) $utmid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new entity from provided params.
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function getNewEntity(array $params)
|
||||
{
|
||||
return $this->model->checkForDuplicateContact($params);
|
||||
}
|
||||
|
||||
protected function prepareParametersForBinding(Request $request, $parameters, $entity, $action)
|
||||
{
|
||||
// Unset the tags from params to avoid a validation error
|
||||
if (isset($parameters['tags'])) {
|
||||
unset($parameters['tags']);
|
||||
}
|
||||
|
||||
// keep existing tags
|
||||
foreach ($entity->getTags() as $tag) {
|
||||
$parameters['tags'][] = $tag->getId();
|
||||
}
|
||||
|
||||
// keep existing owner if it is not set or should be reset to null
|
||||
if (!array_key_exists('owner', $parameters) && $entity->getOwner()) {
|
||||
$parameters['owner'] = $entity->getOwner()->getId();
|
||||
}
|
||||
// keep existing stage if it is not set or should be reset to null
|
||||
if (!array_key_exists('stage', $parameters) && $entity->getStage()) {
|
||||
$parameters['stage'] = $entity->getStage()->getId();
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Lead $entity
|
||||
* @param array $parameters
|
||||
* @param string $action
|
||||
*/
|
||||
protected function preSaveEntity(&$entity, $form, $parameters, $action = 'edit')
|
||||
{
|
||||
if ('edit' === $action) {
|
||||
// Merge existing duplicate contact based on unique fields if exist
|
||||
// new endpoints will leverage getNewEntity in order to return the correct status codes
|
||||
$existingEntity = $this->model->checkForDuplicateContact($this->entityRequestParameters);
|
||||
\assert($existingEntity instanceof Lead);
|
||||
|
||||
$contactMerger = $this->contactMerger;
|
||||
|
||||
if ($entity->getId() && $existingEntity->getId()) {
|
||||
try {
|
||||
$entity = $contactMerger->merge($entity, $existingEntity);
|
||||
} catch (SameContactException) {
|
||||
}
|
||||
} elseif ($existingEntity->getId()) {
|
||||
$entity = $existingEntity;
|
||||
}
|
||||
}
|
||||
|
||||
$manipulatorObject = $this->inBatchMode ? 'api-batch' : 'api-single';
|
||||
|
||||
$entity->setManipulator(new LeadManipulator(
|
||||
'lead',
|
||||
$manipulatorObject,
|
||||
null,
|
||||
$this->userHelper->getUser()->getName()
|
||||
));
|
||||
|
||||
if (isset($parameters['companies'])) {
|
||||
$this->model->modifyCompanies($entity, $parameters['companies']);
|
||||
unset($parameters['companies']);
|
||||
}
|
||||
|
||||
if (isset($parameters['owner'])) {
|
||||
$owner = $this->getModel('user.user')->getEntity((int) $parameters['owner']);
|
||||
$entity->setOwner($owner);
|
||||
unset($parameters['owner']);
|
||||
}
|
||||
|
||||
if (isset($parameters['stage'])) {
|
||||
$stage = $this->getModel('stage.stage')->getEntity((int) $parameters['stage']);
|
||||
$entity->setStage($stage);
|
||||
unset($parameters['stage']);
|
||||
}
|
||||
|
||||
if (isset($this->entityRequestParameters['tags'])) {
|
||||
$this->model->modifyTags($entity, $this->entityRequestParameters['tags'], null, false);
|
||||
}
|
||||
|
||||
// Since the request can be from 3rd party, check for an IP address if included
|
||||
if (isset($this->entityRequestParameters['ipAddress'])) {
|
||||
$ipAddress = $this->ipLookupHelper->getIpAddress($this->entityRequestParameters['ipAddress']);
|
||||
\assert($ipAddress instanceof IpAddress);
|
||||
|
||||
if (!$entity->getIpAddresses()->contains($ipAddress)) {
|
||||
$entity->addIpAddress($ipAddress);
|
||||
}
|
||||
|
||||
unset($this->entityRequestParameters['ipAddress']);
|
||||
}
|
||||
|
||||
// Check for lastActive date
|
||||
if (isset($this->entityRequestParameters['lastActive'])) {
|
||||
$lastActive = new DateTimeHelper($this->entityRequestParameters['lastActive']);
|
||||
$entity->setLastActive($lastActive->getDateTime());
|
||||
unset($this->entityRequestParameters['lastActive']);
|
||||
}
|
||||
|
||||
// Batch DNC settings
|
||||
if (!empty($parameters['doNotContact']) && is_array($parameters['doNotContact'])) {
|
||||
foreach ($parameters['doNotContact'] as $dnc) {
|
||||
$channel = !empty($dnc['channel']) ? $dnc['channel'] : 'email';
|
||||
$comments = !empty($dnc['comments']) ? $dnc['comments'] : '';
|
||||
|
||||
$reason = (int) ArrayHelper::getValue('reason', $dnc, DoNotContact::MANUAL);
|
||||
|
||||
$doNotContact = $this->doNotContactModel;
|
||||
|
||||
if (DoNotContact::IS_CONTACTABLE === $reason) {
|
||||
if (!empty($entity->getId())) {
|
||||
// Remove DNC record
|
||||
$doNotContact->removeDncForContact($entity->getId(), $channel, false);
|
||||
}
|
||||
} elseif (empty($entity->getId())) {
|
||||
// Contact doesn't exist yet. Directly create a DNC record on the entity.
|
||||
$doNotContact->createDncRecord($entity, $channel, $reason, $comments);
|
||||
} else {
|
||||
// Add DNC record to existing contact
|
||||
$doNotContact->addDncForContact($entity->getId(), $channel, $reason, $comments, false);
|
||||
}
|
||||
}
|
||||
unset($parameters['doNotContact']);
|
||||
}
|
||||
|
||||
if (!empty($parameters['frequencyRules'])) {
|
||||
$viewParameters = [];
|
||||
$data = $this->getFrequencyRuleFormData($entity, null, null, false, $parameters['frequencyRules']);
|
||||
|
||||
if (true !== $frequencyForm = $this->getFrequencyRuleForm($entity, $viewParameters, $data)) {
|
||||
$formErrors = $this->getFormErrorMessages($frequencyForm);
|
||||
$msg = $this->getFormErrorMessage($formErrors);
|
||||
|
||||
if (!$msg) {
|
||||
$msg = $this->translator->trans('mautic.core.error.badrequest', [], 'flashes');
|
||||
}
|
||||
|
||||
return $this->returnError($msg, Response::HTTP_BAD_REQUEST, $formErrors);
|
||||
}
|
||||
|
||||
unset($parameters['frequencyRules']);
|
||||
}
|
||||
|
||||
$isPostOrPatch = 'POST' === $this->requestStack->getCurrentRequest()->getMethod() || 'PATCH' === $this->requestStack->getCurrentRequest()->getMethod();
|
||||
$this->setCustomFieldValues($entity, $form, $parameters, $isPostOrPatch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to be used in FrequencyRuleTrait.
|
||||
*/
|
||||
protected function isFormCancelled(?FormInterface $form = null): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to be used in FrequencyRuleTrait.
|
||||
*/
|
||||
protected function isFormValid(FormInterface $form, ?array $data = null): bool
|
||||
{
|
||||
$form->submit($data, 'PATCH' !== $this->requestStack->getCurrentRequest()->getMethod());
|
||||
|
||||
return $form->isSubmitted() && $form->isValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Lead $entity
|
||||
*/
|
||||
protected function detachEntity(object $entity): void
|
||||
{
|
||||
if (empty($entity->getPreviousId())) {
|
||||
parent::detachEntity($entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller\Api;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ApiBundle\Controller\CommonApiController;
|
||||
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
||||
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 Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\LeadBundle\Model\ListModel;
|
||||
use Mautic\LeadBundle\Security\Permissions\LeadPermissions;
|
||||
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;
|
||||
|
||||
/**
|
||||
* @extends CommonApiController<LeadList>
|
||||
*/
|
||||
class ListApiController extends CommonApiController
|
||||
{
|
||||
use LeadAccessTrait;
|
||||
|
||||
/**
|
||||
* @var ListModel|null
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
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)
|
||||
{
|
||||
$listModel = $modelFactory->getModel('lead.list');
|
||||
\assert($listModel instanceof ListModel);
|
||||
|
||||
$this->model = $listModel;
|
||||
$this->entityClass = LeadList::class;
|
||||
$this->entityNameOne = 'list';
|
||||
$this->entityNameMulti = 'lists';
|
||||
$this->serializerGroups = [
|
||||
'leadListDetails',
|
||||
'userList',
|
||||
'publishDetails',
|
||||
'ipAddress',
|
||||
'categoryList',
|
||||
];
|
||||
|
||||
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This conversion won't be needed in couple of years.
|
||||
*
|
||||
* The 'filter' and 'display' fields used to be part of each segment filter root array.
|
||||
* Those fields were moved to 'properties' subarray. We have to ensure BC and remove them
|
||||
* from filter root array so Symfony forms would not fail with unknown field error.
|
||||
*/
|
||||
protected function prepareParametersForBinding(Request $request, $parameters, $entity, $action)
|
||||
{
|
||||
if (empty($parameters['filters']) || !is_array($parameters['filters'])) {
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
foreach ($parameters['filters'] as $key => $filter) {
|
||||
$bcFilterValue = $filter['filter'] ?? null;
|
||||
$filterValue = $filter['properties']['filter'] ?? $bcFilterValue;
|
||||
$parameters['filters'][$key]['properties']['filter'] = $filterValue;
|
||||
|
||||
if (!empty($filter['display']) && !isset($filter['properties']['display'])) {
|
||||
$parameters['filters'][$key]['properties']['display'] = $filter['display'];
|
||||
}
|
||||
|
||||
unset($parameters['filters'][$key]['filter'], $parameters['filters'][$key]['display']);
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of smart lists for the user.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function getListsAction()
|
||||
{
|
||||
$listModel = $this->getModel('lead.list');
|
||||
\assert($listModel instanceof ListModel);
|
||||
$lists = $listModel->getUserLists();
|
||||
$view = $this->view($lists, Response::HTTP_OK);
|
||||
$context = $view->getContext()->setGroups(['leadListList']);
|
||||
$view->setContext($context);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a lead to a list.
|
||||
*
|
||||
* @param int $id List 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) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
$contact = $this->checkLeadAccess($leadId, 'edit');
|
||||
if ($contact instanceof Response) {
|
||||
return $contact;
|
||||
}
|
||||
|
||||
// Does the user have access to the list
|
||||
$lists = $this->model->getUserLists();
|
||||
if (!isset($lists[$id])) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$leadModel = $this->getModel('lead');
|
||||
\assert($leadModel instanceof LeadModel);
|
||||
$leadModel->addToLists($leadId, $entity);
|
||||
|
||||
$view = $this->view(['success' => 1], Response::HTTP_OK);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a leads to a list.
|
||||
*
|
||||
* @param int $id segement ID
|
||||
*
|
||||
* @return Response
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
|
||||
*/
|
||||
public function addLeadsAction(Request $request, $id)
|
||||
{
|
||||
$contactIds = $request->request->all()['ids'] ?? null;
|
||||
if (null === $contactIds) {
|
||||
return $this->returnError('mautic.core.error.badrequest', Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$entity = $this->model->getEntity($id);
|
||||
|
||||
if (null === $entity) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
// Does the user have access to the list
|
||||
$lists = $this->model->getUserLists();
|
||||
if (!isset($lists[$id])) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$responseDetail = [];
|
||||
foreach ($contactIds as $contactId) {
|
||||
$contact = $this->checkLeadAccess($contactId, 'edit');
|
||||
if ($contact instanceof Response) {
|
||||
$responseDetail[$contactId] = ['success' => false];
|
||||
} else {
|
||||
$leadModel = $this->getModel('lead');
|
||||
\assert($leadModel instanceof LeadModel);
|
||||
/* @var \Mautic\LeadBundle\Entity\Lead $contact */
|
||||
$leadModel->addToLists($contact, $entity);
|
||||
$responseDetail[$contact->getId()] = ['success' => true];
|
||||
}
|
||||
}
|
||||
|
||||
$view = $this->view(['success' => 1, 'details' => $responseDetail], Response::HTTP_OK);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes given contact from a list.
|
||||
*
|
||||
* @param int $id List 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) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
$contact = $this->checkLeadAccess($leadId, 'edit');
|
||||
if ($contact instanceof Response) {
|
||||
return $contact;
|
||||
}
|
||||
|
||||
// Does the user have access to the list
|
||||
$lists = $this->model->getUserLists();
|
||||
if (!isset($lists[$id])) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$leadModel = $this->getModel('lead');
|
||||
\assert($leadModel instanceof LeadModel);
|
||||
$leadModel->removeFromLists($leadId, $entity);
|
||||
|
||||
$view = $this->view(['success' => 1], Response::HTTP_OK);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if user has permission to access retrieved entity.
|
||||
*
|
||||
* @param mixed $entity
|
||||
* @param string $action view|create|edit|publish|delete
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function checkEntityAccess($entity, $action = 'view')
|
||||
{
|
||||
if ('create' == $action || 'edit' == $action || 'view' == $action) {
|
||||
return $this->security->isGranted(LeadPermissions::LISTS_VIEW_OWN);
|
||||
} elseif ('delete' == $action) {
|
||||
return $this->security->hasEntityAccess(
|
||||
true, LeadPermissions::LISTS_DELETE_OTHER, $entity->getCreatedBy()
|
||||
);
|
||||
}
|
||||
|
||||
return parent::checkEntityAccess($entity, $action);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller\Api;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ApiBundle\Controller\CommonApiController;
|
||||
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
||||
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 Mautic\LeadBundle\Entity\LeadNote;
|
||||
use Mautic\LeadBundle\Model\NoteModel;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonApiController<LeadNote>
|
||||
*/
|
||||
class NoteApiController 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)
|
||||
{
|
||||
$leadNoteModel = $modelFactory->getModel('lead.note');
|
||||
\assert($leadNoteModel instanceof NoteModel);
|
||||
|
||||
$this->model = $leadNoteModel;
|
||||
$this->entityClass = LeadNote::class;
|
||||
$this->entityNameOne = 'note';
|
||||
$this->entityNameMulti = 'notes';
|
||||
$this->serializerGroups = ['leadNoteDetails', 'leadList'];
|
||||
|
||||
// When a user passes in a note like "This is <strong>text</strong>", this will
|
||||
// keep the HTML that was passed in.
|
||||
$this->dataInputMasks = ['text' => 'html'];
|
||||
|
||||
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Mautic\LeadBundle\Entity\Lead &$entity
|
||||
* @param string $action
|
||||
*/
|
||||
protected function preSaveEntity(&$entity, $form, $parameters, $action = 'edit')
|
||||
{
|
||||
if (!empty($parameters['lead'])) {
|
||||
$lead = $this->checkLeadAccess($parameters['lead'], $action);
|
||||
|
||||
if ($lead instanceof Response) {
|
||||
return $lead;
|
||||
}
|
||||
|
||||
$entity->setLead($lead);
|
||||
unset($parameters['lead']);
|
||||
} elseif ('new' === $action) {
|
||||
return $this->returnError('lead ID is mandatory', Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller\Api;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ApiBundle\Controller\CommonApiController;
|
||||
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
||||
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\Entity\Tag;
|
||||
use Mautic\LeadBundle\Model\TagModel;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonApiController<Tag>
|
||||
*/
|
||||
class TagApiController extends CommonApiController
|
||||
{
|
||||
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)
|
||||
{
|
||||
$leadTagModel = $modelFactory->getModel('lead.tag');
|
||||
\assert($leadTagModel instanceof TagModel);
|
||||
|
||||
$this->model = $leadTagModel;
|
||||
$this->entityClass = Tag::class;
|
||||
$this->entityNameOne = 'tag';
|
||||
$this->entityNameMulti = 'tags';
|
||||
|
||||
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new entity from provided params.
|
||||
*
|
||||
* @return object
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function getNewEntity(array $params)
|
||||
{
|
||||
if (empty($params[$this->entityNameOne])) {
|
||||
throw new \InvalidArgumentException($this->translator->trans('mautic.lead.api.tag.required', [], 'validators'));
|
||||
}
|
||||
|
||||
$tagModel = $this->model;
|
||||
\assert($tagModel instanceof TagModel);
|
||||
|
||||
return $tagModel->getRepository()->getTagByNameOrCreateNewOne($params[$this->entityNameOne]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Controller\CommonController;
|
||||
use Mautic\CoreBundle\Helper\ExportHelper;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
use Mautic\CoreBundle\Twig\Helper\DateHelper;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class AuditlogController extends CommonController
|
||||
{
|
||||
use LeadAccessTrait;
|
||||
use LeadDetailsTrait;
|
||||
|
||||
public function indexAction(Request $request, $leadId, $page = 1)
|
||||
{
|
||||
if (empty($leadId)) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$lead = $this->checkLeadAccess($leadId, 'view');
|
||||
if ($lead instanceof Response) {
|
||||
return $lead;
|
||||
}
|
||||
|
||||
$this->setListFilters();
|
||||
|
||||
$session = $request->getSession();
|
||||
if ('POST' == $request->getMethod() && $request->request->has('search')) {
|
||||
$filters = [
|
||||
'search' => InputHelper::clean($request->request->get('search')),
|
||||
'includeEvents' => InputHelper::clean($request->request->all()['includeEvents'] ?? []),
|
||||
'excludeEvents' => InputHelper::clean($request->request->all()['excludeEvents'] ?? []),
|
||||
];
|
||||
$session->set('mautic.lead.'.$leadId.'.auditlog.filters', $filters);
|
||||
} else {
|
||||
$filters = null;
|
||||
}
|
||||
|
||||
$order = [
|
||||
$session->get('mautic.lead.'.$leadId.'.auditlog.orderby'),
|
||||
$session->get('mautic.lead.'.$leadId.'.auditlog.orderbydir'),
|
||||
];
|
||||
|
||||
$events = $this->getAuditlogs($lead, $filters, $order, $page);
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'lead' => $lead,
|
||||
'page' => $page,
|
||||
'events' => $events,
|
||||
'enableExportPermission' => $this->security->isAdmin() || $this->security->isGranted('report:export:enable', 'MATCH_ONE'),
|
||||
],
|
||||
'passthroughVars' => [
|
||||
'route' => false,
|
||||
'mauticContent' => 'leadAuditlog',
|
||||
'auditLogCount' => $events['total'],
|
||||
],
|
||||
'contentTemplate' => '@MauticLead/Auditlog/_list.html.twig',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|Response
|
||||
*/
|
||||
public function batchExportAction(Request $request, DateHelper $dateHelper, ExportHelper $exportHelper, $leadId)
|
||||
{
|
||||
if (empty($leadId)) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$lead = $this->checkLeadAccess($leadId, 'view');
|
||||
if ($lead instanceof Response) {
|
||||
return $lead;
|
||||
}
|
||||
|
||||
if (!$this->security->isGranted('report:export:enable', 'MATCH_ONE')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$this->setListFilters();
|
||||
|
||||
$session = $request->getSession();
|
||||
if ('POST' == $request->getMethod() && $request->request->has('search')) {
|
||||
$filters = [
|
||||
'search' => InputHelper::clean($request->request->get('search')),
|
||||
'includeEvents' => InputHelper::clean($request->request->all()['includeEvents'] ?? []),
|
||||
'excludeEvents' => InputHelper::clean($request->request->all()['excludeEvents'] ?? []),
|
||||
];
|
||||
$session->set('mautic.lead.'.$leadId.'.auditlog.filters', $filters);
|
||||
} else {
|
||||
$filters = null;
|
||||
}
|
||||
|
||||
$order = [
|
||||
$session->get('mautic.lead.'.$leadId.'.auditlog.orderby'),
|
||||
$session->get('mautic.lead.'.$leadId.'.auditlog.orderbydir'),
|
||||
];
|
||||
|
||||
$dataType = $request->get('filetype', 'csv');
|
||||
|
||||
$resultsCallback = function ($event) use ($dateHelper): array {
|
||||
$eventLabel = $event['eventLabel'] ?? $event['eventType'];
|
||||
if (is_array($eventLabel)) {
|
||||
$eventLabel = $eventLabel['label'];
|
||||
}
|
||||
|
||||
return [
|
||||
'eventName' => $eventLabel,
|
||||
'eventType' => $event['eventType'] ?? '',
|
||||
'eventTimestamp' => $dateHelper->toText($event['timestamp'], 'local', 'Y-m-d H:i:s', true),
|
||||
];
|
||||
};
|
||||
|
||||
$results = $this->getAuditlogs($lead, $filters, $order, 1, 200);
|
||||
$count = $results['total'];
|
||||
$items = $results['events'];
|
||||
$iterations = ceil($count / 200);
|
||||
$loop = 1;
|
||||
|
||||
// Max of 50 iterations for 10K result export
|
||||
if ($iterations > 50) {
|
||||
$iterations = 50;
|
||||
}
|
||||
|
||||
$toExport = [];
|
||||
|
||||
while ($loop <= $iterations) {
|
||||
if (is_callable($resultsCallback)) {
|
||||
foreach ($items as $item) {
|
||||
$toExport[] = $resultsCallback($item);
|
||||
}
|
||||
} else {
|
||||
foreach ($items as $item) {
|
||||
$toExport[] = (array) $item;
|
||||
}
|
||||
}
|
||||
|
||||
$items = $this->getAuditlogs($lead, $filters, $order, $loop + 1, 200);
|
||||
|
||||
$this->doctrine->getManager()->clear();
|
||||
|
||||
++$loop;
|
||||
}
|
||||
|
||||
return $this->exportResultsAs($toExport, $dataType, 'contact_auditlog', $exportHelper);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\CoreBundle\Controller\AbstractFormController;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Service\FlashBag;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Form\Type\BatchType;
|
||||
use Mautic\LeadBundle\Model\ListModel;
|
||||
use Mautic\LeadBundle\Model\SegmentActionModel;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
class BatchSegmentController extends AbstractFormController
|
||||
{
|
||||
public function __construct(
|
||||
private SegmentActionModel $segmentActionModel,
|
||||
private ListModel $segmentModel,
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* API for batch action.
|
||||
*/
|
||||
public function setAction(Request $request): JsonResponse
|
||||
{
|
||||
$params = $request->query->all()['lead_batch'] ?? $request->request->all()['lead_batch'] ?? [];
|
||||
$contactIds = empty($params['ids']) ? [] : json_decode($params['ids']);
|
||||
|
||||
if ($contactIds && is_array($contactIds)) {
|
||||
$segmentsToAdd = $params['add'] ?? [];
|
||||
$segmentsToRemove = $params['remove'] ?? [];
|
||||
|
||||
if ($segmentsToAdd) {
|
||||
$this->segmentActionModel->addContacts($contactIds, $segmentsToAdd);
|
||||
}
|
||||
|
||||
if ($segmentsToRemove) {
|
||||
$this->segmentActionModel->removeContacts($contactIds, $segmentsToRemove);
|
||||
}
|
||||
|
||||
$this->addFlashMessage('mautic.lead.batch_leads_affected', [
|
||||
'%count%' => count($contactIds),
|
||||
]);
|
||||
} else {
|
||||
$this->addFlashMessage('mautic.core.error.ids.missing');
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'closeModal' => true,
|
||||
'flashes' => $this->getFlashContent(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* View for batch action.
|
||||
*/
|
||||
public function indexAction(): \Symfony\Component\HttpFoundation\Response
|
||||
{
|
||||
$route = $this->generateUrl('mautic_segment_batch_contact_set');
|
||||
$lists = $this->segmentModel->getUserLists();
|
||||
$items = [];
|
||||
|
||||
foreach ($lists as $list) {
|
||||
$items[$list['name'].' ('.$list['id'].')'] = $list['id'];
|
||||
}
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'form' => $this->createForm(
|
||||
BatchType::class,
|
||||
[],
|
||||
[
|
||||
'items' => $items,
|
||||
'action' => $route,
|
||||
]
|
||||
)->createView(),
|
||||
],
|
||||
'contentTemplate' => '@MauticLead/Batch/form.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_contact_index',
|
||||
'mauticContent' => 'leadBatch',
|
||||
'route' => $route,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Factory\PageHelperFactoryInterface;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
trait EntityContactsTrait
|
||||
{
|
||||
/**
|
||||
* @param string|int $entityId
|
||||
* @param int $page
|
||||
* @param string[]|string $permission
|
||||
* @param string $sessionVar
|
||||
* @param string $entityJoinTable Table to join to obtain list of related contacts or a DBAL QueryBuilder object defining custom joins
|
||||
* @param string|null $dncChannel Channel for this entity to get do not contact records for
|
||||
* @param string|null $entityIdColumnName If the entity ID in $joinTable is not "id", set the column name here
|
||||
* @param array|null $contactFilter Array of additional filters for the getEntityContactsWithFields() function
|
||||
* @param array|null $additionalJoins [ ['type' => 'join|leftJoin', 'from_alias' => '', 'table' => '', 'condition' => ''], ... ]
|
||||
* @param string|null $contactColumnName Column of the contact in the join table
|
||||
* @param string|null $paginationTarget DOM selector for injecting new content when pagination is used
|
||||
* @param string|null $orderBy optional OrderBy column, to be used to increase performance with joins
|
||||
* @param string|null $orderByDir optional $orderBy direction, to be used to increase performance with joins
|
||||
* @param int|null $count optional $count if already known to avoid an extra query
|
||||
* @param \DateTimeInterface|null $dateFrom optionally limit to leads added between From and To dates
|
||||
* @param \DateTimeInterface|null $dateTo optionally limit to leads added between From and To dates
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function generateContactsGrid(
|
||||
Request $request,
|
||||
PageHelperFactoryInterface $pageHelperFactory,
|
||||
$entityId,
|
||||
$page,
|
||||
$permission,
|
||||
$sessionVar,
|
||||
$entityJoinTable,
|
||||
$dncChannel = null,
|
||||
$entityIdColumnName = 'id',
|
||||
?array $contactFilter = null,
|
||||
?array $additionalJoins = null,
|
||||
$contactColumnName = null,
|
||||
?array $routeParameters = [],
|
||||
$paginationTarget = null,
|
||||
$orderBy = null,
|
||||
$orderByDir = null,
|
||||
$count = null,
|
||||
?\DateTimeInterface $dateFrom = null,
|
||||
?\DateTimeInterface $dateTo = null,
|
||||
) {
|
||||
if ($permission && !$this->security->isGranted($permission)) {
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'page' => $page,
|
||||
'items' => [], // return 0 contacts if user has no permissions
|
||||
'totalItems' => 0,
|
||||
'tmpl' => $sessionVar.'Contacts',
|
||||
'indexMode' => 'grid',
|
||||
'routeParameters' => $routeParameters,
|
||||
'sessionVar' => $sessionVar.'.contact',
|
||||
'objectId' => $entityId,
|
||||
'target' => $paginationTarget,
|
||||
],
|
||||
'contentTemplate' => '@MauticLead/Lead/grid.html.twig',
|
||||
'passthroughVars' => [
|
||||
'mauticContent' => $sessionVar.'Contacts',
|
||||
'route' => false,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Set the route if not standardized
|
||||
$route = "mautic_{$sessionVar}_contacts";
|
||||
if (method_exists($this, 'getRouteBase') && $this->getRouteBase()) {
|
||||
$route = 'mautic_'.$this->getRouteBase().'_contacts';
|
||||
}
|
||||
|
||||
// Apply filters
|
||||
if ('POST' == $request->getMethod()) {
|
||||
$this->setListFilters($sessionVar.'.contact');
|
||||
}
|
||||
|
||||
$search = $request->get('search', $request->getSession()->get('mautic.'.$sessionVar.'.contact.filter', ''));
|
||||
$request->getSession()->set('mautic.'.$sessionVar.'.contact.filter', $search);
|
||||
|
||||
$pageHelper = $pageHelperFactory->make("mautic.{$sessionVar}", $page);
|
||||
|
||||
$filter = ['string' => $search, 'force' => []];
|
||||
$orderBy = $orderBy ?: $request->getSession()->get('mautic.'.$sessionVar.'.contact.orderby', 'l.id');
|
||||
$orderByDir = $orderByDir ?: $request->getSession()->get('mautic.'.$sessionVar.'.contact.orderbydir', 'DESC');
|
||||
|
||||
$limit = $request->getSession()->get(
|
||||
'mautic.'.$sessionVar.'.contact.limit',
|
||||
$this->coreParametersHelper->get('default_pagelimit')
|
||||
);
|
||||
|
||||
$start = (1 === $page) ? 0 : (($page - 1) * $limit);
|
||||
if ($start < 0) {
|
||||
$start = 0;
|
||||
}
|
||||
|
||||
/** @var LeadRepository $repo */
|
||||
$repo = $this->getModel('lead')->getRepository();
|
||||
$contacts = $repo->getEntityContacts(
|
||||
[
|
||||
'withTotalCount' => (null === $count),
|
||||
'start' => $start,
|
||||
'limit' => $limit,
|
||||
'filter' => $filter,
|
||||
'orderBy' => $orderBy,
|
||||
'orderByDir' => $orderByDir,
|
||||
'select' => ListController::SEGMENT_CONTACT_FIELDS,
|
||||
'route' => $route,
|
||||
],
|
||||
$entityJoinTable,
|
||||
$entityId,
|
||||
$contactFilter,
|
||||
$entityIdColumnName,
|
||||
$additionalJoins,
|
||||
$contactColumnName,
|
||||
$dateFrom,
|
||||
$dateTo
|
||||
);
|
||||
|
||||
// Normalize results regarding withTotalCount.
|
||||
if (isset($contacts['count'])) {
|
||||
$count = (int) $contacts['count'];
|
||||
} else {
|
||||
$contacts = [
|
||||
'results' => $contacts,
|
||||
'count' => $count,
|
||||
];
|
||||
}
|
||||
|
||||
if ($count && $count < ($start + 1)) {
|
||||
// the number of entities are now less then the current page so redirect to the last page
|
||||
$lastPage = $pageHelper->countPage($count);
|
||||
$pageHelper->rememberPage($lastPage);
|
||||
$returnUrl = $this->generateUrl($route, array_merge(['objectId' => $entityId, 'page' => $lastPage], $routeParameters));
|
||||
|
||||
return $this->postActionRedirect(
|
||||
[
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $lastPage, 'objectId' => $entityId],
|
||||
'contentTemplate' => '@MauticLead/Lead/grid.html.twig',
|
||||
'forwardController' => false,
|
||||
'passthroughVars' => [
|
||||
'mauticContent' => $sessionVar.'Contacts',
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$pageHelper->rememberPage($page);
|
||||
|
||||
// Get DNC for the contact
|
||||
$dnc = [];
|
||||
if ($dncChannel && $count > 0) {
|
||||
$dnc = $this->doctrine->getManager()->getRepository(\Mautic\LeadBundle\Entity\DoNotContact::class)->getChannelList(
|
||||
$dncChannel,
|
||||
array_keys($contacts['results'])
|
||||
);
|
||||
}
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'page' => $page,
|
||||
'items' => $contacts['results'],
|
||||
'totalItems' => $contacts['count'],
|
||||
'tmpl' => $sessionVar.'Contacts',
|
||||
'indexMode' => 'grid',
|
||||
'route' => $route,
|
||||
'routeParameters' => $routeParameters,
|
||||
'sessionVar' => $sessionVar.'.contact',
|
||||
'limit' => $limit,
|
||||
'objectId' => $entityId,
|
||||
'noContactList' => $dnc,
|
||||
'target' => $paginationTarget,
|
||||
],
|
||||
'contentTemplate' => '@MauticLead/Lead/grid.html.twig',
|
||||
'passthroughVars' => [
|
||||
'mauticContent' => $sessionVar.'Contacts',
|
||||
'route' => false,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,571 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Controller\FormController;
|
||||
use Mautic\CoreBundle\Exception\SchemaException;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Field\Exception\AbortColumnCreateException;
|
||||
use Mautic\LeadBundle\Field\Exception\AbortColumnUpdateException;
|
||||
use Mautic\LeadBundle\Helper\FieldAliasHelper;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class FieldController extends FormController
|
||||
{
|
||||
/**
|
||||
* Generate's default list view.
|
||||
*
|
||||
* @param int $page
|
||||
*
|
||||
* @return array|JsonResponse|RedirectResponse|Response
|
||||
*/
|
||||
public function indexAction(Request $request, FieldModel $fieldModel, $page = 1)
|
||||
{
|
||||
// set some permissions
|
||||
$permissions = $this->security->isGranted(['lead:fields:view', 'lead:fields:full'], 'RETURN_ARRAY');
|
||||
|
||||
$session = $request->getSession();
|
||||
|
||||
if (!$permissions['lead:fields:view'] && !$permissions['lead:fields:full']) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$this->setListFilters();
|
||||
|
||||
$limit = $session->get('mautic.leadfield.limit', $this->coreParametersHelper->get('default_pagelimit'));
|
||||
$search = $request->get('search', $session->get('mautic.leadfield.filter', ''));
|
||||
$session->set('mautic.leadfield.filter', $search);
|
||||
|
||||
// do some default filtering
|
||||
$orderBy = $request->getSession()->get('mautic.leadfield.orderby', 'f.order');
|
||||
$orderByDir = $request->getSession()->get('mautic.leadfield.orderbydir', 'ASC');
|
||||
|
||||
$start = (1 === $page) ? 0 : (($page - 1) * $limit);
|
||||
if ($start < 0) {
|
||||
$start = 0;
|
||||
}
|
||||
|
||||
$fields = $fieldModel->getEntities([
|
||||
'start' => $start,
|
||||
'limit' => $limit,
|
||||
'filter' => [
|
||||
'string' => $search,
|
||||
'force' => [
|
||||
[
|
||||
'column' => 'f.columnIsNotRemoved',
|
||||
'value' => false,
|
||||
'expr' => 'eq',
|
||||
],
|
||||
],
|
||||
],
|
||||
'orderBy' => $orderBy,
|
||||
'orderByDir' => $orderByDir,
|
||||
]);
|
||||
$count = count($fields);
|
||||
|
||||
if ($count && $count < ($start + 1)) {
|
||||
// the number of entities are now less then the current page so redirect to the last page
|
||||
if (1 === $count) {
|
||||
$lastPage = 1;
|
||||
} else {
|
||||
$lastPage = (ceil($count / $limit)) ?: 1;
|
||||
}
|
||||
$session->set('mautic.leadfield.page', $lastPage);
|
||||
$returnUrl = $this->generateUrl('mautic_contactfield_index', ['page' => $lastPage]);
|
||||
|
||||
return $this->postActionRedirect([
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $lastPage],
|
||||
'contentTemplate' => 'Mautic\LeadBundle\Controller\FieldController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_contactfield_index',
|
||||
'mauticContent' => 'leadfield',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// set what page currently on so that we can return here after form submission/cancellation
|
||||
$session->set('mautic.leadfield.page', $page);
|
||||
|
||||
$tmpl = $request->isXmlHttpRequest() ? $request->get('tmpl', 'index') : 'index';
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'items' => $fields,
|
||||
'searchValue' => $search,
|
||||
'permissions' => $permissions,
|
||||
'tmpl' => $tmpl,
|
||||
'totalItems' => $count,
|
||||
'limit' => $limit,
|
||||
'page' => $page,
|
||||
],
|
||||
'contentTemplate' => '@MauticLead/Field/list.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_contactfield_index',
|
||||
'route' => $this->generateUrl('mautic_contactfield_index', ['page' => $page]),
|
||||
'mauticContent' => 'leadfield',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate's new form and processes post data.
|
||||
*
|
||||
* @return JsonResponse|RedirectResponse|Response
|
||||
*/
|
||||
public function newAction(Request $request, ?LeadField $entity = null)
|
||||
{
|
||||
if (!$this->security->isGranted('lead:fields:full')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
// retrieve the entity
|
||||
$field = $entity instanceof LeadField ? $entity : new LeadField();
|
||||
|
||||
/** @var FieldModel $model */
|
||||
$model = $this->getModel('lead.field');
|
||||
// set the return URL for post actions
|
||||
$returnUrl = $this->generateUrl('mautic_contactfield_index');
|
||||
$action = $this->generateUrl('mautic_contactfield_action', ['objectAction' => 'new']);
|
||||
// get the user form factory
|
||||
$form = $model->createForm($field, $this->formFactory, $action);
|
||||
|
||||
// /Check for a submitted form and process it
|
||||
if ('POST' === $request->getMethod()) {
|
||||
$valid = false;
|
||||
if (!$cancelled = $this->isFormCancelled($form)) {
|
||||
if ($valid = $this->isFormValid($form)) {
|
||||
$requestData = $request->request->all();
|
||||
if (isset($requestData['leadfield']['properties'])) {
|
||||
$result = $model->setFieldProperties($field, $requestData['leadfield']['properties']);
|
||||
if (true !== $result) {
|
||||
// set the error
|
||||
$form->get('properties')->addError(
|
||||
new FormError(
|
||||
$this->translator->trans($result, [], 'validators')
|
||||
)
|
||||
);
|
||||
$valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($valid) {
|
||||
$flashMessage = 'mautic.core.notice.created';
|
||||
try {
|
||||
// form is valid so process the data
|
||||
$model->saveEntity($field);
|
||||
} catch (\Doctrine\DBAL\Exception $ee) {
|
||||
$flashMessage = $ee->getMessage();
|
||||
} catch (AbortColumnCreateException) {
|
||||
$flashMessage = $this->translator->trans('mautic.lead.field.pushed_to_background');
|
||||
} catch (SchemaException $e) {
|
||||
$flashMessage = $e->getMessage();
|
||||
$form['alias']->addError(new FormError($e->getMessage()));
|
||||
$valid = false;
|
||||
} catch (\Exception $e) {
|
||||
$form['alias']->addError(
|
||||
new FormError(
|
||||
$this->translator->trans('mautic.lead.field.failed', ['%error%' => $e->getMessage()], 'validators')
|
||||
)
|
||||
);
|
||||
$valid = false;
|
||||
}
|
||||
$this->addFlashMessage(
|
||||
$flashMessage,
|
||||
[
|
||||
'%name%' => $field->getLabel(),
|
||||
'%menu_link%' => 'mautic_contactfield_index',
|
||||
'%url%' => $this->generateUrl(
|
||||
'mautic_contactfield_action',
|
||||
[
|
||||
'objectAction' => 'edit',
|
||||
'objectId' => $field->getId(),
|
||||
]
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($cancelled || ($valid && $this->getFormButton($form, ['buttons', 'save'])->isClicked())) {
|
||||
return $this->postActionRedirect(
|
||||
[
|
||||
'returnUrl' => $returnUrl,
|
||||
'contentTemplate' => 'Mautic\LeadBundle\Controller\FieldController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_contactfield_index',
|
||||
'mauticContent' => 'leadfield',
|
||||
],
|
||||
]
|
||||
);
|
||||
} elseif ($valid && !$cancelled) {
|
||||
return $this->editAction($request, $field->getId(), true);
|
||||
} elseif (!$valid) {
|
||||
// some bug in Symfony prevents repopulating list options on errors
|
||||
$field = $form->getData();
|
||||
$newForm = $model->createForm($field, $this->formFactory, $action);
|
||||
$this->copyErrorsRecursively($form, $newForm);
|
||||
$form = $newForm;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'form' => $form->createView(),
|
||||
'leadField' => $entity,
|
||||
],
|
||||
'contentTemplate' => '@MauticLead/Field/form.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_contactfield_index',
|
||||
'route' => $this->generateUrl('mautic_contactfield_action', ['objectAction' => 'new']),
|
||||
'mauticContent' => 'leadfield',
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate's edit form and processes post data.
|
||||
*
|
||||
* @param bool|false $ignorePost
|
||||
*
|
||||
* @return array|JsonResponse|RedirectResponse|Response
|
||||
*/
|
||||
public function editAction(Request $request, $objectId, $ignorePost = false)
|
||||
{
|
||||
if (!$this->security->isGranted('lead:fields:full')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
/** @var FieldModel $model */
|
||||
$model = $this->getModel('lead.field');
|
||||
$field = $model->getEntity($objectId);
|
||||
|
||||
// set the return URL
|
||||
$returnUrl = $this->generateUrl('mautic_contactfield_index');
|
||||
|
||||
$postActionVars = [
|
||||
'returnUrl' => $returnUrl,
|
||||
'contentTemplate' => 'Mautic\LeadBundle\Controller\FieldController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_contactfield_index',
|
||||
'mauticContent' => 'leadfield',
|
||||
],
|
||||
];
|
||||
// list not found
|
||||
if (null === $field) {
|
||||
return $this->postActionRedirect(
|
||||
array_merge($postActionVars, [
|
||||
'flashes' => [
|
||||
[
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.lead.field.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
],
|
||||
],
|
||||
])
|
||||
);
|
||||
} elseif ($model->isLocked($field)) {
|
||||
// deny access if the entity is locked
|
||||
return $this->isLocked($postActionVars, $field, 'lead.field');
|
||||
}
|
||||
|
||||
$action = $this->generateUrl('mautic_contactfield_action', ['objectAction' => 'edit', 'objectId' => $objectId]);
|
||||
$form = $model->createForm($field, $this->formFactory, $action);
|
||||
|
||||
// /Check for a submitted form and process it
|
||||
if (!$ignorePost && 'POST' === $request->getMethod()) {
|
||||
$valid = false;
|
||||
if (!$cancelled = $this->isFormCancelled($form)) {
|
||||
if ($valid = $this->isFormValid($form)) {
|
||||
$requestData = $request->request->all();
|
||||
if (isset($requestData['leadfield']['properties'])) {
|
||||
$result = $model->setFieldProperties($field, $requestData['leadfield']['properties']);
|
||||
if (true !== $result) {
|
||||
// set the error
|
||||
$form->get('properties')->addError(new FormError(
|
||||
$this->translator->trans($result, [], 'validators')
|
||||
));
|
||||
$valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($valid) {
|
||||
$flashMessage = 'mautic.core.notice.updated';
|
||||
|
||||
// form is valid so process the data
|
||||
try {
|
||||
$model->saveEntity($field, $this->getFormButton($form, ['buttons', 'save'])->isClicked());
|
||||
} catch (AbortColumnUpdateException) {
|
||||
$flashMessage = $this->translator->trans('mautic.lead.field.update_pushed_to_background');
|
||||
} catch (SchemaException $e) {
|
||||
$flashMessage = $e->getMessage();
|
||||
$form['alias']->addError(new FormError($e->getMessage()));
|
||||
$valid = false;
|
||||
}
|
||||
|
||||
$this->addFlashMessage($flashMessage, [
|
||||
'%name%' => $field->getLabel(),
|
||||
'%menu_link%' => 'mautic_contactfield_index',
|
||||
'%url%' => $this->generateUrl('mautic_contactfield_action', [
|
||||
'objectAction' => 'edit',
|
||||
'objectId' => $field->getId(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// unlock the entity
|
||||
$model->unlockEntity($field);
|
||||
}
|
||||
|
||||
if ($cancelled || ($valid && $this->getFormButton($form, ['buttons', 'save'])->isClicked())) {
|
||||
return $this->postActionRedirect(
|
||||
array_merge($postActionVars, [
|
||||
'viewParameters' => ['objectId' => $field->getId()],
|
||||
'contentTemplate' => 'Mautic\LeadBundle\Controller\FieldController::indexAction',
|
||||
]
|
||||
)
|
||||
);
|
||||
} elseif ($valid) {
|
||||
// Rebuild the form with new action so that apply doesn't keep creating a clone
|
||||
$action = $this->generateUrl('mautic_contactfield_action', ['objectAction' => 'edit', 'objectId' => $field->getId()]);
|
||||
$form = $model->createForm($field, $this->formFactory, $action);
|
||||
} else {
|
||||
// some bug in Symfony prevents repopulating list options on errors
|
||||
$field = $form->getData();
|
||||
$newForm = $model->createForm($field, $this->formFactory, $action);
|
||||
$this->copyErrorsRecursively($form, $newForm);
|
||||
$form = $newForm;
|
||||
}
|
||||
} else {
|
||||
// lock the entity
|
||||
$model->lockEntity($field);
|
||||
}
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'form' => $form->createView(),
|
||||
],
|
||||
'contentTemplate' => '@MauticLead/Field/form.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_contactfield_index',
|
||||
'route' => $action,
|
||||
'mauticContent' => 'leadfield',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone an entity.
|
||||
*/
|
||||
public function cloneAction(Request $request, FieldAliasHelper $fieldAliasHelper, FieldModel $fieldModel, $objectId): RedirectResponse|Response
|
||||
{
|
||||
$entity = $fieldModel->getEntity($objectId);
|
||||
|
||||
if (!$entity) {
|
||||
throw $this->createNotFoundException('Entity not found');
|
||||
}
|
||||
|
||||
$clone = clone $entity;
|
||||
|
||||
$fieldAliasHelper->makeAliasUnique($clone);
|
||||
|
||||
$action = $this->generateUrl('mautic_contactfield_action', ['objectAction' => 'new']);
|
||||
$form = $fieldModel->createForm($clone, $this->formFactory, $action);
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'form' => $form->createView(),
|
||||
'leadField' => $clone,
|
||||
],
|
||||
'contentTemplate' => '@MauticLead/Field/form.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_contactfield_index',
|
||||
'route' => $this->generateUrl('mautic_contactfield_action', ['objectAction' => 'clone', 'objectId' => $objectId]),
|
||||
'mauticContent' => 'leadfield',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a field.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function deleteAction(Request $request, $objectId)
|
||||
{
|
||||
if (!$this->security->isGranted('lead:fields:full')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$returnUrl = $this->generateUrl('mautic_contactfield_index');
|
||||
$flashes = [];
|
||||
|
||||
$postActionVars = [
|
||||
'returnUrl' => $returnUrl,
|
||||
'contentTemplate' => 'Mautic\LeadBundle\Controller\FieldController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_contactfield_index',
|
||||
'mauticContent' => 'lead',
|
||||
],
|
||||
];
|
||||
|
||||
if ('POST' === $request->getMethod()) {
|
||||
/** @var FieldModel $model */
|
||||
$model = $this->getModel('lead.field');
|
||||
$field = $model->getEntity($objectId);
|
||||
|
||||
if (null === $field) {
|
||||
$flashes[] = [
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.lead.field.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
];
|
||||
} elseif ($model->isLocked($field)) {
|
||||
return $this->isLocked($postActionVars, $field, 'lead.field');
|
||||
} elseif ($field->isFixed()) {
|
||||
// cannot delete fixed fields
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$segments = [];
|
||||
foreach ($model->getFieldSegments($field) as $segment) {
|
||||
$segments[] = sprintf('"%s" (%d)', $segment->getName(), $segment->getId());
|
||||
}
|
||||
|
||||
if (count($segments)) {
|
||||
$flashMessage = [
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.core.notice.used.field',
|
||||
'msgVars' => [
|
||||
'%name%' => $field->getLabel(),
|
||||
'%id%' => $objectId,
|
||||
'%segments%' => implode(', ', $segments),
|
||||
],
|
||||
];
|
||||
} else {
|
||||
$model->deleteEntity($field);
|
||||
$flashMessage = [
|
||||
'type' => 'notice',
|
||||
'msg' => 'mautic.core.notice.deleted',
|
||||
'msgVars' => [
|
||||
'%name%' => $field->getLabel(),
|
||||
'%id%' => $objectId,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$flashes[] = $flashMessage;
|
||||
} // else don't do anything
|
||||
|
||||
return $this->postActionRedirect(
|
||||
array_merge($postActionVars, [
|
||||
'flashes' => $flashes,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a group of entities.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function batchDeleteAction(Request $request)
|
||||
{
|
||||
if (!$this->security->isGranted('lead:fields:full')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$returnUrl = $this->generateUrl('mautic_contactfield_index');
|
||||
$flashes = [];
|
||||
|
||||
$postActionVars = [
|
||||
'returnUrl' => $returnUrl,
|
||||
'contentTemplate' => 'Mautic\LeadBundle\Controller\FieldController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_contactfield_index',
|
||||
'mauticContent' => 'lead',
|
||||
],
|
||||
];
|
||||
|
||||
if ('POST' === $request->getMethod()) {
|
||||
/** @var FieldModel $model */
|
||||
$model = $this->getModel('lead.field');
|
||||
$ids = json_decode($request->query->get('ids', '{}'));
|
||||
$deleteIds = [];
|
||||
|
||||
// Loop over the IDs to perform access checks pre-delete
|
||||
foreach ($ids as $objectId) {
|
||||
$entity = $model->getEntity($objectId);
|
||||
|
||||
if (null === $entity) {
|
||||
$flashes[] = [
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.lead.field.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
];
|
||||
} elseif ($entity->isFixed()) {
|
||||
$flashes[] = $this->accessDenied(true);
|
||||
} elseif ($model->isLocked($entity)) {
|
||||
$flashes[] = $this->isLocked($postActionVars, $entity, 'lead.field', true);
|
||||
} else {
|
||||
$deleteIds[] = $objectId;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete everything we are able to
|
||||
if (!empty($deleteIds)) {
|
||||
$filteredDeleteIds = $model->filterUsedFieldIds($deleteIds);
|
||||
$usedFieldIds = array_diff($deleteIds, $filteredDeleteIds);
|
||||
$segments = [];
|
||||
$usedFieldsNames = [];
|
||||
|
||||
// Iterating through all used fileds to get segments they are used in
|
||||
foreach ($usedFieldIds as $usedFieldId) {
|
||||
$fieldEntity = $model->getEntity($usedFieldId);
|
||||
foreach ($model->getFieldSegments($fieldEntity) as $segment) {
|
||||
$segments[$segment->getId()] = sprintf('"%s" (%d)', $segment->getName(), $segment->getId());
|
||||
$usedFieldsNames[] = sprintf('"%s"', $fieldEntity->getName());
|
||||
}
|
||||
}
|
||||
|
||||
if ($filteredDeleteIds !== $deleteIds) {
|
||||
$flashes[] = [
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.core.notice.used.fields',
|
||||
'msgVars' => [
|
||||
'%segments%' => implode(', ', $segments),
|
||||
'%fields%' => implode(', ', array_unique($usedFieldsNames)),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
if (count($filteredDeleteIds)) {
|
||||
$entities = $model->deleteEntities($filteredDeleteIds);
|
||||
|
||||
$flashes[] = [
|
||||
'type' => 'notice',
|
||||
'msg' => 'mautic.lead.field.notice.batch_deleted',
|
||||
'msgVars' => [
|
||||
'%count%' => count($entities),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
} // else don't do anything
|
||||
|
||||
return $this->postActionRedirect(
|
||||
array_merge($postActionVars, [
|
||||
'flashes' => $flashes,
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Controller;
|
||||
|
||||
use Mautic\EmailBundle\Model\EmailModel;
|
||||
use Mautic\LeadBundle\Entity\DoNotContact;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Form\Type\ContactFrequencyType;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Symfony\Component\Form\Form;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
trait FrequencyRuleTrait
|
||||
{
|
||||
protected $leadLists;
|
||||
|
||||
protected $dncChannels;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $isPublicView = false;
|
||||
|
||||
private \Mautic\LeadBundle\Model\DoNotContact $doNotContactModel;
|
||||
|
||||
private ?RequestStack $requestStack = null;
|
||||
|
||||
/**
|
||||
* @param array $viewParameters
|
||||
* @param bool $isPublic
|
||||
* @param bool $isPreferenceCenter
|
||||
*
|
||||
* @return true|FormInterface
|
||||
*/
|
||||
protected function getFrequencyRuleForm($lead, &$viewParameters = [], &$data = null, $isPublic = false, $action = null, $isPreferenceCenter = false)
|
||||
{
|
||||
/** @var LeadModel $model */
|
||||
$model = $this->getModel('lead');
|
||||
|
||||
$leadChannels = $model->getContactChannels($lead);
|
||||
$allChannels = $model->getPreferenceChannels();
|
||||
$leadLists = $model->getLists($lead, true, true, $isPublic, $isPreferenceCenter);
|
||||
|
||||
$viewParameters = array_merge(
|
||||
$viewParameters,
|
||||
[
|
||||
'leadsLists' => $leadLists,
|
||||
'channels' => $allChannels,
|
||||
'leadChannels' => $leadChannels,
|
||||
]
|
||||
);
|
||||
|
||||
// find the email
|
||||
$currentChannelId = null;
|
||||
if (!empty($viewParameters['idHash'])) {
|
||||
$emailModel = $this->getModel('email');
|
||||
\assert($emailModel instanceof EmailModel);
|
||||
if ($stat = $emailModel->getEmailStatus($viewParameters['idHash'])) {
|
||||
if ($email = $stat->getEmail()) {
|
||||
$currentChannelId = $email->getId();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (null == $data) {
|
||||
$data = $this->getFrequencyRuleFormData($lead, $allChannels, $leadChannels, $isPublic, null, $isPreferenceCenter);
|
||||
}
|
||||
/** @var FormInterface $form */
|
||||
$form = $this->formFactory->create(
|
||||
ContactFrequencyType::class,
|
||||
$data,
|
||||
[
|
||||
'action' => $action,
|
||||
'channels' => $allChannels,
|
||||
'public_view' => $isPublic,
|
||||
'preference_center_only' => $isPreferenceCenter,
|
||||
'allow_extra_fields' => true,
|
||||
]
|
||||
);
|
||||
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
\assert(null !== $request);
|
||||
$method = $request->getMethod();
|
||||
if ('GET' !== $method) {
|
||||
if (!$this->isFormCancelled($form)) {
|
||||
if ($this->isFormValid($form)) {
|
||||
$this->persistFrequencyRuleFormData($lead, $form->getData(), $allChannels, $leadChannels, $currentChannelId);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $isPublic
|
||||
*/
|
||||
protected function getFrequencyRuleFormData(Lead $lead, ?array $allChannels = null, $leadChannels = null, $isPublic = false, $frequencyRules = null, $isPreferenceCenter = false): array
|
||||
{
|
||||
$data = [];
|
||||
|
||||
/** @var LeadModel $model */
|
||||
$model = $this->getModel('lead');
|
||||
if (null === $allChannels) {
|
||||
$allChannels = $model->getPreferenceChannels();
|
||||
}
|
||||
|
||||
if (null === $leadChannels) {
|
||||
$leadChannels = $model->getContactChannels($lead);
|
||||
}
|
||||
|
||||
if (null === $frequencyRules) {
|
||||
$frequencyRules = $model->getFrequencyRules($lead);
|
||||
}
|
||||
|
||||
foreach ($allChannels as $channel) {
|
||||
if (isset($frequencyRules[$channel])) {
|
||||
$frequencyRule = $frequencyRules[$channel];
|
||||
$data['lead_channels']['frequency_number_'.$channel] = $frequencyRule['frequency_number'];
|
||||
$data['lead_channels']['frequency_time_'.$channel] = $frequencyRule['frequency_time'];
|
||||
if ($frequencyRule['pause_from_date']) {
|
||||
$data['lead_channels']['contact_pause_start_date_'.$channel] = new \DateTime($frequencyRule['pause_from_date']);
|
||||
}
|
||||
|
||||
if ($frequencyRule['pause_to_date']) {
|
||||
$data['lead_channels']['contact_pause_end_date_'.$channel] = new \DateTime($frequencyRule['pause_to_date']);
|
||||
}
|
||||
|
||||
if (!empty($frequencyRule['preferred_channel'])) {
|
||||
$data['lead_channels']['preferred_channel'] = $channel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$data['global_categories'] = $frequencyRules['global_categories'] ?? $model->getSubscribedAndNewCategoryIds(
|
||||
$lead, ['global', 'email']);
|
||||
|
||||
$this->leadLists = $model->getLists($lead, false, false, $isPublic, $isPreferenceCenter);
|
||||
$data['lead_lists'] = [];
|
||||
foreach ($this->leadLists as $leadList) {
|
||||
$data['lead_lists'][] = $leadList->getId();
|
||||
}
|
||||
|
||||
$data['lead_channels']['subscribed_channels'] = $leadChannels;
|
||||
$this->isPublicView = $isPublic;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $currentChannelId
|
||||
*/
|
||||
protected function persistFrequencyRuleFormData(Lead $lead, array $formData, array $allChannels, $leadChannels, $currentChannelId = null)
|
||||
{
|
||||
/** @var LeadModel $leadModel */
|
||||
$leadModel = $this->getModel('lead.lead');
|
||||
|
||||
$dncModel = $this->doNotContactModel;
|
||||
\assert($dncModel instanceof \Mautic\LeadBundle\Model\DoNotContact);
|
||||
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
\assert(null !== $request);
|
||||
// iF subscribed_channels are enabled in form, then touch DNC
|
||||
if (isset($request->request->all()['lead_contact_frequency_rules']['lead_channels'])) {
|
||||
foreach ($formData['lead_channels']['subscribed_channels'] as $contactChannel) {
|
||||
if (!isset($leadChannels[$contactChannel])) {
|
||||
$contactable = $dncModel->isContactable($lead, $contactChannel);
|
||||
if (DoNotContact::UNSUBSCRIBED == $contactable || DoNotContact::MANUAL == $contactable) {
|
||||
$dncModel->removeDncForContact($lead->getId(), $contactChannel);
|
||||
}
|
||||
}
|
||||
}
|
||||
$dncChannels = array_diff($allChannels, $formData['lead_channels']['subscribed_channels']);
|
||||
foreach ($dncChannels as $channel) {
|
||||
if ($currentChannelId) {
|
||||
$channel = [$channel => $currentChannelId];
|
||||
}
|
||||
$dncModel->addDncForContact($lead->getId(), $channel, ($this->isPublicView) ? DoNotContact::UNSUBSCRIBED : DoNotContact::MANUAL, 'user');
|
||||
}
|
||||
}
|
||||
$leadModel->setFrequencyRules($lead, $formData, $this->leadLists);
|
||||
}
|
||||
|
||||
#[\Symfony\Contracts\Service\Attribute\Required]
|
||||
public function setDoNotContactModel(\Mautic\LeadBundle\Model\DoNotContact $doNotContactModel): void
|
||||
{
|
||||
$this->doNotContactModel = $doNotContactModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name is different, so it won't collide with other setters.
|
||||
*/
|
||||
#[\Symfony\Contracts\Service\Attribute\Required]
|
||||
public function setRequestStackObject(RequestStack $requestStack): void
|
||||
{
|
||||
$this->requestStack = $requestStack;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,730 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\CoreBundle\Controller\FormController;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\CsvHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Service\FlashBag;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\FormBundle\Helper\FormFieldHelper;
|
||||
use Mautic\LeadBundle\Entity\Import;
|
||||
use Mautic\LeadBundle\Event\ImportInitEvent;
|
||||
use Mautic\LeadBundle\Event\ImportMappingEvent;
|
||||
use Mautic\LeadBundle\Event\ImportValidateEvent;
|
||||
use Mautic\LeadBundle\Form\Type\LeadImportFieldType;
|
||||
use Mautic\LeadBundle\Form\Type\LeadImportType;
|
||||
use Mautic\LeadBundle\Helper\Progress;
|
||||
use Mautic\LeadBundle\LeadEvents;
|
||||
use Mautic\LeadBundle\Model\ImportModel;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\LogLevel;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Symfony\Component\Form\Exception\LogicException;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\File\Exception\FileException;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
class ImportController extends FormController
|
||||
{
|
||||
// Steps of the import
|
||||
public const STEP_UPLOAD_CSV = 1;
|
||||
|
||||
public const STEP_MATCH_FIELDS = 2;
|
||||
|
||||
public const STEP_PROGRESS_BAR = 3;
|
||||
|
||||
public const STEP_IMPORT_FROM_CSV = 4;
|
||||
|
||||
private ImportModel $importModel;
|
||||
|
||||
public function __construct(
|
||||
FormFactoryInterface $formFactory,
|
||||
FormFieldHelper $fieldHelper,
|
||||
private LoggerInterface $logger,
|
||||
ManagerRegistry $doctrine,
|
||||
ModelFactory $modelFactory,
|
||||
UserHelper $userHelper,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
Translator $translator,
|
||||
FlashBag $flashBag,
|
||||
private RequestStack $requestStack,
|
||||
CorePermissions $security,
|
||||
) {
|
||||
/** @var ImportModel $model */
|
||||
$model = $modelFactory->getModel($this->getModelName());
|
||||
|
||||
$this->importModel = $model;
|
||||
|
||||
parent::__construct($formFactory, $fieldHelper, $doctrine, $modelFactory, $userHelper, $coreParametersHelper, $dispatcher, $translator, $flashBag, $requestStack, $security);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $page
|
||||
*
|
||||
* @return JsonResponse|RedirectResponse
|
||||
*/
|
||||
public function indexAction(Request $request, $page = 1): Response
|
||||
{
|
||||
$initEvent = $this->dispatchImportOnInit();
|
||||
$this->requestStack->getSession()->set('mautic.import.object', $initEvent->objectSingular);
|
||||
|
||||
return $this->indexStandard($request, $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get items for index list.
|
||||
*
|
||||
* @param int $start
|
||||
* @param int $limit
|
||||
* @param mixed[] $filter
|
||||
* @param string $orderBy
|
||||
* @param string $orderByDir
|
||||
* @param mixed[] $args
|
||||
*/
|
||||
protected function getIndexItems($start, $limit, $filter, $orderBy, $orderByDir, array $args = []): array
|
||||
{
|
||||
$object = $this->requestStack->getSession()->get('mautic.import.object');
|
||||
|
||||
$filter['force'][] = [
|
||||
'column' => $this->importModel->getRepository()->getTableAlias().'.object',
|
||||
'expr' => 'eq',
|
||||
'value' => $object,
|
||||
];
|
||||
|
||||
$items = $this->importModel->getEntities(
|
||||
array_merge(
|
||||
[
|
||||
'start' => $start,
|
||||
'limit' => $limit,
|
||||
'filter' => $filter,
|
||||
'orderBy' => $orderBy,
|
||||
'orderByDir' => $orderByDir,
|
||||
],
|
||||
$args
|
||||
)
|
||||
);
|
||||
|
||||
$count = count($items);
|
||||
|
||||
return [$count, $items];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $objectId
|
||||
*
|
||||
* @return array|JsonResponse|RedirectResponse|Response
|
||||
*/
|
||||
public function viewAction(Request $request, $objectId)
|
||||
{
|
||||
return $this->viewStandard($request, $objectId, 'import', 'lead');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel and unpublish the import during manual import.
|
||||
*
|
||||
* @return JsonResponse|RedirectResponse
|
||||
*/
|
||||
public function cancelAction(Request $request): Response
|
||||
{
|
||||
$initEvent = $this->dispatchImportOnInit();
|
||||
$object = $initEvent->objectSingular;
|
||||
$fullPath = $this->getFullCsvPath($object);
|
||||
$import = $this->importModel->getEntity($this->requestStack->getSession()->get('mautic.lead.import.id', null));
|
||||
|
||||
if ($import && $import->getId()) {
|
||||
$import->setStatus($import::STOPPED)
|
||||
->setIsPublished(false);
|
||||
$this->importModel->saveEntity($import);
|
||||
}
|
||||
|
||||
$this->resetImport($object);
|
||||
$this->removeImportFile($fullPath);
|
||||
$this->logger->log(LogLevel::INFO, "Import for file {$fullPath} was canceled.");
|
||||
|
||||
return $this->indexAction($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules manual import to background queue.
|
||||
*/
|
||||
public function queueAction(Request $request): Response
|
||||
{
|
||||
$initEvent = $this->dispatchImportOnInit();
|
||||
$object = $initEvent->objectSingular;
|
||||
$fullPath = $this->getFullCsvPath($object);
|
||||
$import = $this->importModel->getEntity($this->requestStack->getSession()->get('mautic.lead.import.id', null));
|
||||
|
||||
if ($import) {
|
||||
$import->setStatus($import::QUEUED);
|
||||
$this->importModel->saveEntity($import);
|
||||
}
|
||||
|
||||
$this->resetImport($object);
|
||||
$this->logger->log(LogLevel::INFO, "Import for file {$fullPath} moved to be processed in the background.");
|
||||
|
||||
return $this->indexAction($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $objectId
|
||||
* @param bool $ignorePost
|
||||
*/
|
||||
public function newAction(Request $request, $objectId = 0, $ignorePost = false): Response
|
||||
{
|
||||
$dispatcher = $this->dispatcher;
|
||||
|
||||
try {
|
||||
$initEvent = $this->dispatchImportOnInit();
|
||||
} catch (AccessDeniedException $e) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
if (!$initEvent->objectSupported) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
$object = $initEvent->objectSingular;
|
||||
|
||||
$this->requestStack->getSession()->set('mautic.import.object', $object);
|
||||
|
||||
// Move the file to cache and rename it
|
||||
$forceStop = $request->get('cancel', false);
|
||||
$step = ($forceStop) ? self::STEP_UPLOAD_CSV : $this->requestStack->getSession()->get('mautic.'.$object.'.import.step', self::STEP_UPLOAD_CSV);
|
||||
$fileName = $this->getImportFileName($object);
|
||||
$importDir = $this->getImportDirName();
|
||||
$fullPath = $this->getFullCsvPath($object);
|
||||
$fs = new Filesystem();
|
||||
$complete = false;
|
||||
|
||||
if (!file_exists($fullPath) && self::STEP_UPLOAD_CSV !== $step) {
|
||||
// Force step one if the file doesn't exist
|
||||
$this->logger->log(LogLevel::WARNING, "File {$fullPath} does not exist anymore. Reseting import to step STEP_UPLOAD_CSV.");
|
||||
$this->addFlashMessage('mautic.import.file.missing', ['%file%' => $this->getImportFileName($object)], FlashBag::LEVEL_ERROR);
|
||||
$step = self::STEP_UPLOAD_CSV;
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.step', self::STEP_UPLOAD_CSV);
|
||||
}
|
||||
|
||||
$progress = (new Progress())->bindArray($this->requestStack->getSession()->get('mautic.'.$object.'.import.progress', [0, 0]));
|
||||
$import = $this->importModel->getEntity();
|
||||
$action = $this->generateUrl('mautic_import_action', ['object' => $request->get('object'), 'objectAction' => 'new']);
|
||||
|
||||
switch ($step) {
|
||||
case self::STEP_UPLOAD_CSV:
|
||||
if ($forceStop) {
|
||||
$this->resetImport($object);
|
||||
$this->removeImportFile($fullPath);
|
||||
$this->logger->log(LogLevel::WARNING, "Import for file {$fullPath} was force-stopped.");
|
||||
}
|
||||
|
||||
$form = $this->formFactory->create(LeadImportType::class, [], ['action' => $action]);
|
||||
break;
|
||||
case self::STEP_MATCH_FIELDS:
|
||||
$mappingEvent = $dispatcher->dispatch(
|
||||
new ImportMappingEvent($request->get('object')),
|
||||
LeadEvents::IMPORT_ON_FIELD_MAPPING
|
||||
);
|
||||
|
||||
try {
|
||||
$form = $this->formFactory->create(
|
||||
LeadImportFieldType::class,
|
||||
[],
|
||||
[
|
||||
'object' => $object,
|
||||
'action' => $action,
|
||||
'all_fields' => $mappingEvent->fields,
|
||||
'import_fields' => $this->requestStack->getSession()->get('mautic.'.$object.'.import.importfields', []),
|
||||
'line_count_limit' => $this->getLineCountLimit(),
|
||||
]
|
||||
);
|
||||
} catch (LogicException $e) {
|
||||
$this->resetImport($object);
|
||||
$this->removeImportFile($fullPath);
|
||||
$this->logger->log(LogLevel::INFO, "Import for file {$fullPath} failed with: {$e->getMessage()}.");
|
||||
|
||||
return $this->newAction($request, 0, true);
|
||||
}
|
||||
|
||||
break;
|
||||
case self::STEP_PROGRESS_BAR:
|
||||
// Just show the progress form
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.step', self::STEP_IMPORT_FROM_CSV);
|
||||
break;
|
||||
|
||||
case self::STEP_IMPORT_FROM_CSV:
|
||||
ignore_user_abort(true);
|
||||
|
||||
$inProgress = $this->requestStack->getSession()->get('mautic.'.$object.'.import.inprogress', false);
|
||||
$checks = $this->requestStack->getSession()->get('mautic.'.$object.'.import.progresschecks', 1);
|
||||
if (!$inProgress || $checks > 5) {
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.inprogress', true);
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.progresschecks', 1);
|
||||
|
||||
$import = $this->importModel->getEntity($this->requestStack->getSession()->get('mautic.'.$object.'.import.id', null));
|
||||
|
||||
if (!$import->getDateStarted()) {
|
||||
$import->setDateStarted(new \DateTime());
|
||||
}
|
||||
|
||||
$this->importModel->process($import, $progress);
|
||||
|
||||
// Clear in progress
|
||||
if ($progress->isFinished()) {
|
||||
$import->setStatus($import::IMPORTED)
|
||||
->setDateEnded(new \DateTime());
|
||||
$this->resetImport($object);
|
||||
$this->removeImportFile($fullPath);
|
||||
$complete = true;
|
||||
} else {
|
||||
$complete = false;
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.inprogress', false);
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.progress', $progress->toArray());
|
||||
}
|
||||
|
||||
$this->importModel->saveEntity($import);
|
||||
|
||||
break;
|
||||
} else {
|
||||
++$checks;
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.progresschecks', $checks);
|
||||
}
|
||||
}
|
||||
|
||||
// /Check for a submitted form and process it
|
||||
if (!$ignorePost && 'POST' === $request->getMethod()) {
|
||||
if (!isset($form) || $this->isFormCancelled($form)) {
|
||||
$this->resetImport($object);
|
||||
$this->removeImportFile($fullPath);
|
||||
$reason = isset($form) ? 'the form is empty' : 'the form was canceled';
|
||||
$this->logger->log(LogLevel::WARNING, "Import for file {$fullPath} was aborted because {$reason}.");
|
||||
|
||||
return $this->newAction($request, 0, true);
|
||||
}
|
||||
|
||||
$valid = $this->isFormValid($form);
|
||||
switch ($step) {
|
||||
case self::STEP_UPLOAD_CSV:
|
||||
if ($valid) {
|
||||
if (file_exists($fullPath)) {
|
||||
unlink($fullPath);
|
||||
}
|
||||
|
||||
$fileData = $form['file']->getData();
|
||||
if (!empty($fileData)) {
|
||||
$errorMessage = null;
|
||||
$errorParameters = [];
|
||||
try {
|
||||
// Create the import dir recursively
|
||||
$fs->mkdir($importDir);
|
||||
|
||||
$fileData->move($importDir, $fileName);
|
||||
|
||||
$file = new \SplFileObject($fullPath);
|
||||
|
||||
$config = $form->getData();
|
||||
unset($config['file']);
|
||||
unset($config['start']);
|
||||
|
||||
foreach ($config as $key => &$c) {
|
||||
$c = htmlspecialchars_decode($c);
|
||||
|
||||
if ('batchlimit' == $key) {
|
||||
$c = (int) $c;
|
||||
}
|
||||
}
|
||||
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.config', $config);
|
||||
|
||||
// Get the headers for matching
|
||||
$headers = $file->fgetcsv($config['delimiter'], $config['enclosure'], $config['escape']);
|
||||
|
||||
// Get the number of lines so we can track progress
|
||||
$file->seek(PHP_INT_MAX);
|
||||
$linecount = $file->key();
|
||||
|
||||
if (!empty($headers) && is_array($headers)) {
|
||||
$headers = CsvHelper::sanitizeHeaders($headers);
|
||||
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.headers', $headers);
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.step', self::STEP_MATCH_FIELDS);
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.importfields', CsvHelper::convertHeadersIntoFields($headers));
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.progress', [0, $linecount]);
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.original.file', $fileData->getClientOriginalName());
|
||||
|
||||
return $this->newAction($request, 0, true);
|
||||
}
|
||||
} catch (FileException $e) {
|
||||
if (str_contains($e->getMessage(), 'upload_max_filesize')) {
|
||||
$errorMessage = 'mautic.lead.import.filetoolarge';
|
||||
$errorParameters = [
|
||||
'%upload_max_filesize%' => ini_get('upload_max_filesize'),
|
||||
];
|
||||
} else {
|
||||
$errorMessage = 'mautic.lead.import.filenotreadable';
|
||||
}
|
||||
} catch (\Exception) {
|
||||
$errorMessage = 'mautic.lead.import.filenotreadable';
|
||||
} finally {
|
||||
if (!is_null($errorMessage)) {
|
||||
$form->addError(
|
||||
new FormError(
|
||||
$this->translator->trans($errorMessage, $errorParameters, 'validators')
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case self::STEP_MATCH_FIELDS:
|
||||
$validateEvent = new ImportValidateEvent($request->get('object'), $form);
|
||||
|
||||
$dispatcher->dispatch($validateEvent, LeadEvents::IMPORT_ON_VALIDATE);
|
||||
|
||||
if ($validateEvent->hasErrors()) {
|
||||
break;
|
||||
}
|
||||
|
||||
$matchedFields = $validateEvent->getMatchedFields();
|
||||
|
||||
if (empty($matchedFields)) {
|
||||
$this->resetImport($object);
|
||||
$this->removeImportFile($fullPath);
|
||||
$this->logger->log(LogLevel::WARNING, "Import for file {$fullPath} was aborted as there were no matched files found.");
|
||||
|
||||
return $this->newAction($request, 0, true);
|
||||
}
|
||||
|
||||
/** @var Import $import */
|
||||
$import = $this->importModel->getEntity();
|
||||
|
||||
$import->setMatchedFields($matchedFields)
|
||||
->setObject($object)
|
||||
->setDir($importDir)
|
||||
->setLineCount($this->getLineCount($object))
|
||||
->setFile($fileName)
|
||||
->setOriginalFile($this->requestStack->getSession()->get('mautic.'.$object.'.import.original.file'))
|
||||
->setDefault('owner', $validateEvent->getOwnerId())
|
||||
->setDefault('list', $validateEvent->getList())
|
||||
->setDefault('tags', $validateEvent->getTags())
|
||||
->setDefault('skip_if_exists', $validateEvent->getSkipIfExists())
|
||||
->setHeaders($this->requestStack->getSession()->get('mautic.'.$object.'.import.headers'))
|
||||
->setParserConfig($this->requestStack->getSession()->get('mautic.'.$object.'.import.config'));
|
||||
|
||||
$successMessage = 'mautic.lead.batch.import.created';
|
||||
if (!$this->security->isGranted($this->getPermissionBase().':publish')) {
|
||||
$import->setIsPublished(false);
|
||||
$successMessage = 'mautic.lead.batch.import.created.unpublished';
|
||||
}
|
||||
|
||||
// In case the user chose to import in browser
|
||||
if ($this->importInBrowser($form, $object)) {
|
||||
$import->setStatus($import::MANUAL);
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.step', self::STEP_PROGRESS_BAR);
|
||||
}
|
||||
$this->importModel->saveEntity($import);
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.id', $import->getId());
|
||||
// In case the user decided to queue the import
|
||||
if ($this->importInCli($form, $object)) {
|
||||
$this->addFlashMessage($successMessage);
|
||||
$this->resetImport($object);
|
||||
|
||||
return $this->indexAction($request);
|
||||
}
|
||||
|
||||
return $this->newAction($request, 0, true);
|
||||
default:
|
||||
// Done or something wrong
|
||||
|
||||
$this->resetImport($object);
|
||||
$this->removeImportFile($fullPath);
|
||||
$this->logger->log(LogLevel::ERROR, "Import for file {$fullPath} was aborted for unknown step of '{$step}'");
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (self::STEP_UPLOAD_CSV === $step || self::STEP_MATCH_FIELDS === $step) {
|
||||
$contentTemplate = '@MauticLead/Import/new.html.twig';
|
||||
$viewParameters = [
|
||||
'form' => $form->createView(),
|
||||
'objectName' => $initEvent->objectName,
|
||||
];
|
||||
} else {
|
||||
$contentTemplate = '@MauticLead/Import/progress.html.twig';
|
||||
$viewParameters = [
|
||||
'progress' => $progress,
|
||||
'import' => $import,
|
||||
'complete' => $complete,
|
||||
'failedRows' => $this->importModel->getFailedRows($import->getId(), $import->getObject()),
|
||||
'objectName' => $initEvent->objectName,
|
||||
'indexRoute' => $initEvent->indexRoute,
|
||||
'indexRouteParams' => $initEvent->indexRouteParams,
|
||||
];
|
||||
}
|
||||
|
||||
if (!$complete && $request->query->has('importbatch')) {
|
||||
// Ajax request to batch process so just return ajax response unless complete
|
||||
|
||||
$response = new JsonResponse(['success' => 1, 'ignore_wdt' => 1]);
|
||||
} else {
|
||||
$viewParameters['step'] = $step;
|
||||
|
||||
$response = $this->delegateView(
|
||||
[
|
||||
'viewParameters' => $viewParameters,
|
||||
'contentTemplate' => $contentTemplate,
|
||||
'passthroughVars' => [
|
||||
'activeLink' => $initEvent->activeLink,
|
||||
'mauticContent' => 'leadImport',
|
||||
'route' => $this->generateUrl(
|
||||
'mautic_import_action',
|
||||
[
|
||||
'object' => $initEvent->routeObjectName,
|
||||
'objectAction' => 'new',
|
||||
]
|
||||
),
|
||||
'step' => $step,
|
||||
'progress' => $progress,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
// For uploading file Keep-Alive should not be used.
|
||||
$response->headers->set('Connection', 'close');
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns line count from the session.
|
||||
*
|
||||
* @param string $object
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function getLineCount($object)
|
||||
{
|
||||
$progress = $this->requestStack->getSession()->get('mautic.'.$object.'.import.progress', [0, 0]);
|
||||
|
||||
return $progress[1] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide whether the import will be processed in client's browser.
|
||||
*
|
||||
* @param FormInterface<mixed> $form
|
||||
* @param string $object
|
||||
*/
|
||||
protected function importInBrowser(FormInterface $form, $object): bool
|
||||
{
|
||||
$browserImportLimit = $this->getLineCountLimit();
|
||||
|
||||
if ($browserImportLimit && $this->getLineCount($object) < $browserImportLimit) {
|
||||
return true;
|
||||
} elseif (!$browserImportLimit && $this->getFormButton($form, ['buttons', 'save'])->isClicked()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function getLineCountLimit()
|
||||
{
|
||||
return $this->coreParametersHelper->get('background_import_if_more_rows_than', 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide whether the import will be queued to be processed by the CLI command in the background.
|
||||
*
|
||||
* @param FormInterface<mixed> $form
|
||||
* @param string $object
|
||||
*/
|
||||
protected function importInCli(FormInterface $form, $object): bool
|
||||
{
|
||||
$browserImportLimit = $this->getLineCountLimit();
|
||||
|
||||
if ($browserImportLimit && $this->getLineCount($object) >= $browserImportLimit) {
|
||||
return true;
|
||||
} elseif (!$browserImportLimit && $this->getFormButton($form, ['buttons', 'apply'])->isClicked()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates import directory path.
|
||||
*/
|
||||
protected function getImportDirName(): string
|
||||
{
|
||||
return $this->importModel->getImportDir();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates unique import directory name inside the cache dir if not stored in the session.
|
||||
* If it exists in the session, returns that one.
|
||||
*
|
||||
* @param string $object
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getImportFileName($object)
|
||||
{
|
||||
// Return the dir path from session if exists
|
||||
if ($fileName = $this->requestStack->getSession()->get('mautic.'.$object.'.import.file')) {
|
||||
return $fileName;
|
||||
}
|
||||
|
||||
$fileName = $this->importModel->getUniqueFileName();
|
||||
|
||||
// Set the dir path to session
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.file', $fileName);
|
||||
|
||||
return $fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return full absolute path to the CSV file.
|
||||
*
|
||||
* @param string $object
|
||||
*/
|
||||
protected function getFullCsvPath($object): string
|
||||
{
|
||||
return $this->getImportDirName().'/'.$this->getImportFileName($object);
|
||||
}
|
||||
|
||||
private function resetImport(string $object): void
|
||||
{
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.headers', []);
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.file', null);
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.step', self::STEP_UPLOAD_CSV);
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.progress', [0, 0]);
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.inprogress', false);
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.importfields', []);
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.original.file', null);
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.id', null);
|
||||
}
|
||||
|
||||
private function removeImportFile(string $filepath): void
|
||||
{
|
||||
if (file_exists($filepath) && is_readable($filepath)) {
|
||||
unlink($filepath);
|
||||
|
||||
$this->logger->log(LogLevel::WARNING, "File {$filepath} was removed.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getViewArguments(array $args, $action): array
|
||||
{
|
||||
switch ($action) {
|
||||
case 'view':
|
||||
/** @var Import $entity */
|
||||
$entity = $args['entity'];
|
||||
|
||||
$args['viewParameters'] = array_merge(
|
||||
$args['viewParameters'],
|
||||
[
|
||||
'failedRows' => $this->importModel->getFailedRows($entity->getId(), $entity->getObject()),
|
||||
'importedRowsChart' => $entity->getDateStarted() ? $this->importModel->getImportedRowsLineChartData(
|
||||
'i',
|
||||
$entity->getDateStarted(),
|
||||
$entity->getDateEnded() ?: $entity->getDateModified(),
|
||||
null,
|
||||
[
|
||||
'object_id' => $entity->getId(),
|
||||
]
|
||||
) : [],
|
||||
]
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Support non-index pages such as modal forms.
|
||||
*/
|
||||
protected function generateUrl(string $route, array $parameters = [], int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): string
|
||||
{
|
||||
if (!isset($parameters['object'])) {
|
||||
$request = $this->getCurrentRequest();
|
||||
$parameters['object'] = $request->get('object', 'contacts');
|
||||
}
|
||||
|
||||
return parent::generateUrl($route, $parameters, $referenceType);
|
||||
}
|
||||
|
||||
protected function getModelName(): string
|
||||
{
|
||||
return 'lead.import';
|
||||
}
|
||||
|
||||
protected function getSessionBase($objectId = null): string
|
||||
{
|
||||
$initEvent = $this->dispatchImportOnInit();
|
||||
$object = $initEvent->objectSingular;
|
||||
|
||||
return $object.'.import'.(($objectId) ? '.'.$objectId : '');
|
||||
}
|
||||
|
||||
protected function getPermissionBase()
|
||||
{
|
||||
return $this->getModel($this->getModelName())->getPermissionBase();
|
||||
}
|
||||
|
||||
protected function getRouteBase(): string
|
||||
{
|
||||
return 'import';
|
||||
}
|
||||
|
||||
protected function getTemplateBase(): string
|
||||
{
|
||||
return '@MauticLead/Import';
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide the name of the column which is used for default ordering.
|
||||
*/
|
||||
protected function getDefaultOrderColumn(): string
|
||||
{
|
||||
return 'dateAdded';
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide the direction for default ordering.
|
||||
*/
|
||||
protected function getDefaultOrderDirection(): string
|
||||
{
|
||||
return 'DESC';
|
||||
}
|
||||
|
||||
private function dispatchImportOnInit(): ImportInitEvent
|
||||
{
|
||||
$request = $this->getCurrentRequest();
|
||||
$event = new ImportInitEvent($request->get('object'));
|
||||
|
||||
$this->dispatcher->dispatch($event, LeadEvents::IMPORT_ON_INITIALIZE);
|
||||
|
||||
return $event;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller;
|
||||
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
trait LeadAccessTrait
|
||||
{
|
||||
/**
|
||||
* Determines if the user has access to the lead the note is for.
|
||||
*
|
||||
* @param bool $isPlugin
|
||||
*
|
||||
* @return Response|Lead
|
||||
*/
|
||||
protected function checkLeadAccess($leadId, $action, $isPlugin = false, $integration = '')
|
||||
{
|
||||
if (!$leadId instanceof Lead) {
|
||||
// make sure the user has view access to this lead
|
||||
$leadModel = $this->getModel('lead');
|
||||
$lead = $leadModel->getEntity((int) $leadId);
|
||||
} else {
|
||||
$lead = $leadId;
|
||||
$leadId = $lead->getId();
|
||||
}
|
||||
|
||||
if (null === $lead || !$lead->getId()) {
|
||||
if (method_exists($this, 'postActionRedirect')) {
|
||||
// set the return URL
|
||||
$page = $this->getCurrentRequest()->getSession()->get($isPlugin ? 'mautic.'.$integration.'.page' : 'mautic.lead.page', 1);
|
||||
$returnUrl = $this->generateUrl($isPlugin ? 'mautic_plugin_timeline_index' : 'mautic_contact_index', ['page' => $page]);
|
||||
|
||||
return $this->postActionRedirect(
|
||||
[
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $page],
|
||||
'contentTemplate' => $isPlugin ? 'Mautic\LeadBundle\Controller\LeadController::pluginIndexAction' : 'Mautic\LeadBundle\Controller\LeadController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => $isPlugin ? '#mautic_plugin_timeline_index' : '#mautic_contact_index',
|
||||
'mauticContent' => 'leadTimeline',
|
||||
],
|
||||
'flashes' => [
|
||||
[
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.lead.lead.error.notfound',
|
||||
'msgVars' => ['%id%' => $leadId],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
} else {
|
||||
return $this->notFound('mautic.contact.error.notfound');
|
||||
}
|
||||
} elseif (!$this->security->hasEntityAccess(
|
||||
'lead:leads:'.$action.'own',
|
||||
'lead:leads:'.$action.'other',
|
||||
$lead->getPermissionUser()
|
||||
)
|
||||
) {
|
||||
return $this->accessDenied();
|
||||
} else {
|
||||
return $lead;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns leads the user has access to.
|
||||
*
|
||||
* @return array|\Symfony\Component\HttpFoundation\RedirectResponse
|
||||
*/
|
||||
protected function checkAllAccess($action, $limit)
|
||||
{
|
||||
/** @var LeadModel $model */
|
||||
$model = $this->getModel('lead');
|
||||
|
||||
// make sure the user has view access to leads
|
||||
$repo = $model->getRepository();
|
||||
|
||||
// order by lastactive, filter
|
||||
$leads = $repo->getEntities(
|
||||
[
|
||||
'filter' => [
|
||||
'force' => [
|
||||
[
|
||||
'column' => 'l.date_identified',
|
||||
'expr' => 'isNotNull',
|
||||
],
|
||||
],
|
||||
],
|
||||
'oderBy' => 'r.last_active',
|
||||
'orderByDir' => 'DESC',
|
||||
'limit' => $limit,
|
||||
'hydration_mode' => 'HYDRATE_ARRAY',
|
||||
]);
|
||||
|
||||
if (null === $leads) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
foreach ($leads as $lead) {
|
||||
if (!$this->security->hasEntityAccess(
|
||||
'lead:leads:'.$action.'own',
|
||||
'lead:leads:'.$action.'other',
|
||||
$lead->getOwner()
|
||||
)
|
||||
) {
|
||||
unset($lead);
|
||||
}
|
||||
}
|
||||
|
||||
return $leads;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,394 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Entity\AuditLogRepository;
|
||||
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
|
||||
use Mautic\CoreBundle\Helper\Chart\LineChart;
|
||||
use Mautic\CoreBundle\Model\AuditLogModel;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
trait LeadDetailsTrait
|
||||
{
|
||||
private ?RequestStack $requestStack = null;
|
||||
|
||||
/**
|
||||
* @param int $page
|
||||
*/
|
||||
protected function getAllEngagements(array $leads, ?array $filters = null, ?array $orderBy = null, $page = 1, $limit = 25): array
|
||||
{
|
||||
$session = $this->requestStack->getCurrentRequest()->getSession();
|
||||
|
||||
if (null == $filters) {
|
||||
$filters = $session->get(
|
||||
'mautic.plugin.timeline.filters',
|
||||
[
|
||||
'search' => '',
|
||||
'includeEvents' => [],
|
||||
'excludeEvents' => [],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if (null == $orderBy) {
|
||||
if (!$session->has('mautic.plugin.timeline.orderby')) {
|
||||
$session->set('mautic.plugin.timeline.orderby', 'timestamp');
|
||||
$session->set('mautic.plugin.timeline.orderbydir', 'DESC');
|
||||
}
|
||||
|
||||
$orderBy = [
|
||||
$session->get('mautic.plugin.timeline.orderby'),
|
||||
$session->get('mautic.plugin.timeline.orderbydir'),
|
||||
];
|
||||
}
|
||||
|
||||
// prepare result object
|
||||
$result = [
|
||||
'events' => [],
|
||||
'filters' => $filters,
|
||||
'order' => $orderBy,
|
||||
'types' => [],
|
||||
'total' => 0,
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
'maxPages' => 0,
|
||||
];
|
||||
|
||||
// get events for each contact
|
||||
foreach ($leads as $lead) {
|
||||
// if (!$lead->getEmail()) continue; // discard contacts without email
|
||||
|
||||
/** @var LeadModel $model */
|
||||
$model = $this->getModel('lead');
|
||||
$engagements = $model->getEngagements($lead, $filters, $orderBy, $page, $limit);
|
||||
$events = $engagements['events'];
|
||||
$types = $engagements['types'];
|
||||
|
||||
// inject lead into events
|
||||
foreach ($events as &$event) {
|
||||
$event['leadId'] = $lead->getId();
|
||||
$event['leadEmail'] = $lead->getEmail();
|
||||
$event['leadName'] = $lead->getName() ?: $lead->getEmail();
|
||||
}
|
||||
|
||||
$result['events'] = array_merge($result['events'], $events);
|
||||
$result['types'] = array_merge($result['types'], $types);
|
||||
$result['total'] += $engagements['total'];
|
||||
}
|
||||
|
||||
$result['maxPages'] = ($limit <= 0) ? 1 : round(ceil($result['total'] / $limit));
|
||||
|
||||
usort($result['events'], [$this, 'cmp']); // sort events by
|
||||
|
||||
// now all events are merged, let's limit to $limit
|
||||
array_splice($result['events'], $limit);
|
||||
|
||||
$result['total'] = count($result['events']);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure that the event filter array is in the right format.
|
||||
*
|
||||
* @param mixed $filters
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws InvalidArgumentException if not an array
|
||||
*/
|
||||
public function sanitizeEventFilter($filters)
|
||||
{
|
||||
if (!is_array($filters)) {
|
||||
throw new \InvalidArgumentException('filters parameter must be an array');
|
||||
}
|
||||
|
||||
if (!isset($filters['search'])) {
|
||||
$filters['search'] = '';
|
||||
}
|
||||
|
||||
if (!isset($filters['includeEvents'])) {
|
||||
$filters['includeEvents'] = [];
|
||||
}
|
||||
|
||||
if (!isset($filters['excludeEvents'])) {
|
||||
$filters['excludeEvents'] = [];
|
||||
}
|
||||
|
||||
return $filters;
|
||||
}
|
||||
|
||||
private function cmp($a, $b): int
|
||||
{
|
||||
return $b['timestamp'] <=> $a['timestamp'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of places for the lead based on IP location.
|
||||
*/
|
||||
protected function getPlaces(Lead $lead): array
|
||||
{
|
||||
// Get Places from IP addresses
|
||||
$places = [];
|
||||
if ($lead->getIpAddresses()->count() > 0) {
|
||||
foreach ($lead->getIpAddresses() as $ip) {
|
||||
if ($details = $ip->getIpDetails()) {
|
||||
if (!empty($details['latitude']) && !empty($details['longitude'])) {
|
||||
$name = 'N/A';
|
||||
if (!empty($details['city'])) {
|
||||
$name = $details['city'];
|
||||
} elseif (!empty($details['region'])) {
|
||||
$name = $details['region'];
|
||||
}
|
||||
$place = [
|
||||
'latLng' => [$details['latitude'], $details['longitude']],
|
||||
'name' => $name,
|
||||
];
|
||||
$places[] = $place;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $places;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
protected function getEngagementData(Lead $lead, ?\DateTime $fromDate = null, ?\DateTime $toDate = null): array
|
||||
{
|
||||
$translator = $this->translator;
|
||||
|
||||
if (null == $fromDate) {
|
||||
$fromDate = new \DateTime('first day of this month 00:00:00');
|
||||
$fromDate->modify('-6 months');
|
||||
}
|
||||
if (null == $toDate) {
|
||||
$toDate = new \DateTime();
|
||||
}
|
||||
|
||||
$lineChart = new LineChart(null, $fromDate, $toDate);
|
||||
$chartQuery = new ChartQuery($this->doctrine->getConnection(), $fromDate, $toDate);
|
||||
|
||||
/** @var LeadModel $model */
|
||||
$model = $this->getModel('lead');
|
||||
$engagements = $model->getEngagementCount($lead, $fromDate, $toDate, 'm', $chartQuery);
|
||||
$lineChart->setDataset($translator->trans('mautic.lead.graph.line.all_engagements'), $engagements['byUnit']);
|
||||
|
||||
$pointStats = $chartQuery->fetchSumTimeData('lead_points_change_log', 'date_added', ['lead_id' => $lead->getId()], 'delta');
|
||||
$lineChart->setDataset($translator->trans('mautic.lead.graph.line.points'), $pointStats);
|
||||
|
||||
return $lineChart->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
protected function getAuditlogs(Lead $lead, ?array $filters = null, ?array $orderBy = null, int $page = 1, int $limit = 25): array
|
||||
{
|
||||
$session = $this->requestStack->getCurrentRequest()->getSession();
|
||||
|
||||
if (null == $filters) {
|
||||
$filters = $session->get(
|
||||
'mautic.lead.'.$lead->getId().'.auditlog.filters',
|
||||
[
|
||||
'search' => '',
|
||||
'includeEvents' => [],
|
||||
'excludeEvents' => [],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if (null == $orderBy) {
|
||||
if (!$session->has('mautic.lead.'.$lead->getId().'.auditlog.orderby')) {
|
||||
$session->set('mautic.lead.'.$lead->getId().'.auditlog.orderby', 'al.dateAdded');
|
||||
$session->set('mautic.lead.'.$lead->getId().'.auditlog.orderbydir', 'DESC');
|
||||
}
|
||||
|
||||
$orderBy = [
|
||||
$session->get('mautic.lead.'.$lead->getId().'.auditlog.orderby'),
|
||||
$session->get('mautic.lead.'.$lead->getId().'.auditlog.orderbydir'),
|
||||
];
|
||||
}
|
||||
|
||||
// Audit Log
|
||||
/** @var AuditLogModel $auditlogModel */
|
||||
$auditlogModel = $this->getModel('core.auditlog');
|
||||
/** @var AuditLogRepository $repo */
|
||||
$repo = $auditlogModel->getRepository();
|
||||
$logCount = $repo->getAuditLogsCount($lead, $filters);
|
||||
$logs = $repo->getAuditLogs($lead, $filters, $orderBy, $page, $limit);
|
||||
|
||||
$logEvents = array_map(fn ($l): array => [
|
||||
'eventType' => $l['action'],
|
||||
'eventLabel' => $l['userName'],
|
||||
'timestamp' => $l['dateAdded'],
|
||||
'details' => $l['details'],
|
||||
'contentTemplate' => '@MauticLead/Auditlog/details.html.twig',
|
||||
], $logs);
|
||||
|
||||
$types = [
|
||||
'delete' => $this->translator->trans('mautic.lead.event.delete'),
|
||||
'create' => $this->translator->trans('mautic.lead.event.create'),
|
||||
'identified' => $this->translator->trans('mautic.lead.event.identified'),
|
||||
'ipadded' => $this->translator->trans('mautic.lead.event.ipadded'),
|
||||
'merge' => $this->translator->trans('mautic.lead.event.merge'),
|
||||
'update' => $this->translator->trans('mautic.lead.event.update'),
|
||||
];
|
||||
|
||||
return [
|
||||
'events' => $logEvents,
|
||||
'filters' => $filters,
|
||||
'order' => $orderBy,
|
||||
'types' => $types,
|
||||
'total' => $logCount,
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
'maxPages' => ceil($logCount / $limit),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $page
|
||||
* @param int $limit
|
||||
*/
|
||||
protected function getEngagements(Lead $lead, ?array $filters = null, ?array $orderBy = null, $page = 1, $limit = 25): array
|
||||
{
|
||||
$session = $this->requestStack->getCurrentRequest()->getSession();
|
||||
|
||||
if (null == $filters) {
|
||||
$filters = $session->get(
|
||||
'mautic.lead.'.$lead->getId().'.timeline.filters',
|
||||
[
|
||||
'search' => '',
|
||||
'includeEvents' => [],
|
||||
'excludeEvents' => [],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if (null == $orderBy) {
|
||||
if (!$session->has('mautic.lead.'.$lead->getId().'.timeline.orderby')) {
|
||||
$session->set('mautic.lead.'.$lead->getId().'.timeline.orderby', 'timestamp');
|
||||
$session->set('mautic.lead.'.$lead->getId().'.timeline.orderbydir', 'DESC');
|
||||
}
|
||||
|
||||
$orderBy = [
|
||||
$session->get('mautic.lead.'.$lead->getId().'.timeline.orderby'),
|
||||
$session->get('mautic.lead.'.$lead->getId().'.timeline.orderbydir'),
|
||||
];
|
||||
}
|
||||
/** @var LeadModel $model */
|
||||
$model = $this->getModel('lead');
|
||||
|
||||
return $model->getEngagements($lead, $filters, $orderBy, $page, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array with engagements and points of a contact.
|
||||
*/
|
||||
protected function getStatsCount(Lead $lead, ?\DateTime $fromDate = null, ?\DateTime $toDate = null): array
|
||||
{
|
||||
if (null == $fromDate) {
|
||||
$fromDate = new \DateTime('first day of this month 00:00:00');
|
||||
$fromDate->modify('-6 months');
|
||||
}
|
||||
if (null == $toDate) {
|
||||
$toDate = new \DateTime();
|
||||
}
|
||||
|
||||
/** @var LeadModel $model */
|
||||
$model = $this->getModel('lead');
|
||||
$chartQuery = new ChartQuery($this->doctrine->getConnection(), $fromDate, $toDate);
|
||||
|
||||
$engagements = $model->getEngagementCount($lead, $fromDate, $toDate, 'm', $chartQuery);
|
||||
$pointStats = $chartQuery->fetchSumTimeData('lead_points_change_log', 'date_added', ['lead_id' => $lead->getId()], 'delta');
|
||||
|
||||
return [
|
||||
'engagements' => $engagements,
|
||||
'points' => $pointStats,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array to create company's engagements graph.
|
||||
*
|
||||
* @param array $contacts
|
||||
*/
|
||||
protected function getCompanyEngagementData($contacts): array
|
||||
{
|
||||
$engagements = [0, 0, 0, 0, 0, 0];
|
||||
$points = [0, 0, 0, 0, 0, 0];
|
||||
foreach ($contacts as $contact) {
|
||||
/** @var LeadModel $model */
|
||||
$model = $this->getModel('lead.lead');
|
||||
|
||||
if (!isset($contact['lead_id'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$lead = $model->getEntity($contact['lead_id']);
|
||||
|
||||
if (!$lead instanceof Lead) {
|
||||
continue;
|
||||
}
|
||||
$model->getRepository()->refetchEntity($lead);
|
||||
$engagementsData = $this->getStatsCount($lead);
|
||||
|
||||
$engagements = array_map(fn ($a, $b) => $a + $b, $engagementsData['engagements']['byUnit'], $engagements);
|
||||
$points = array_map(fn ($points_first_user, $points_second_user) => $points_first_user + $points_second_user, $engagementsData['points'], $points);
|
||||
}
|
||||
|
||||
return [
|
||||
'engagements' => $engagements,
|
||||
'points' => $points,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get company graph for points and engagements.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function getCompanyEngagementsForGraph($contacts): array
|
||||
{
|
||||
$graphData = $this->getCompanyEngagementData($contacts);
|
||||
$translator = $this->translator;
|
||||
|
||||
$fromDate = new \DateTime('first day of this month 00:00:00');
|
||||
$fromDate->modify('-6 months');
|
||||
|
||||
$toDate = new \DateTime();
|
||||
|
||||
$lineChart = new LineChart(null, $fromDate, $toDate);
|
||||
|
||||
$lineChart->setDataset($translator->trans('mautic.lead.graph.line.all_engagements'), $graphData['engagements']);
|
||||
|
||||
$lineChart->setDataset($translator->trans('mautic.lead.graph.line.points'), $graphData['points']);
|
||||
|
||||
return $lineChart->render();
|
||||
}
|
||||
|
||||
protected function getScheduledCampaignEvents(Lead $lead): array
|
||||
{
|
||||
// Upcoming events from Campaign Bundle
|
||||
/** @var \Mautic\CampaignBundle\Entity\LeadEventLogRepository $leadEventLogRepository */
|
||||
$leadEventLogRepository = $this->doctrine->getManager()->getRepository(\Mautic\CampaignBundle\Entity\LeadEventLog::class);
|
||||
|
||||
return $leadEventLogRepository->getUpcomingEvents(
|
||||
[
|
||||
'lead' => $lead,
|
||||
'eventType' => ['action', 'condition'],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[\Symfony\Contracts\Service\Attribute\Required]
|
||||
public function setRequestStackLeadDetailsTrait(?RequestStack $requestStack): void
|
||||
{
|
||||
$this->requestStack = $requestStack;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,352 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Controller\FormController;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
use Mautic\LeadBundle\Entity\LeadNote;
|
||||
use Mautic\LeadBundle\Model\NoteModel;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class NoteController extends FormController
|
||||
{
|
||||
use LeadAccessTrait;
|
||||
|
||||
/**
|
||||
* Generate's default list view.
|
||||
*
|
||||
* @return JsonResponse|Response
|
||||
*/
|
||||
public function indexAction(Request $request, $leadId = 0, $page = 1)
|
||||
{
|
||||
if (empty($leadId)) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$lead = $this->checkLeadAccess($leadId, 'view');
|
||||
if ($lead instanceof Response) {
|
||||
return $lead;
|
||||
}
|
||||
|
||||
$this->setListFilters();
|
||||
|
||||
$session = $request->getSession();
|
||||
|
||||
// set limits
|
||||
$limit = $session->get(
|
||||
'mautic.lead.'.$lead->getId().'.note.limit',
|
||||
$this->coreParametersHelper->get('default_pagelimit')
|
||||
);
|
||||
$start = (1 === $page) ? 0 : (($page - 1) * $limit);
|
||||
if ($start < 0) {
|
||||
$start = 0;
|
||||
}
|
||||
|
||||
$search = $request->get('search', $session->get('mautic.lead.'.$lead->getId().'.note.filter', ''));
|
||||
$session->set('mautic.lead.'.$lead->getId().'.note.filter', $search);
|
||||
|
||||
// do some default filtering
|
||||
$orderBy = $session->get('mautic.lead.'.$lead->getId().'.note.orderby', 'n.dateTime');
|
||||
$orderByDir = $session->get('mautic.lead.'.$lead->getId().'.note.orderbydir', 'DESC');
|
||||
|
||||
$model = $this->getModel('lead.note');
|
||||
$force = [
|
||||
[
|
||||
'column' => 'n.lead',
|
||||
'expr' => 'eq',
|
||||
'value' => $lead,
|
||||
],
|
||||
];
|
||||
|
||||
$tmpl = $request->isXmlHttpRequest() ? $request->get('tmpl', 'index') : 'index';
|
||||
$noteType = InputHelper::clean($request->request->all()['noteTypes'] ?? []);
|
||||
if (empty($noteType) && 'index' === $tmpl) {
|
||||
$noteType = $session->get('mautic.lead.'.$lead->getId().'.notetype.filter', []);
|
||||
}
|
||||
$session->set('mautic.lead.'.$lead->getId().'.notetype.filter', $noteType);
|
||||
|
||||
$noteTypes = [
|
||||
'general' => 'mautic.lead.note.type.general',
|
||||
'email' => 'mautic.lead.note.type.email',
|
||||
'call' => 'mautic.lead.note.type.call',
|
||||
'meeting' => 'mautic.lead.note.type.meeting',
|
||||
];
|
||||
|
||||
if (!empty($noteType)) {
|
||||
$force[] = [
|
||||
'column' => 'n.type',
|
||||
'expr' => 'in',
|
||||
'value' => $noteType,
|
||||
];
|
||||
}
|
||||
|
||||
$items = $model->getEntities(
|
||||
[
|
||||
'filter' => [
|
||||
'force' => $force,
|
||||
'string' => $search,
|
||||
],
|
||||
'start' => $start,
|
||||
'limit' => $limit,
|
||||
'orderBy' => $orderBy,
|
||||
'orderByDir' => $orderByDir,
|
||||
'hydration_mode' => 'HYDRATE_ARRAY',
|
||||
]
|
||||
);
|
||||
|
||||
$security = $this->security;
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'notes' => $items,
|
||||
'lead' => $lead,
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
'search' => $search,
|
||||
'noteType' => $noteType,
|
||||
'noteTypes' => $noteTypes,
|
||||
'tmpl' => $tmpl,
|
||||
'permissions' => [
|
||||
'edit' => $security->hasEntityAccess('lead:leads:editown', 'lead:leads:editother', $lead->getPermissionUser()),
|
||||
'delete' => $security->hasEntityAccess('lead:leads:deleteown', 'lead:leads:deleteown', $lead->getPermissionUser()),
|
||||
],
|
||||
],
|
||||
'passthroughVars' => [
|
||||
'route' => false,
|
||||
'mauticContent' => 'leadNote',
|
||||
'noteCount' => count($items),
|
||||
],
|
||||
'contentTemplate' => '@MauticLead/Note/list.html.twig',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate's new note and processes post data.
|
||||
*
|
||||
* @return JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response
|
||||
*/
|
||||
public function newAction(Request $request, $leadId)
|
||||
{
|
||||
$lead = $this->checkLeadAccess($leadId, 'view');
|
||||
if ($lead instanceof Response) {
|
||||
return $lead;
|
||||
}
|
||||
|
||||
// retrieve the entity
|
||||
$note = new LeadNote();
|
||||
$note->setLead($lead);
|
||||
|
||||
$model = $this->getModel('lead.note');
|
||||
\assert($model instanceof NoteModel);
|
||||
$action = $this->generateUrl(
|
||||
'mautic_contactnote_action',
|
||||
[
|
||||
'objectAction' => 'new',
|
||||
'leadId' => $leadId,
|
||||
]
|
||||
);
|
||||
// get the user form factory
|
||||
$form = $model->createForm($note, $this->formFactory, $action);
|
||||
$closeModal = false;
|
||||
$valid = false;
|
||||
// /Check for a submitted form and process it
|
||||
if (Request::METHOD_POST === $request->getMethod()) {
|
||||
if (!$cancelled = $this->isFormCancelled($form)) {
|
||||
if ($valid = $this->isFormValid($form)) {
|
||||
$closeModal = true;
|
||||
|
||||
// form is valid so process the data
|
||||
$model->saveEntity($note);
|
||||
}
|
||||
} else {
|
||||
$closeModal = true;
|
||||
}
|
||||
}
|
||||
|
||||
$security = $this->security;
|
||||
$permissions = [
|
||||
'edit' => $security->hasEntityAccess('lead:leads:editown', 'lead:leads:editother', $lead->getPermissionUser()),
|
||||
'delete' => $security->hasEntityAccess('lead:leads:deleteown', 'lead:leads:deleteown', $lead->getPermissionUser()),
|
||||
];
|
||||
|
||||
if ($closeModal) {
|
||||
// just close the modal
|
||||
$passthroughVars = [
|
||||
'closeModal' => 1,
|
||||
'mauticContent' => 'leadNote',
|
||||
];
|
||||
|
||||
if ($valid && !$cancelled) {
|
||||
$passthroughVars['upNoteCount'] = 1;
|
||||
$passthroughVars['noteHtml'] = $this->renderView(
|
||||
'@MauticLead/Note/note.html.twig',
|
||||
[
|
||||
'note' => $note,
|
||||
'lead' => $lead,
|
||||
'permissions' => $permissions,
|
||||
]
|
||||
);
|
||||
$passthroughVars['noteId'] = $note->getId();
|
||||
|
||||
$this->addFlashMessage('mautic.lead.note.created');
|
||||
}
|
||||
|
||||
$passthroughVars['flashes'] = $this->getFlashContent();
|
||||
|
||||
return new JsonResponse($passthroughVars);
|
||||
} else {
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'form' => $form->createView(),
|
||||
'lead' => $lead,
|
||||
'permissions' => $permissions,
|
||||
],
|
||||
'contentTemplate' => '@MauticLead/Note/form.html.twig',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate's edit form and processes post data.
|
||||
*
|
||||
* @return JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response
|
||||
*/
|
||||
public function editAction(Request $request, $leadId, $objectId)
|
||||
{
|
||||
$lead = $this->checkLeadAccess($leadId, 'view');
|
||||
if ($lead instanceof Response) {
|
||||
return $lead;
|
||||
}
|
||||
|
||||
$model = $this->getModel('lead.note');
|
||||
\assert($model instanceof NoteModel);
|
||||
$note = $model->getEntity($objectId);
|
||||
$closeModal = false;
|
||||
$valid = false;
|
||||
|
||||
if (null === $note || !$this->security->hasEntityAccess('lead:leads:editown', 'lead:leads:editother', $lead->getPermissionUser())) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$action = $this->generateUrl(
|
||||
'mautic_contactnote_action',
|
||||
[
|
||||
'objectAction' => 'edit',
|
||||
'objectId' => $objectId,
|
||||
'leadId' => $leadId,
|
||||
]
|
||||
);
|
||||
$form = $model->createForm($note, $this->formFactory, $action);
|
||||
|
||||
// /Check for a submitted form and process it
|
||||
if (Request::METHOD_POST === $request->getMethod()) {
|
||||
if (!$cancelled = $this->isFormCancelled($form)) {
|
||||
if ($valid = $this->isFormValid($form)) {
|
||||
// form is valid so process the data
|
||||
$model->saveEntity($note);
|
||||
$closeModal = true;
|
||||
}
|
||||
} else {
|
||||
$closeModal = true;
|
||||
}
|
||||
}
|
||||
|
||||
$security = $this->security;
|
||||
$permissions = [
|
||||
'edit' => $security->hasEntityAccess('lead:leads:editown', 'lead:leads:editother', $lead->getPermissionUser()),
|
||||
'delete' => $security->hasEntityAccess('lead:leads:deleteown', 'lead:leads:deleteown', $lead->getPermissionUser()),
|
||||
];
|
||||
|
||||
if ($closeModal) {
|
||||
// just close the modal
|
||||
$passthroughVars['closeModal'] = 1;
|
||||
|
||||
if ($valid && !$cancelled) {
|
||||
$passthroughVars['noteHtml'] = $this->renderView(
|
||||
'@MauticLead/Note/note.html.twig',
|
||||
[
|
||||
'note' => $note,
|
||||
'lead' => $lead,
|
||||
'permissions' => $permissions,
|
||||
]
|
||||
);
|
||||
$passthroughVars['noteId'] = $note->getId();
|
||||
}
|
||||
|
||||
$passthroughVars['mauticContent'] = 'leadNote';
|
||||
|
||||
return new JsonResponse($passthroughVars);
|
||||
} else {
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'form' => $form->createView(),
|
||||
'lead' => $lead,
|
||||
'permissions' => $permissions,
|
||||
],
|
||||
'contentTemplate' => '@MauticLead/Note/form.html.twig',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the entity.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function deleteAction(Request $request, $leadId, $objectId)
|
||||
{
|
||||
$lead = $this->checkLeadAccess($leadId, 'view');
|
||||
if ($lead instanceof Response) {
|
||||
return $lead;
|
||||
}
|
||||
$model = $this->getModel('lead.note');
|
||||
\assert($model instanceof NoteModel);
|
||||
$note = $model->getEntity($objectId);
|
||||
|
||||
if (null === $note) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
if (
|
||||
!$this->security->hasEntityAccess('lead:leads:editown', 'lead:leads:editother', $lead->getPermissionUser())
|
||||
|| $model->isLocked($note)
|
||||
) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$model->deleteEntity($note);
|
||||
|
||||
return new JsonResponse(
|
||||
[
|
||||
'deleteId' => $objectId,
|
||||
'mauticContent' => 'leadNote',
|
||||
'downNoteCount' => 1,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an action defined in route.
|
||||
*
|
||||
* @param int $objectId
|
||||
* @param int $leadId
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function executeNoteAction(Request $request, $objectAction, $objectId = 0, $leadId = 0)
|
||||
{
|
||||
if (method_exists($this, "{$objectAction}Action")) {
|
||||
return $this->{"{$objectAction}Action"}($request, $leadId, $objectId);
|
||||
} else {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Controller\CommonController;
|
||||
use Mautic\CoreBundle\Helper\ExportHelper;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
use Mautic\CoreBundle\Twig\Helper\DateHelper;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class TimelineController extends CommonController
|
||||
{
|
||||
use LeadAccessTrait;
|
||||
use LeadDetailsTrait;
|
||||
|
||||
public function indexAction(Request $request, $leadId, $page = 1)
|
||||
{
|
||||
if (empty($leadId)) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$lead = $this->checkLeadAccess($leadId, 'view');
|
||||
if ($lead instanceof Response) {
|
||||
return $lead;
|
||||
}
|
||||
|
||||
$this->setListFilters();
|
||||
|
||||
$session = $request->getSession();
|
||||
if ('POST' == $request->getMethod() && $request->request->has('search')) {
|
||||
$filters = [
|
||||
'search' => InputHelper::clean($request->request->get('search')),
|
||||
'includeEvents' => InputHelper::clean($request->request->all()['includeEvents'] ?? []),
|
||||
'excludeEvents' => InputHelper::clean($request->request->all()['excludeEvents'] ?? []),
|
||||
];
|
||||
$session->set('mautic.lead.'.$leadId.'.timeline.filters', $filters);
|
||||
} else {
|
||||
$filters = null;
|
||||
}
|
||||
|
||||
$order = [
|
||||
$session->get('mautic.lead.'.$leadId.'.timeline.orderby'),
|
||||
$session->get('mautic.lead.'.$leadId.'.timeline.orderbydir'),
|
||||
];
|
||||
|
||||
$events = $this->getEngagements($lead, $filters, $order, $page);
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'lead' => $lead,
|
||||
'page' => $page,
|
||||
'events' => $events,
|
||||
],
|
||||
'passthroughVars' => [
|
||||
'route' => false,
|
||||
'mauticContent' => 'leadTimeline',
|
||||
'timelineCount' => $events['total'],
|
||||
],
|
||||
'contentTemplate' => '@MauticLead/Timeline/_list.html.twig',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function pluginIndexAction(Request $request, $integration, $page = 1)
|
||||
{
|
||||
$limit = 25;
|
||||
$leads = $this->checkAllAccess('view', $limit);
|
||||
|
||||
if ($leads instanceof Response) {
|
||||
return $leads;
|
||||
}
|
||||
|
||||
$this->setListFilters();
|
||||
|
||||
$session = $request->getSession();
|
||||
if ('POST' === $request->getMethod() && $request->request->has('search')) {
|
||||
$filters = [
|
||||
'search' => InputHelper::clean($request->request->get('search')),
|
||||
'includeEvents' => InputHelper::clean($request->request->all()['includeEvents'] ?? []),
|
||||
'excludeEvents' => InputHelper::clean($request->request->all()['excludeEvents'] ?? []),
|
||||
];
|
||||
$session->set('mautic.plugin.timeline.filters', $filters);
|
||||
} else {
|
||||
$filters = null;
|
||||
}
|
||||
|
||||
$order = [
|
||||
$session->get('mautic.plugin.timeline.orderby'),
|
||||
$session->get('mautic.plugin.timeline.orderbydir'),
|
||||
];
|
||||
|
||||
// get all events grouped by lead
|
||||
$events = $this->getAllEngagements($leads, $filters, $order, $page, $limit);
|
||||
|
||||
$str = $request->server->get('QUERY_STRING');
|
||||
parse_str($str, $query);
|
||||
|
||||
$tmpl = 'table';
|
||||
if (array_key_exists('from', $query) && 'iframe' === $query['from']) {
|
||||
$tmpl = 'list';
|
||||
}
|
||||
if (array_key_exists('tmpl', $query)) {
|
||||
$tmpl = $query['tmpl'];
|
||||
}
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'leads' => $leads,
|
||||
'page' => $page,
|
||||
'events' => $events,
|
||||
'integration' => $integration,
|
||||
'tmpl' => (!$request->isXmlHttpRequest()) ? 'index' : '',
|
||||
'newCount' => (array_key_exists('count', $query) && $query['count']) ? $query['count'] : 0,
|
||||
],
|
||||
'passthroughVars' => [
|
||||
'route' => false,
|
||||
'mauticContent' => 'pluginTimeline',
|
||||
'timelineCount' => $events['total'],
|
||||
],
|
||||
'contentTemplate' => sprintf('@MauticLead/Timeline/plugin_%s.html.twig', $tmpl),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function pluginViewAction(Request $request, $integration, $leadId, $page = 1)
|
||||
{
|
||||
if (empty($leadId)) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
$lead = $this->checkLeadAccess($leadId, 'view', true, $integration);
|
||||
if ($lead instanceof Response) {
|
||||
return $lead;
|
||||
}
|
||||
|
||||
$this->setListFilters();
|
||||
|
||||
$session = $request->getSession();
|
||||
if ('POST' === $request->getMethod() && $request->request->has('search')) {
|
||||
$filters = [
|
||||
'search' => InputHelper::clean($request->request->get('search')),
|
||||
'includeEvents' => InputHelper::clean($request->request->all()['includeEvents'] ?? []),
|
||||
'excludeEvents' => InputHelper::clean($request->request->all()['excludeEvents'] ?? []),
|
||||
];
|
||||
$session->set('mautic.plugin.timeline.'.$leadId.'.filters', $filters);
|
||||
} else {
|
||||
$filters = null;
|
||||
}
|
||||
|
||||
$order = [
|
||||
$session->get('mautic.plugin.timeline.'.$leadId.'.orderby'),
|
||||
$session->get('mautic.plugin.timeline.'.$leadId.'.orderbydir'),
|
||||
];
|
||||
|
||||
$events = $this->getEngagements($lead, $filters, $order, $page);
|
||||
|
||||
$str = $request->server->get('QUERY_STRING');
|
||||
parse_str($str, $query);
|
||||
|
||||
$tmpl = 'table';
|
||||
if (array_key_exists('from', $query) && 'iframe' === $query['from']) {
|
||||
$tmpl = 'list';
|
||||
}
|
||||
if (array_key_exists('tmpl', $query)) {
|
||||
$tmpl = $query['tmpl'];
|
||||
}
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'lead' => $lead,
|
||||
'page' => $page,
|
||||
'integration' => $integration,
|
||||
'events' => $events,
|
||||
'newCount' => (array_key_exists('count', $query) && $query['count']) ? $query['count'] : 0,
|
||||
],
|
||||
'passthroughVars' => [
|
||||
'route' => false,
|
||||
'mauticContent' => 'pluginTimeline',
|
||||
'timelineCount' => $events['total'],
|
||||
],
|
||||
'contentTemplate' => sprintf('@MauticLead/Timeline/plugin_%s.html.twig', $tmpl),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function batchExportAction(Request $request, DateHelper $dateHelper, ExportHelper $exportHelper, $leadId): array|Response
|
||||
{
|
||||
if (empty($leadId)) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$lead = $this->checkLeadAccess($leadId, 'view');
|
||||
if ($lead instanceof Response) {
|
||||
return $lead;
|
||||
}
|
||||
|
||||
if (!$this->security->isGranted('report:export:enable', 'MATCH_ONE')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$this->setListFilters();
|
||||
|
||||
$session = $request->getSession();
|
||||
if ('POST' == $request->getMethod() && $request->request->has('search')) {
|
||||
$filters = [
|
||||
'search' => InputHelper::clean($request->request->get('search')),
|
||||
'includeEvents' => InputHelper::clean($request->request->all()['includeEvents'] ?? []),
|
||||
'excludeEvents' => InputHelper::clean($request->request->all()['excludeEvents'] ?? []),
|
||||
];
|
||||
$session->set('mautic.lead.'.$leadId.'.timeline.filters', $filters);
|
||||
} else {
|
||||
$filters = null;
|
||||
}
|
||||
|
||||
$order = [
|
||||
$session->get('mautic.lead.'.$leadId.'.timeline.orderby'),
|
||||
$session->get('mautic.lead.'.$leadId.'.timeline.orderbydir'),
|
||||
];
|
||||
|
||||
$dataType = $request->get('filetype', 'csv');
|
||||
|
||||
$resultsCallback = function ($event) use ($dateHelper): array {
|
||||
$eventLabel = $event['eventLabel'] ?? $event['eventType'];
|
||||
if (is_array($eventLabel)) {
|
||||
$eventLabel = $eventLabel['label'];
|
||||
}
|
||||
|
||||
return [
|
||||
'eventName' => $eventLabel,
|
||||
'eventType' => $event['eventType'] ?? '',
|
||||
'eventTimestamp' => $dateHelper->toText($event['timestamp'], 'local', 'Y-m-d H:i:s', true),
|
||||
];
|
||||
};
|
||||
|
||||
$results = $this->getEngagements($lead, $filters, $order, 1, 200);
|
||||
$count = $results['total'];
|
||||
$items = $results['events'];
|
||||
$iterations = ceil($count / 200);
|
||||
$loop = 1;
|
||||
|
||||
// Max of 50 iterations for 10K result export
|
||||
if ($iterations > 50) {
|
||||
$iterations = 50;
|
||||
}
|
||||
|
||||
$toExport = [];
|
||||
|
||||
while ($loop <= $iterations) {
|
||||
if (is_callable($resultsCallback)) {
|
||||
foreach ($items as $item) {
|
||||
$toExport[] = $resultsCallback($item);
|
||||
}
|
||||
} else {
|
||||
foreach ($items as $item) {
|
||||
$toExport[] = (array) $item;
|
||||
}
|
||||
}
|
||||
|
||||
$items = $this->getEngagements($lead, $filters, $order, $loop + 1, 200);
|
||||
|
||||
$this->doctrine->getManager()->clear();
|
||||
|
||||
++$loop;
|
||||
}
|
||||
|
||||
return $this->exportResultsAs($toExport, $dataType, 'contact_timeline', $exportHelper);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user