Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Controller;
|
||||
|
||||
use Mautic\AssetBundle\Model\AssetModel;
|
||||
use Mautic\CacheBundle\Cache\CacheProvider;
|
||||
use Mautic\CoreBundle\Controller\AjaxController as CommonAjaxController;
|
||||
use Mautic\CoreBundle\Controller\AjaxLookupControllerTrait;
|
||||
use Mautic\CoreBundle\Controller\VariantAjaxControllerTrait;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\EmailBundle\Helper\PlainTextHelper;
|
||||
use Mautic\EmailBundle\Mailer\Message\MauticMessage;
|
||||
use Mautic\EmailBundle\Model\EmailModel;
|
||||
use Mautic\EmailBundle\MonitoredEmail\Mailbox;
|
||||
use Mautic\EmailBundle\Stats\EmailDependencies;
|
||||
use Mautic\PageBundle\Form\Type\AbTestPropertiesType;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Mailer\Transport\TransportInterface;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Twig\Environment;
|
||||
|
||||
class AjaxController extends CommonAjaxController
|
||||
{
|
||||
use VariantAjaxControllerTrait;
|
||||
use AjaxLookupControllerTrait;
|
||||
|
||||
public function getAbTestFormAction(Request $request, FormFactoryInterface $formFactory, EmailModel $emailModel, Environment $twig): JsonResponse
|
||||
{
|
||||
return $this->sendJsonResponse($this->getAbTestForm(
|
||||
$request,
|
||||
$emailModel,
|
||||
fn ($formType, $formOptions) => $formFactory->create(AbTestPropertiesType::class, [], ['formType' => $formType, 'formTypeOptions' => $formOptions]),
|
||||
fn ($form) => $this->renderView('@MauticEmail/AbTest/form.html.twig', ['form' => $this->setFormTheme($form, $twig, ['@MauticEmail/AbTest/form.html.twig', '@MauticEmail/FormTheme/Email/layout.html.twig'])]),
|
||||
'email_abtest_settings',
|
||||
'emailform'
|
||||
));
|
||||
}
|
||||
|
||||
public function sendBatchAction(Request $request): JsonResponse
|
||||
{
|
||||
$dataArray = ['success' => 0];
|
||||
|
||||
/** @var EmailModel $model */
|
||||
$model = $this->getModel('email');
|
||||
$objectId = $request->request->get('id', 0);
|
||||
$pending = $request->request->get('pending', 0);
|
||||
$limit = $request->request->get('batchlimit', 100);
|
||||
|
||||
if ($objectId && $entity = $model->getEntity($objectId)) {
|
||||
$dataArray['success'] = 1;
|
||||
$session = $request->getSession();
|
||||
$progress = $session->get('mautic.email.send.progress', [0, (int) $pending]);
|
||||
$stats = $session->get('mautic.email.send.stats', ['sent' => 0, 'failed' => 0, 'failedRecipients' => []]);
|
||||
$inProgress = $session->get('mautic.email.send.active', false);
|
||||
|
||||
if ($pending && !$inProgress && $entity->isPublished()) {
|
||||
$session->set('mautic.email.send.active', true);
|
||||
[$batchSentCount, $batchFailedCount, $batchFailedRecipients] = $model->sendEmailToLists($entity, null, $limit);
|
||||
|
||||
$progress[0] += ($batchSentCount + $batchFailedCount);
|
||||
$stats['sent'] += $batchSentCount;
|
||||
$stats['failed'] += $batchFailedCount;
|
||||
|
||||
foreach ($batchFailedRecipients as $emails) {
|
||||
$stats['failedRecipients'] = $stats['failedRecipients'] + $emails;
|
||||
}
|
||||
|
||||
$session->set('mautic.email.send.progress', $progress);
|
||||
$session->set('mautic.email.send.stats', $stats);
|
||||
$session->set('mautic.email.send.active', false);
|
||||
}
|
||||
|
||||
$dataArray['percent'] = ($progress[1]) ? ceil(($progress[0] / $progress[1]) * 100) : 100;
|
||||
$dataArray['progress'] = $progress;
|
||||
$dataArray['stats'] = $stats;
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by parent::getBuilderTokensAction().
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getBuilderTokens($query)
|
||||
{
|
||||
/** @var EmailModel $model */
|
||||
$model = $this->getModel('email');
|
||||
|
||||
return $model->getBuilderComponents(null, ['tokens'], (string) $query);
|
||||
}
|
||||
|
||||
public function generatePlaintTextAction(Request $request): JsonResponse
|
||||
{
|
||||
$custom = $request->request->get('custom');
|
||||
|
||||
$parser = new PlainTextHelper(
|
||||
[
|
||||
'base_url' => $request->getSchemeAndHttpHost().$request->getBasePath(),
|
||||
]
|
||||
);
|
||||
|
||||
$dataArray = [
|
||||
'text' => $parser->setHtml($custom)->getText(),
|
||||
];
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
public function getAttachmentsSizeAction(Request $request, AssetModel $assetModel): JsonResponse
|
||||
{
|
||||
$assets = $request->query->all()['assets'] ?? [];
|
||||
$size = 0;
|
||||
if ($assets) {
|
||||
$size = $assetModel->getTotalFilesize($assets);
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse(['size' => $size]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests monitored email connection settings.
|
||||
*/
|
||||
public function testMonitoredEmailServerConnectionAction(Request $request, Mailbox $mailbox): JsonResponse
|
||||
{
|
||||
$dataArray = ['success' => 0, 'message' => ''];
|
||||
|
||||
if ($this->user->isAdmin()) {
|
||||
$settings = $request->request->all();
|
||||
|
||||
if (empty($settings['password'])) {
|
||||
$existingMonitoredSettings = $this->coreParametersHelper->get('monitored_email');
|
||||
if (is_array($existingMonitoredSettings) && (!empty($existingMonitoredSettings[$settings['mailbox']]['password']))) {
|
||||
$settings['password'] = $existingMonitoredSettings[$settings['mailbox']]['password'];
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$mailbox->setMailboxSettings($settings);
|
||||
$folders = $mailbox->getListingFolders();
|
||||
if (!empty($folders)) {
|
||||
$dataArray['folders'] = '';
|
||||
foreach ($folders as $folder) {
|
||||
$dataArray['folders'] .= "<option value=\"$folder\">$folder</option>\n";
|
||||
}
|
||||
}
|
||||
$dataArray['success'] = 1;
|
||||
$dataArray['message'] = $this->translator->trans('mautic.core.success');
|
||||
} catch (\Exception $e) {
|
||||
$dataArray['message'] = $this->translator->trans($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
public function sendTestEmailAction(TransportInterface $transport, UserHelper $userHelper, CoreParametersHelper $parametersHelper): Response
|
||||
{
|
||||
$user = $userHelper->getUser();
|
||||
$email = (new MauticMessage())
|
||||
->subject($this->translator->trans('mautic.email.config.mailer.transport.test_send.subject'))
|
||||
->text($this->translator->trans('mautic.email.config.mailer.transport.test_send.body'))
|
||||
->from(new Address($parametersHelper->get('mailer_from_email'), $parametersHelper->get('mailer_from_name') ?: ''))
|
||||
->to(new Address($user->getEmail(), trim($user->getFirstName().' '.$user->getLastName()) ?: ''));
|
||||
|
||||
$success = 1;
|
||||
$message = $this->translator->trans('mautic.core.success');
|
||||
|
||||
try {
|
||||
$transport->send($email);
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
$success = 0;
|
||||
$message = $e->getMessage();
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse(['success' => $success, 'message' => $message]);
|
||||
}
|
||||
|
||||
public function getEmailCountStatsAction(Request $request): JsonResponse
|
||||
{
|
||||
/** @var EmailModel $model */
|
||||
$model = $this->getModel('email');
|
||||
|
||||
$id = $request->query->get('id');
|
||||
$ids = $request->query->all()['ids'] ?? [];
|
||||
|
||||
// Support for legacy calls
|
||||
if (!$ids && $id) {
|
||||
$ids = [$id];
|
||||
}
|
||||
|
||||
$data = [];
|
||||
foreach ($ids as $id) {
|
||||
if ($email = $model->getEntity($id)) {
|
||||
$pending = $model->getPendingLeads($email, null, true);
|
||||
$queued = $model->getQueuedCounts($email);
|
||||
|
||||
$data[] = [
|
||||
'id' => $email->getId(),
|
||||
'pending' => 'list' === $email->getEmailType() && $pending ? $this->translator->trans(
|
||||
'mautic.email.stat.leadcount',
|
||||
['%count%' => $pending]
|
||||
) : 0,
|
||||
'queued' => ($queued) ? $this->translator->trans('mautic.email.stat.queued', ['%count%' => $queued]) : 0,
|
||||
'sentCount' => $this->translator->trans('mautic.email.stat.sentcount', ['%count%' => $email->getSentCount(true)]),
|
||||
'readCount' => $this->translator->trans('mautic.email.stat.readcount', ['%count%' => $email->getReadCount(true)]),
|
||||
'readPercent' => $this->translator->trans('mautic.email.stat.readpercent', ['%count%' => $email->getReadPercentage(true)]),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Support for legacy calls
|
||||
if ($request->get('id') && !empty($data[0])) {
|
||||
$data = $data[0];
|
||||
} else {
|
||||
$data = [
|
||||
'success' => 1,
|
||||
'stats' => $data,
|
||||
];
|
||||
}
|
||||
|
||||
return new JsonResponse($data);
|
||||
}
|
||||
|
||||
public function getEmailDeliveredCountAction(Request $request, CacheProvider $cacheProvider): JsonResponse
|
||||
{
|
||||
$emailId = (int) InputHelper::clean($request->query->get('id'));
|
||||
|
||||
if (0 === $emailId) {
|
||||
return $this->sendJsonResponse([
|
||||
'success' => 0,
|
||||
'message' => $this->translator->trans('mautic.core.error.badrequest'),
|
||||
], 400);
|
||||
}
|
||||
|
||||
$cacheTimeout = (int) $this->coreParametersHelper->get('cached_data_timeout');
|
||||
$cacheItem = $cacheProvider->getItem('email.stats.delivered.'.$emailId);
|
||||
|
||||
if ($cacheItem->isHit()) {
|
||||
$deliveredCount = $cacheItem->get();
|
||||
} else {
|
||||
/** @var EmailModel $model */
|
||||
$model = $this->getModel('email');
|
||||
|
||||
$email = $model->getEntity($emailId);
|
||||
if (null === $email) {
|
||||
return $this->sendJsonResponse([
|
||||
'success' => 0,
|
||||
'message' => $this->translator->trans('mautic.api.call.notfound'),
|
||||
], 404);
|
||||
}
|
||||
$deliveredCount = $model->getDeliveredCount($email);
|
||||
$cacheItem->set($deliveredCount);
|
||||
$cacheItem->expiresAfter($cacheTimeout * 60);
|
||||
$cacheProvider->save($cacheItem);
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse([
|
||||
'success' => 1,
|
||||
'delivered' => $deliveredCount,
|
||||
]);
|
||||
}
|
||||
|
||||
public function heatmapAction(Request $request, EmailModel $model): JsonResponse
|
||||
{
|
||||
$emailId = (int) $request->query->get('id');
|
||||
$email = $model->getEntity($emailId);
|
||||
|
||||
if (null === $email) {
|
||||
return $this->sendJsonResponse([
|
||||
'message' => $this->translator->trans('mautic.api.call.notfound'),
|
||||
], 404);
|
||||
}
|
||||
|
||||
if (!$this->security->hasEntityAccess(
|
||||
'email:emails:viewown',
|
||||
'email:emails:viewother',
|
||||
$email->getCreatedBy()
|
||||
)
|
||||
) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$content = $email->getCustomHtml();
|
||||
$clickStats = $model->getEmailClickStats($emailId);
|
||||
$totalUniqueClicks = array_sum(array_column($clickStats, 'unique_hits'));
|
||||
$totalClicks = array_sum(array_column($clickStats, 'hits'));
|
||||
foreach ($clickStats as &$stat) {
|
||||
$stat['unique_hits_rate'] = round($totalUniqueClicks > 0 ? ($stat['unique_hits'] / $totalUniqueClicks) : 0, 4);
|
||||
$stat['unique_hits_text'] = $this->translator->trans('mautic.email.heatmap.clicks', ['%count%' => $stat['unique_hits']]);
|
||||
$stat['hits_rate'] = round($totalClicks > 0 ? ($stat['hits'] / $totalClicks) : 0, 4);
|
||||
$stat['hits_text'] = $this->translator->trans('mautic.email.heatmap.clicks', ['%count%' => $stat['hits']]);
|
||||
}
|
||||
$legendTemplate = $this->renderView('@MauticEmail/Heatmap/heatmap_legend.html.twig', [
|
||||
'totalClicks' => $totalClicks,
|
||||
'totalUniqueClicks' => $totalUniqueClicks,
|
||||
]);
|
||||
|
||||
return $this->sendJsonResponse([
|
||||
'content' => $content,
|
||||
'clickStats' => $clickStats,
|
||||
'totalUniqueClicks' => $totalUniqueClicks,
|
||||
'totalClicks' => $totalClicks,
|
||||
'legendTemplate' => $legendTemplate,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getEmailUsagesAction(Request $request, EmailDependencies $emailDependencies): JsonResponse
|
||||
{
|
||||
$emailId = (int) $request->query->get('id');
|
||||
|
||||
if (0 === $emailId) {
|
||||
return $this->sendJsonResponse([
|
||||
'message' => $this->translator->trans('mautic.core.error.badrequest'),
|
||||
], 400);
|
||||
}
|
||||
|
||||
$usagesHtml = $this->renderView('@MauticCore/Helper/usage.html.twig', [
|
||||
'title' => $this->translator->trans('mautic.email.usages'),
|
||||
'stats' => $emailDependencies->getChannelsIds($emailId),
|
||||
'noUsages' => $this->translator->trans('mautic.email.no_usages'),
|
||||
]);
|
||||
|
||||
return $this->sendJsonResponse([
|
||||
'usagesHtml' => $usagesHtml,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Controller\Api;
|
||||
|
||||
use Doctrine\ORM\EntityNotFoundException;
|
||||
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\Helper\InputHelper;
|
||||
use Mautic\CoreBundle\Helper\RandomHelper\RandomHelperInterface;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\EmailBundle\Helper\MailHelper;
|
||||
use Mautic\EmailBundle\Model\EmailModel;
|
||||
use Mautic\EmailBundle\MonitoredEmail\Processor\Reply;
|
||||
use Mautic\LeadBundle\Controller\LeadAccessTrait;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
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<Email>
|
||||
*/
|
||||
class EmailApiController extends CommonApiController
|
||||
{
|
||||
use LeadAccessTrait;
|
||||
|
||||
/**
|
||||
* @var EmailModel|null
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
/**
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected $extraGetEntitiesArguments = ['ignoreListJoin' => true];
|
||||
|
||||
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)
|
||||
{
|
||||
$emailModel = $modelFactory->getModel('email');
|
||||
\assert($emailModel instanceof EmailModel);
|
||||
|
||||
$this->model = $emailModel;
|
||||
$this->entityClass = Email::class;
|
||||
$this->entityNameOne = 'email';
|
||||
$this->entityNameMulti = 'emails';
|
||||
$this->serializerGroups = [
|
||||
'emailDetails',
|
||||
'categoryList',
|
||||
'publishDetails',
|
||||
'assetList',
|
||||
'formList',
|
||||
'leadListList',
|
||||
];
|
||||
$this->dataInputMasks = [
|
||||
'customHtml' => 'html',
|
||||
'dynamicContent' => [
|
||||
'content' => 'html',
|
||||
'filters' => [
|
||||
'content' => 'html',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of emails.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function getEntitiesAction(Request $request, UserHelper $userHelper)
|
||||
{
|
||||
// get parent level only
|
||||
$this->listFilters[] = [
|
||||
'column' => 'e.variantParent',
|
||||
'expr' => 'isNull',
|
||||
];
|
||||
|
||||
return parent::getEntitiesAction($request, $userHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the email to it's assigned lists.
|
||||
*
|
||||
* @param int $id Email ID
|
||||
*
|
||||
* @return Response
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
|
||||
*/
|
||||
public function sendAction(Request $request, $id)
|
||||
{
|
||||
$entity = $this->model->getEntity($id);
|
||||
|
||||
if (null === $entity || !$entity->isPublished()) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
if (!$this->checkEntityAccess($entity)) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$lists = $request->request->all()['lists'] ?? [];
|
||||
$limit = $request->request->get('limit', null);
|
||||
$batch = $request->request->get('batch', null);
|
||||
|
||||
[$count, $failed] = $this->model->sendEmailToLists($entity, $lists, $limit, $batch);
|
||||
|
||||
$view = $this->view(
|
||||
[
|
||||
'success' => 1,
|
||||
'sentCount' => $count,
|
||||
'failedRecipients' => $failed,
|
||||
],
|
||||
Response::HTTP_OK
|
||||
);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the email to a specific lead.
|
||||
*
|
||||
* @param int $id Email ID
|
||||
* @param int $leadId Lead ID
|
||||
*
|
||||
* @return Response
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
|
||||
*/
|
||||
public function sendLeadAction(Request $request, $id, $leadId)
|
||||
{
|
||||
$entity = $this->model->getEntity($id);
|
||||
|
||||
if (!$entity) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
if (!$this->checkEntityAccess($entity)) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
/** @var Lead $lead */
|
||||
$lead = $this->checkLeadAccess($leadId, 'edit');
|
||||
if ($lead instanceof Response) {
|
||||
return $lead;
|
||||
}
|
||||
|
||||
$post = $request->request->all();
|
||||
$tokens = (!empty($post['tokens'])) ? $post['tokens'] : [];
|
||||
$assetsIds = (!empty($post['assetAttachments'])) ? $post['assetAttachments'] : [];
|
||||
$response = ['success' => false];
|
||||
|
||||
$cleanTokens = [];
|
||||
|
||||
foreach ($tokens as $token => $value) {
|
||||
$value = InputHelper::html($value);
|
||||
if (!preg_match('/^{.*?}$/', $token)) {
|
||||
$token = '{'.$token.'}';
|
||||
}
|
||||
|
||||
$cleanTokens[$token] = $value;
|
||||
}
|
||||
|
||||
$leadFields = array_merge(['id' => $leadId], $lead->getProfileFields());
|
||||
// Set owner_id to support the "Owner is mailer" feature
|
||||
if ($lead->getOwner()) {
|
||||
$leadFields['owner_id'] = $lead->getOwner()->getId();
|
||||
}
|
||||
|
||||
$result = $this->model->sendEmail(
|
||||
$entity,
|
||||
$leadFields,
|
||||
[
|
||||
'source' => ['api', 0],
|
||||
'tokens' => $cleanTokens,
|
||||
'assetAttachments' => $assetsIds,
|
||||
'return_errors' => true,
|
||||
'ignoreDNC' => true,
|
||||
'email_type' => MailHelper::EMAIL_TYPE_TRANSACTIONAL,
|
||||
]
|
||||
);
|
||||
|
||||
if (is_bool($result)) {
|
||||
$response['success'] = $result;
|
||||
} else {
|
||||
$response['failed'] = $result;
|
||||
}
|
||||
|
||||
$view = $this->view($response, Response::HTTP_OK);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $trackingHash
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function replyAction(Reply $replyService, RandomHelperInterface $randomHelper, $trackingHash)
|
||||
{
|
||||
try {
|
||||
$replyService->createReplyByHash($trackingHash, "api-{$randomHelper->generate()}");
|
||||
} catch (EntityNotFoundException $e) {
|
||||
return $this->notFound($e->getMessage());
|
||||
}
|
||||
|
||||
return $this->handleView(
|
||||
$this->view(['success' => true], Response::HTTP_CREATED)
|
||||
);
|
||||
}
|
||||
|
||||
protected function prepareParametersFromRequest(FormInterface $form, array &$params, ?object $entity = null, array $masks = [], array $fields = []): void
|
||||
{
|
||||
if (isset($params['publicPreview']) && $entity instanceof Email) {
|
||||
$entity->setPublicPreview(InputHelper::boolean($params['publicPreview']) ?? false);
|
||||
unset($params['publicPreview']);
|
||||
}
|
||||
parent::prepareParametersFromRequest($form, $params, $entity, $masks, $fields);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Controller;
|
||||
|
||||
use Mautic\CategoryBundle\Model\CategoryModel;
|
||||
use Mautic\CoreBundle\Controller\AbstractFormController;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\EmailBundle\Form\Type\BatchCategoryType;
|
||||
use Mautic\EmailBundle\Model\EmailActionModel;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class BatchEmailController extends AbstractFormController
|
||||
{
|
||||
/**
|
||||
* Adds or removes categories to multiple emails defined by email ID.
|
||||
*/
|
||||
public function execAction(Request $request, EmailActionModel $actionModel, CategoryModel $categoryModel): JsonResponse
|
||||
{
|
||||
$params = $request->get('email_batch');
|
||||
$ids = empty($params['ids']) ? [] : json_decode($params['ids']);
|
||||
|
||||
if ($ids && is_array($ids)) {
|
||||
$newCategoryId = $params['newCategory'];
|
||||
|
||||
$newCategory = $categoryModel->getEntity($newCategoryId);
|
||||
$affected = $actionModel->setCategory($ids, $newCategory);
|
||||
|
||||
$this->addFlashMessage('mautic.email.batch_emails_affected', [
|
||||
'%count%' => count($affected),
|
||||
]);
|
||||
} else {
|
||||
$this->addFlashMessage('mautic.core.error.ids.missing');
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'closeModal' => true,
|
||||
'flashes' => $this->getFlashContent(),
|
||||
'affected' => !empty($affected) ? array_map(fn (Email $affected) => $affected->getId(), $affected) : [],
|
||||
'newCategory' => [
|
||||
'name' => !empty($newCategory) ? $newCategory->getTitle() : null,
|
||||
'color' => !empty($newCategory) ? $newCategory->getColor() : null,
|
||||
],
|
||||
'callback' => 'emailBatchSubmitCallback',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* View the modal form for adding contacts into categories in batches.
|
||||
*/
|
||||
public function indexAction(): Response
|
||||
{
|
||||
$route = $this->generateUrl('mautic_email_batch_categories_set');
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'form' => $this->createForm(
|
||||
BatchCategoryType::class,
|
||||
[],
|
||||
[
|
||||
'action' => $route,
|
||||
'attr' => [
|
||||
'data-submit-callback' => 'emailBatchSubmit',
|
||||
],
|
||||
]
|
||||
)->createView(),
|
||||
],
|
||||
'contentTemplate' => '@MauticEmail/Batch/form.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_email_index',
|
||||
'mauticContent' => 'emailBatch',
|
||||
'route' => $route,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Form\Type\DateRangeType;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\EmailBundle\Model\EmailModel;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
|
||||
class EmailGraphStatsController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* Loads a specific form into the detailed panel.
|
||||
*
|
||||
* @param int $objectId
|
||||
* @param bool $isVariant
|
||||
* @param string $dateFrom
|
||||
* @param string $dateTo
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function viewAction(
|
||||
Request $request,
|
||||
EmailModel $model,
|
||||
FormFactoryInterface $formFactory,
|
||||
CorePermissions $security,
|
||||
$objectId,
|
||||
$isVariant,
|
||||
$dateFrom = null,
|
||||
$dateTo = null,
|
||||
): \Symfony\Component\HttpFoundation\Response {
|
||||
/** @var \Mautic\EmailBundle\Entity\Email $email */
|
||||
$email = $model->getEntity($objectId);
|
||||
|
||||
// Init the date range filter form
|
||||
$dateRangeValues = ['date_from' => $dateFrom, 'date_to' => $dateTo];
|
||||
$action = $this->generateUrl('mautic_email_action', ['objectAction' => 'view', 'objectId' => $objectId]);
|
||||
$dateRangeForm = $formFactory->create(DateRangeType::class, $dateRangeValues, ['action' => $action]);
|
||||
|
||||
if (null === $email || !$security->hasEntityAccess(
|
||||
'email:emails:viewown',
|
||||
'email:emails:viewother',
|
||||
$email->getCreatedBy()
|
||||
)) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
||||
// get A/B test information
|
||||
[$parent, $children] = $email->getVariants();
|
||||
|
||||
// get related translations
|
||||
[$translationParent, $translationChildren] = $email->getTranslations();
|
||||
|
||||
// Prepare stats for bargraph
|
||||
if ($chartStatsSource = $request->query->get('stats')) {
|
||||
$includeVariants = ('all' === $chartStatsSource);
|
||||
} else {
|
||||
$includeVariants = (($email->isVariant() && $parent === $email) || ($email->isTranslation() && $translationParent === $email));
|
||||
}
|
||||
|
||||
$dateFromObject = new \DateTime($dateFrom);
|
||||
$dateToObject = new \DateTime($dateTo);
|
||||
|
||||
if ('template' === $email->getEmailType()) {
|
||||
$stats = $model->getEmailGeneralStats(
|
||||
$email,
|
||||
$includeVariants,
|
||||
null,
|
||||
$dateFromObject,
|
||||
$dateToObject
|
||||
);
|
||||
} else {
|
||||
$stats = $model->getEmailListStats(
|
||||
$email,
|
||||
$includeVariants,
|
||||
$dateFromObject,
|
||||
$dateToObject
|
||||
);
|
||||
}
|
||||
|
||||
$statsDevices = $model->getEmailDeviceStats(
|
||||
$email,
|
||||
$includeVariants,
|
||||
$dateFromObject,
|
||||
$dateToObject
|
||||
);
|
||||
|
||||
return $this->render(
|
||||
'@MauticEmail/Email/graph.html.twig',
|
||||
[
|
||||
'email' => $email,
|
||||
'stats' => $stats,
|
||||
'statsDevices' => $statsDevices,
|
||||
'showAllStats' => $includeVariants,
|
||||
'dateRangeForm' => $dateRangeForm->createView(),
|
||||
'isVariant' => $isVariant,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Controller;
|
||||
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Mautic\CoreBundle\Helper\MapHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\EmailBundle\Model\EmailModel;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
|
||||
class EmailMapStatsController extends AbstractController
|
||||
{
|
||||
public const MAP_OPTIONS = [
|
||||
'read_count' => [
|
||||
'label' => 'mautic.email.stat.read',
|
||||
'unit' => 'Read',
|
||||
],
|
||||
'clicked_through_count'=> [
|
||||
'label' => 'mautic.email.clicked',
|
||||
'unit' => 'Click',
|
||||
],
|
||||
];
|
||||
|
||||
public const LEGEND_TEXT = 'Total: %total (%withCountry with country)';
|
||||
|
||||
public function __construct(protected EmailModel $model)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<int, array<string, int|string>>>
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getData(Email $entity, \DateTimeImmutable $dateFromObject, \DateTimeImmutable $dateToObject): array
|
||||
{
|
||||
// get A/B test information
|
||||
$parent = $entity->getVariantParent();
|
||||
|
||||
// get translation parent
|
||||
$translationParent = $entity->getTranslationParent();
|
||||
|
||||
$includeVariants = (($entity->isVariant() && empty($parent)) || ($entity->isTranslation() && empty($translationParent)));
|
||||
|
||||
return $this->model->getCountryStats(
|
||||
$entity,
|
||||
$dateFromObject,
|
||||
$dateToObject,
|
||||
$includeVariants,
|
||||
);
|
||||
}
|
||||
|
||||
public function hasAccess(CorePermissions $security, Email $entity): bool
|
||||
{
|
||||
return $security->hasEntityAccess(
|
||||
'email:emails:viewown',
|
||||
'email:emails:viewother',
|
||||
$entity->getCreatedBy()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<string, string>>
|
||||
*/
|
||||
public function getMapOptions(): array
|
||||
{
|
||||
return self::MAP_OPTIONS;
|
||||
}
|
||||
|
||||
public function getMapOptionsTitle(): string
|
||||
{
|
||||
return 'mautic.email.stats.options.title';
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function viewAction(
|
||||
CorePermissions $security,
|
||||
int $objectId,
|
||||
string $dateFrom = '',
|
||||
string $dateTo = '',
|
||||
): Response {
|
||||
$entity = $this->model->getEntity($objectId);
|
||||
|
||||
if (empty($entity) || !$this->hasAccess($security, $entity)) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
||||
$statsCountries = $this->getData($entity, new \DateTimeImmutable($dateFrom), new \DateTimeImmutable($dateTo));
|
||||
$mapData = MapHelper::buildMapData($statsCountries, $this->getMapOptions(), self::LEGEND_TEXT);
|
||||
|
||||
return $this->render(
|
||||
'@MauticCore/Helper/map.html.twig',
|
||||
[
|
||||
'data' => $mapData[0]['data'],
|
||||
'height' => 315,
|
||||
'optionsEnabled' => true,
|
||||
'optionsTitle' => $this->getMapOptionsTitle(),
|
||||
'options' => $mapData,
|
||||
'legendEnabled' => true,
|
||||
'statUnit' => $mapData[0]['unit'],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,814 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Controller\FormController as CommonFormController;
|
||||
use Mautic\CoreBundle\Helper\ThemeHelper;
|
||||
use Mautic\CoreBundle\Helper\TrackingPixelHelper;
|
||||
use Mautic\CoreBundle\Twig\Helper\AnalyticsHelper;
|
||||
use Mautic\CoreBundle\Twig\Helper\AssetsHelper;
|
||||
use Mautic\EmailBundle\EmailEvents;
|
||||
use Mautic\EmailBundle\Entity\Stat;
|
||||
use Mautic\EmailBundle\Event\EmailSendEvent;
|
||||
use Mautic\EmailBundle\Event\TransportWebhookEvent;
|
||||
use Mautic\EmailBundle\Helper\EmailConfig;
|
||||
use Mautic\EmailBundle\Helper\MailHashHelper;
|
||||
use Mautic\EmailBundle\Helper\MailHelper;
|
||||
use Mautic\EmailBundle\Model\EmailModel;
|
||||
use Mautic\FormBundle\Model\FormModel;
|
||||
use Mautic\LeadBundle\Controller\FrequencyRuleTrait;
|
||||
use Mautic\LeadBundle\Entity\DoNotContact;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Helper\FakeContactHelper;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\LeadBundle\Tracker\ContactTracker;
|
||||
use Mautic\MessengerBundle\Message\EmailHitNotification;
|
||||
use Mautic\PageBundle\Entity\Page;
|
||||
use Mautic\PageBundle\Event\PageDisplayEvent;
|
||||
use Mautic\PageBundle\EventListener\BuilderSubscriber;
|
||||
use Mautic\PageBundle\Model\PageModel;
|
||||
use Mautic\PageBundle\PageEvents;
|
||||
use Mautic\PluginBundle\Helper\IntegrationHelper;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Contracts\Translation\LocaleAwareInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class PublicController extends CommonFormController
|
||||
{
|
||||
use FrequencyRuleTrait;
|
||||
|
||||
/**
|
||||
* @return Response
|
||||
*/
|
||||
public function indexAction(Request $request, AnalyticsHelper $analyticsHelper, $idHash)
|
||||
{
|
||||
/** @var EmailModel $model */
|
||||
$model = $this->getModel('email');
|
||||
$stat = $model->getEmailStatus($idHash);
|
||||
|
||||
if (!empty($stat)) {
|
||||
if ($this->security->isAnonymous()) {
|
||||
$model->hitEmail($stat, $request, true);
|
||||
}
|
||||
|
||||
$tokens = $stat->getTokens();
|
||||
if (is_array($tokens)) {
|
||||
// Override tracking_pixel so as to not cause a double hit
|
||||
$tokens['{tracking_pixel}'] = MailHelper::getBlankPixel();
|
||||
}
|
||||
|
||||
if ($copy = $stat->getStoredCopy()) {
|
||||
$subject = $copy->getSubject();
|
||||
$content = $copy->getBody();
|
||||
|
||||
// Replace tokens
|
||||
if (is_array($tokens)) {
|
||||
$content = str_ireplace(array_keys($tokens), $tokens, $content);
|
||||
$subject = str_ireplace(array_keys($tokens), $tokens, $subject);
|
||||
}
|
||||
} else {
|
||||
$subject = '';
|
||||
$content = '';
|
||||
}
|
||||
|
||||
$content = $analyticsHelper->addCode($content);
|
||||
|
||||
// Add subject as title
|
||||
if (!empty($subject)) {
|
||||
if (str_contains($content, '<title></title>')) {
|
||||
$content = str_replace('<title></title>', "<title>$subject</title>", $content);
|
||||
} elseif (!str_contains($content, '<title>')) {
|
||||
$content = str_replace('<head>', "<head>\n<title>$subject</title>", $content);
|
||||
}
|
||||
}
|
||||
|
||||
return new Response($content);
|
||||
}
|
||||
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
public function trackingImageAction(
|
||||
Request $request,
|
||||
MessageBusInterface $messageBus,
|
||||
LoggerInterface $logger,
|
||||
string $idHash,
|
||||
): Response {
|
||||
try {
|
||||
$messageBus->dispatch(new EmailHitNotification($idHash, $request));
|
||||
} catch (\Exception $exception) {
|
||||
$logger->error($exception->getMessage(), ['idHash' => $idHash]);
|
||||
$emailModel = $this->getModel('email');
|
||||
assert($emailModel instanceof EmailModel);
|
||||
|
||||
$emailModel->hitEmail($idHash, $request);
|
||||
}
|
||||
|
||||
return TrackingPixelHelper::getResponse($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Response
|
||||
*
|
||||
* @throws \Exception
|
||||
* @throws \Mautic\CoreBundle\Exception\FileNotFoundException
|
||||
*/
|
||||
public function unsubscribeAction(Request $request, ContactTracker $contactTracker, EmailModel $model, LeadModel $leadModel, FormModel $formModel, PageModel $pageModel, MailHashHelper $mailHash, ThemeHelper $themeHelper, $idHash, ?string $urlEmail = null, ?string $secretHash = null)
|
||||
{
|
||||
$stat = $model->getEmailStatus($idHash);
|
||||
$message = '';
|
||||
$email = null;
|
||||
$lead = null;
|
||||
$template = null;
|
||||
$session = $request->getSession();
|
||||
$isOneClickUnsubscribe = $request->isMethod(Request::METHOD_POST) && 'One-Click' === $request->get('List-Unsubscribe');
|
||||
$isUnsubscribeAll = $request->get('unsubscribe_all');
|
||||
$showContactPreferences = $this->coreParametersHelper->get('show_contact_preferences');
|
||||
|
||||
if (!empty($stat)) {
|
||||
if ($isOneClickUnsubscribe) {
|
||||
// RFC 8058 One-Click unsubscribe
|
||||
$unsubscribeComment = $this->translator->trans('mautic.email.dnc.unsubscribed');
|
||||
$model->setDoNotContact($stat, $unsubscribeComment, DoNotContact::UNSUBSCRIBED);
|
||||
|
||||
return new Response($this->translator->trans('mautic.lead.do.not.contact_unsubscribed'));
|
||||
}
|
||||
|
||||
$email = $stat->getEmail();
|
||||
}
|
||||
|
||||
$isCorrectHash = $secretHash && $urlEmail && $mailHash->getEmailHash($urlEmail) === $secretHash;
|
||||
|
||||
if ($email) {
|
||||
$template = $email->getTemplate();
|
||||
if ('mautic_code_mode' === $template) {
|
||||
// Use system default
|
||||
$template = null;
|
||||
}
|
||||
|
||||
/** @var \Mautic\FormBundle\Entity\Form $unsubscribeForm */
|
||||
$unsubscribeForm = $email->getUnsubscribeForm();
|
||||
if (null != $unsubscribeForm && $unsubscribeForm->isPublished()) {
|
||||
$formTemplate = $unsubscribeForm->getTemplate();
|
||||
$formContent = '<div class="mautic-unsubscribeform">'.$formModel->getContent($unsubscribeForm).'</div>';
|
||||
}
|
||||
} else {
|
||||
if ($isOneClickUnsubscribe) {
|
||||
return new Response($this->translator->trans('mautic.email.stat_record.not_found'), Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($template) && empty($formTemplate)) {
|
||||
$template = $this->coreParametersHelper->get('theme');
|
||||
} elseif (!empty($formTemplate)) {
|
||||
$template = $formTemplate;
|
||||
}
|
||||
|
||||
$theme = $themeHelper->getTheme($template);
|
||||
if ($theme->getTheme() != $template) {
|
||||
$template = $theme->getTheme();
|
||||
}
|
||||
$contentTemplate = $themeHelper->checkForTwigTemplate('@themes/'.$template.'/html/message.html.twig');
|
||||
if (!empty($stat) || $isCorrectHash) {
|
||||
$successSessionName = 'mautic.email.prefscenter.success';
|
||||
|
||||
if (!empty($stat) && $lead = $stat->getLead()) {
|
||||
// Set the lead as current lead
|
||||
$contactTracker->setTrackedContact($lead);
|
||||
|
||||
// Set lead lang
|
||||
if ($language = $lead->getPreferredLocale()) {
|
||||
$this->translator->setLocale($language);
|
||||
}
|
||||
|
||||
// Add contact ID to the session name in case more contacts
|
||||
// share the same session/device and the contact is known.
|
||||
$successSessionName .= ".{$lead->getId()}";
|
||||
} elseif (empty($stat)) {
|
||||
$leadRepo = $leadModel->getRepository();
|
||||
$contacts = $leadRepo->getContactsByEmail($urlEmail);
|
||||
$lead = null;
|
||||
if (is_array($contacts) && count($contacts) > 0) {
|
||||
$lead = array_pop($contacts);
|
||||
} else {
|
||||
$message = $this->translator->trans('mautic.email.stat_record.not_found');
|
||||
}
|
||||
}
|
||||
|
||||
if (!$showContactPreferences || $isUnsubscribeAll) {
|
||||
if (!empty($stat)) {
|
||||
$message = $this->getUnsubscribeMessage($idHash, $model, $stat, $this->translator);
|
||||
} elseif ($lead && $lead instanceof Lead) {
|
||||
$message = $this->getUnsubscribeMessageLead($idHash, $model, $lead, $this->translator, $urlEmail);
|
||||
}
|
||||
} elseif ($lead) {
|
||||
$params = ['idHash' => $idHash, 'urlEmail' => $urlEmail];
|
||||
|
||||
if ($urlEmail) {
|
||||
$params['secretHash'] = $mailHash->getEmailHash($urlEmail);
|
||||
}
|
||||
|
||||
$action = $this->generateUrl('mautic_email_unsubscribe', $params);
|
||||
$viewParameters = [
|
||||
'lead' => $lead,
|
||||
'idHash' => $idHash,
|
||||
'showContactFrequency' => $this->coreParametersHelper->get('show_contact_frequency'),
|
||||
'showContactPauseDates' => $this->coreParametersHelper->get('show_contact_pause_dates'),
|
||||
'showContactPreferredChannels' => $this->coreParametersHelper->get('show_contact_preferred_channels'),
|
||||
'showContactCategories' => $this->coreParametersHelper->get('show_contact_categories'),
|
||||
'showContactSegments' => $this->coreParametersHelper->get('show_contact_segments'),
|
||||
'dncUrl' => $this->generateUrl('mautic_email_unsubscribe_all', $params),
|
||||
];
|
||||
|
||||
if ($session->get($successSessionName)) {
|
||||
$viewParameters['successMessage'] = $this->translator->trans('mautic.email.preferences_center_success_message.text');
|
||||
}
|
||||
|
||||
$form = $this->getFrequencyRuleForm($lead, $viewParameters, $data, true, $action, true);
|
||||
if (true === $form) {
|
||||
$session->set($successSessionName, 1);
|
||||
|
||||
return $this->postActionRedirect(
|
||||
[
|
||||
'returnUrl' => $action,
|
||||
'viewParameters' => $viewParameters,
|
||||
'contentTemplate' => $contentTemplate,
|
||||
]
|
||||
);
|
||||
} else {
|
||||
// success message should not persist on page refresh
|
||||
$session->set($successSessionName, 0);
|
||||
}
|
||||
|
||||
$formView = $form->createView();
|
||||
/** @var Page $prefCenter */
|
||||
if ($email && ($prefCenter = $email->getPreferenceCenter()) && $prefCenter->getIsPreferenceCenter()) {
|
||||
// Set the page language if there is no lead preferred locale
|
||||
if (empty($language) && $language = $prefCenter->getLanguage()) {
|
||||
$this->translator->setLocale($language);
|
||||
}
|
||||
|
||||
$html = $prefCenter->getCustomHtml();
|
||||
// check if tokens are present
|
||||
if (str_contains($html, BuilderSubscriber::saveprefsRegex)) {
|
||||
// set custom tag to inject end form
|
||||
// update show pref center tokens by looking for their presence in the html
|
||||
$showParameters = $this->buildShowParametersBasedOnContent($html, $viewParameters);
|
||||
$eventParameters = array_merge(
|
||||
$viewParameters,
|
||||
$showParameters,
|
||||
[
|
||||
'form' => $formView,
|
||||
'startform' => $this->renderView('@MauticCore/Default/form.html.twig', ['form' => $formView]),
|
||||
'custom_tag' => '<a name="end-'.$formView->vars['id'].'"></a>',
|
||||
]
|
||||
);
|
||||
|
||||
$event = new PageDisplayEvent($html, $prefCenter, $eventParameters);
|
||||
|
||||
$this->dispatcher->dispatch($event, PageEvents::PAGE_ON_DISPLAY);
|
||||
|
||||
$html = $event->getContent();
|
||||
|
||||
if (!$session->has($successSessionName)) {
|
||||
$successMessageData = ['class="pref-successmessage"'];
|
||||
$successMessageDataHidden = [];
|
||||
foreach ($successMessageData as $successMessageData) {
|
||||
$successMessageDataHidden[] = $successMessageData.' style=display:none';
|
||||
}
|
||||
$html = str_replace(
|
||||
$successMessageData,
|
||||
$successMessageDataHidden,
|
||||
$html
|
||||
);
|
||||
} else {
|
||||
$session->remove($successSessionName);
|
||||
}
|
||||
$html = preg_replace(
|
||||
'/'.BuilderSubscriber::identifierToken.'/',
|
||||
$lead->getPrimaryIdentifier(),
|
||||
$html
|
||||
);
|
||||
$pageModel->hitPage($prefCenter, $request, 200, $lead);
|
||||
} else {
|
||||
unset($html);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($html)) {
|
||||
$html = $this->render(
|
||||
'@MauticEmail/Lead/preference_options.html.twig',
|
||||
array_merge(
|
||||
$viewParameters,
|
||||
[
|
||||
'form' => $formView,
|
||||
'currentRoute' => $this->generateUrl(
|
||||
'mautic_contact_action',
|
||||
[
|
||||
'objectAction' => 'contactFrequency',
|
||||
'objectId' => $lead->getId(),
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
)->getContent();
|
||||
}
|
||||
$message = $html;
|
||||
}
|
||||
} else {
|
||||
$message = $this->translator->trans('mautic.email.stat_record.not_found');
|
||||
}
|
||||
|
||||
$config = $theme->getConfig();
|
||||
|
||||
$viewParams = [
|
||||
'email' => $email,
|
||||
'lead' => $lead,
|
||||
'template' => $template,
|
||||
'message' => $message,
|
||||
];
|
||||
|
||||
if (!empty($formContent)) {
|
||||
$viewParams['content'] = $formContent;
|
||||
if (in_array('form', $config['features'])) {
|
||||
$contentTemplate = $themeHelper->checkForTwigTemplate('@themes/'.$template.'/html/form.html.twig');
|
||||
} else {
|
||||
$viewParams['content'] = '';
|
||||
$viewParams['message'] = $message.$formContent;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render($contentTemplate, $viewParams);
|
||||
}
|
||||
|
||||
public function unsubscribeAllAction(Request $request, string $idHash, ?string $urlEmail = null, ?string $secretHash = null): Response
|
||||
{
|
||||
$request->attributes->set('unsubscribe_all', 1);
|
||||
|
||||
return $this->forward(static::class.'::unsubscribeAction', [
|
||||
'request' => $request,
|
||||
'idHash' => $idHash,
|
||||
'urlEmail' => $urlEmail,
|
||||
'secretHash' => $secretHash,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
* @throws \Mautic\CoreBundle\Exception\FileNotFoundException
|
||||
*/
|
||||
public function resubscribeAction(ContactTracker $contactTracker, EmailModel $model, MailHashHelper $mailHash, ThemeHelper $themeHelper, AssetsHelper $assetsHelper, AnalyticsHelper $analyticsHelper, $idHash): Response
|
||||
{
|
||||
$stat = $model->getEmailStatus($idHash);
|
||||
|
||||
if (!empty($stat)) {
|
||||
$email = $stat->getEmail();
|
||||
$lead = $stat->getLead();
|
||||
|
||||
if ($lead) {
|
||||
// Set the lead as current lead
|
||||
$contactTracker->setTrackedContact($lead);
|
||||
|
||||
if (!$this->translator instanceof LocaleAwareInterface) {
|
||||
throw new \LogicException(sprintf('$this->translator must be an instance of "%s"', LocaleAwareInterface::class));
|
||||
}
|
||||
|
||||
// Set lead lang
|
||||
if ($lead->getPreferredLocale()) {
|
||||
$this->translator->setLocale($lead->getPreferredLocale());
|
||||
}
|
||||
}
|
||||
|
||||
$model->removeDoNotContact($stat->getEmailAddress());
|
||||
|
||||
$message = $this->coreParametersHelper->get('resubscribe_message');
|
||||
$toEmail = $stat->getEmailAddress();
|
||||
$unsubscribeHash = $mailHash->getEmailHash($toEmail);
|
||||
|
||||
if (!$message) {
|
||||
$message = $this->translator->trans(
|
||||
'mautic.email.resubscribed.success',
|
||||
[
|
||||
'%unsubscribeUrl%' => '|URL|',
|
||||
'%email%' => '|EMAIL|',
|
||||
]
|
||||
);
|
||||
}
|
||||
$message = str_replace(
|
||||
[
|
||||
'|URL|',
|
||||
'|EMAIL|',
|
||||
],
|
||||
[
|
||||
$this->generateUrl('mautic_email_unsubscribe', ['idHash' => $idHash, 'urlEmail' => $toEmail, 'secretHash' => $unsubscribeHash]),
|
||||
$stat->getEmailAddress(),
|
||||
],
|
||||
$message
|
||||
);
|
||||
} else {
|
||||
$email = $lead = false;
|
||||
$message = $this->translator->trans('mautic.email.stat_record.not_found');
|
||||
}
|
||||
|
||||
$template = (!empty($email) && 'mautic_code_mode' !== $email->getTemplate()) ? $email->getTemplate() : $this->coreParametersHelper->get('theme');
|
||||
|
||||
$theme = $themeHelper->getTheme($template);
|
||||
|
||||
if ($theme->getTheme() != $template) {
|
||||
$template = $theme->getTheme();
|
||||
}
|
||||
|
||||
// Ensure template still exists
|
||||
$theme = $themeHelper->getTheme($template);
|
||||
if (empty($theme) || $theme->getTheme() !== $template) {
|
||||
$template = $this->coreParametersHelper->get('theme');
|
||||
}
|
||||
|
||||
$analytics = $analyticsHelper->getCode();
|
||||
|
||||
if (!empty($analytics)) {
|
||||
$assetsHelper->addCustomDeclaration($analytics);
|
||||
}
|
||||
|
||||
$logicalName = $themeHelper->checkForTwigTemplate('@themes/'.$template.'/html/message.html.twig');
|
||||
|
||||
return $this->render(
|
||||
$logicalName,
|
||||
[
|
||||
'message' => $message,
|
||||
'type' => 'notice',
|
||||
'email' => $email,
|
||||
'lead' => $lead,
|
||||
'template' => $template,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mailer transport webhook post.
|
||||
*/
|
||||
public function mailerCallbackAction(Request $request): Response
|
||||
{
|
||||
$event = new TransportWebhookEvent($request);
|
||||
$this->dispatcher->dispatch($event, EmailEvents::ON_TRANSPORT_WEBHOOK);
|
||||
|
||||
return $event->getResponse() ?? new Response('No email transport that could process this callback was found', Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview email.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function previewAction(
|
||||
AnalyticsHelper $analyticsHelper,
|
||||
ThemeHelper $themeHelper,
|
||||
AssetsHelper $assetsHelper,
|
||||
EmailConfig $emailConfig,
|
||||
EmailModel $model,
|
||||
Request $request,
|
||||
LeadModel $leadModel,
|
||||
FakeContactHelper $fakeLeadHelper,
|
||||
string $objectId,
|
||||
?string $objectType = null,
|
||||
) {
|
||||
$contactId = (int) $request->query->get('contactId');
|
||||
$emailEntity = $model->getEntity($objectId);
|
||||
|
||||
if (null === $emailEntity) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
$publicPreview = $emailEntity->isPublicPreview();
|
||||
$draftEnabled = $emailConfig->isDraftEnabled();
|
||||
if ('draft' === $objectType && $draftEnabled && $emailEntity->hasDraft()) {
|
||||
$publicPreview = $emailEntity->getDraft()->isPublicPreview();
|
||||
}
|
||||
|
||||
if (
|
||||
($this->security->isAnonymous() && !$publicPreview)
|
||||
|| (!$this->security->isAnonymous()
|
||||
&& !$this->security->hasEntityAccess(
|
||||
'email:emails:viewown',
|
||||
'email:emails:viewother',
|
||||
$emailEntity->getCreatedBy()
|
||||
))
|
||||
) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
// bogus ID
|
||||
if ($contactId && (
|
||||
!$this->security->isAdmin()
|
||||
&& !$this->security->hasEntityAccess('lead:leads:viewown', 'lead:leads:viewother')
|
||||
)
|
||||
) {
|
||||
// disallow displaying contact information
|
||||
$contactId = null;
|
||||
}
|
||||
|
||||
// bogus ID
|
||||
$idHash = 'xxxxxxxxxxxxxx';
|
||||
|
||||
$BCcontent = $emailEntity->getContent();
|
||||
$content = $emailEntity->getCustomHtml();
|
||||
|
||||
if ('draft' === $objectType && $draftEnabled && $emailEntity->hasDraft()) {
|
||||
$content = $emailEntity->getDraftContent();
|
||||
}
|
||||
|
||||
if (empty($content) && !empty($BCcontent)) {
|
||||
$template = $emailEntity->getTemplate();
|
||||
|
||||
$assetsHelper->addCustomDeclaration('<meta name="robots" content="noindex">');
|
||||
|
||||
$logicalName = $themeHelper->checkForTwigTemplate('@themes/'.$template.'/html/email.html.twig');
|
||||
|
||||
$response = $this->render(
|
||||
$logicalName,
|
||||
[
|
||||
'inBrowser' => true,
|
||||
'content' => $emailEntity->getContent(),
|
||||
'email' => $emailEntity,
|
||||
'lead' => null,
|
||||
'template' => $template,
|
||||
]
|
||||
);
|
||||
|
||||
// replace tokens
|
||||
$content = $response->getContent();
|
||||
}
|
||||
|
||||
// Override tracking_pixel
|
||||
$tokens = ['{tracking_pixel}' => ''];
|
||||
|
||||
// Prepare contact
|
||||
if ($contactId) {
|
||||
// We have one from request parameter
|
||||
/** @var LeadModel $leadModel */
|
||||
$contact = $leadModel->getRepository()->getLead($contactId);
|
||||
$contact = $model->enrichedContactWithCompanies($contact);
|
||||
} else {
|
||||
// Make fake contact.
|
||||
/** @var FakeContactHelper $fakeLeadHelper */
|
||||
$contact = $fakeLeadHelper->prepareFakeContactWithPrimaryCompany();
|
||||
}
|
||||
// Generate and replace tokens
|
||||
$event = new EmailSendEvent(
|
||||
null,
|
||||
[
|
||||
'content' => $content,
|
||||
'email' => $emailEntity,
|
||||
'idHash' => $idHash,
|
||||
'tokens' => $tokens,
|
||||
'internalSend' => true,
|
||||
'lead' => $contact,
|
||||
]
|
||||
);
|
||||
$this->dispatcher->dispatch($event, EmailEvents::EMAIL_ON_DISPLAY);
|
||||
|
||||
$content = $event->getContent(true);
|
||||
|
||||
if ($this->security->isAnonymous()) {
|
||||
$content = $analyticsHelper->addCode($content);
|
||||
}
|
||||
|
||||
return new Response($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function doTracking(Request $request, IntegrationHelper $integrationHelper, MailHelper $mailer, LoggerInterface $mauticLogger, $integration): void
|
||||
{
|
||||
$logger = $mauticLogger;
|
||||
|
||||
// if additional data were sent with the tracking pixel
|
||||
$query_string = $request->server->get('QUERY_STRING');
|
||||
if (!$query_string) {
|
||||
$logger->log('error', $integration.': query string is not available');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (str_starts_with($query_string, 'r=')) {
|
||||
$query_string = substr($query_string, strpos($query_string, '?') + 1);
|
||||
} // remove route variable
|
||||
|
||||
parse_str($query_string, $query);
|
||||
|
||||
// URL attr 'd' is encoded so let's decode it first.
|
||||
if (!isset($query['d'], $query['sig'])) {
|
||||
$logger->log('error', $integration.': query variables are not found');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// get secret from plugin settings
|
||||
$myIntegration = $integrationHelper->getIntegrationObject($integration);
|
||||
|
||||
if (!$myIntegration) {
|
||||
$logger->log('error', $integration.': integration not found');
|
||||
|
||||
return;
|
||||
}
|
||||
$keys = $myIntegration->getDecryptedApiKeys();
|
||||
|
||||
// generate signature
|
||||
$salt = $keys['secret'];
|
||||
if (!str_contains($salt, '$1$')) {
|
||||
$salt = '$1$'.$salt;
|
||||
} // add MD5 prefix
|
||||
$cr = crypt(urlencode($query['d']), $salt);
|
||||
$mySig = hash('crc32b', $cr); // this hash type is used in c#
|
||||
|
||||
// compare signatures
|
||||
if (hash_equals($mySig, $query['sig'])) {
|
||||
// decode and parse query variables
|
||||
$b64 = base64_decode($query['d']);
|
||||
$gz = gzdecode($b64);
|
||||
parse_str($gz, $query);
|
||||
} else {
|
||||
// signatures don't match: stop
|
||||
$logger->log('error', $integration.': signatures don\'t match');
|
||||
|
||||
unset($query);
|
||||
}
|
||||
|
||||
if (empty($query) || !isset($query['email'], $query['subject'], $query['body'])) {
|
||||
$logger->log('error', $integration.': query variables are empty');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (MAUTIC_ENV === 'dev') {
|
||||
$logger->log('error', $integration.': '.json_encode($query, JSON_PRETTY_PRINT));
|
||||
}
|
||||
|
||||
/** @var EmailModel $model */
|
||||
$model = $this->getModel('email');
|
||||
|
||||
// email is a semicolon delimited list of emails
|
||||
$emails = explode(';', $query['email']);
|
||||
$leadModel = $this->getModel('lead');
|
||||
\assert($leadModel instanceof LeadModel);
|
||||
$repo = $leadModel->getRepository();
|
||||
|
||||
foreach ($emails as $email) {
|
||||
$lead = $repo->getLeadByEmail($email);
|
||||
if (null === $lead) {
|
||||
$lead = $this->createLead($email, $repo);
|
||||
if (null === $lead) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$idHash = hash('crc32', $email.$query['body']);
|
||||
$idHash = substr($idHash.$idHash, 0, 13); // 13 bytes length
|
||||
|
||||
$stat = $model->getEmailStatus($idHash);
|
||||
|
||||
// stat doesn't exist, create one
|
||||
if (null === $stat) {
|
||||
$lead['email'] = $email; // needed for stat
|
||||
$stat = $this->addStat($mailer, $lead, $email, $query, $idHash);
|
||||
}
|
||||
|
||||
$stat->setSource('email.client');
|
||||
|
||||
if ($stat || 'Outlook' !== $integration) { // Outlook requests the tracking gif on send
|
||||
$model->hitEmail($idHash, $request); // add email event
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Response
|
||||
*/
|
||||
public function pluginTrackingGifAction(Request $request, IntegrationHelper $integrationHelper, MailHelper $mailer, LoggerInterface $mauticLogger, $integration)
|
||||
{
|
||||
$this->doTracking($request, $integrationHelper, $mailer, $mauticLogger, $integration);
|
||||
|
||||
return TrackingPixelHelper::getResponse($request); // send gif
|
||||
}
|
||||
|
||||
private function addStat(MailHelper $mailer, $lead, $email, $query, $idHash): ?Stat
|
||||
{
|
||||
if (null !== $lead) {
|
||||
// To lead
|
||||
$mailer->addTo($email);
|
||||
|
||||
// sanitize variables to prevent malicious content
|
||||
$from = filter_var($query['from'], FILTER_SANITIZE_EMAIL);
|
||||
$mailer->setFrom($from, '');
|
||||
|
||||
// Set Content
|
||||
$body = htmlspecialchars(filter_var($query['body'], FILTER_FLAG_STRIP_HIGH));
|
||||
$mailer->setBody($body);
|
||||
$mailer->parsePlainText($body);
|
||||
|
||||
// Set lead
|
||||
$mailer->setLead($lead);
|
||||
$mailer->setIdHash($idHash);
|
||||
|
||||
$subject = htmlspecialchars(filter_var($query['subject'], FILTER_FLAG_STRIP_HIGH));
|
||||
$mailer->setSubject($subject);
|
||||
|
||||
return $mailer->createEmailStat();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function createLead($email, $repo): ?Lead
|
||||
{
|
||||
$model = $this->getModel('lead.lead');
|
||||
\assert($model instanceof LeadModel);
|
||||
$lead = $model->getEntity();
|
||||
// set custom field values
|
||||
$data = ['email' => $email];
|
||||
$model->setFieldValues($lead, $data, true);
|
||||
// create lead
|
||||
$model->saveEntity($lead);
|
||||
|
||||
// return entity
|
||||
return $repo->getLeadByEmail($email);
|
||||
}
|
||||
|
||||
public function getUnsubscribeMessage($idHash, $model, $stat, $translator): string
|
||||
{
|
||||
$model->setDoNotContact($stat, $translator->trans('mautic.email.dnc.unsubscribed'), DoNotContact::UNSUBSCRIBED);
|
||||
|
||||
return $this->getUnsubscribeText($translator, $stat->getEmailAddress(), $idHash);
|
||||
}
|
||||
|
||||
public function getUnsubscribeMessageLead(string $idHash, EmailModel $model, Lead $lead, TranslatorInterface $translator, string $urlEmail): string
|
||||
{
|
||||
$model->setDoNotContactLead($lead, $translator->trans('mautic.email.dnc.unsubscribed'), DoNotContact::UNSUBSCRIBED);
|
||||
|
||||
return $this->getUnsubscribeText($translator, $urlEmail, $idHash);
|
||||
}
|
||||
|
||||
private function getUnsubscribeText(TranslatorInterface $translator, string $email, string $idHash): string
|
||||
{
|
||||
$message = $this->coreParametersHelper->get('unsubscribe_message');
|
||||
if (!$message) {
|
||||
$message = $translator->trans(
|
||||
'mautic.email.unsubscribed.success',
|
||||
[
|
||||
'%resubscribeUrl%' => '|URL|',
|
||||
'%email%' => '|EMAIL|',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return str_replace(
|
||||
[
|
||||
'|URL|',
|
||||
'|EMAIL|',
|
||||
],
|
||||
[
|
||||
$this->generateUrl('mautic_email_resubscribe', ['idHash' => $idHash]),
|
||||
$email,
|
||||
],
|
||||
$message
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The $viewParameters here have already been used to build the $form.
|
||||
* Fields that are set to show based on the app configuration are part
|
||||
* of the form. If the field is not configured to show, but a token exists
|
||||
* for that field in the content, then we need to keep the configuration
|
||||
* value instead of letting the content determine if it should show. This
|
||||
* is because of what was stated above - fields that are not configured to
|
||||
* to show are not part of the form. Attempting to render them will result
|
||||
* in an error.
|
||||
*
|
||||
* @param mixed[] $viewParameters
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
private function buildShowParametersBasedOnContent(string $content, array $viewParameters): array
|
||||
{
|
||||
/*
|
||||
* Since we're going to be merging this with the $viewParameters, filter out `true` values. We do not
|
||||
* want to change a configured value from `false` to `true` because a value of `false` in the $viewParameters
|
||||
* means that the field is not configured to show and therefore is not part of the form. Attempting to
|
||||
* render that field just because a token for it exists will result in an error.
|
||||
*/
|
||||
$showParamsBasedOnContent = array_filter([
|
||||
'showContactFrequency' => str_contains($content, BuilderSubscriber::channelfrequency),
|
||||
'showContactSegments' => str_contains($content, BuilderSubscriber::segmentListRegex),
|
||||
'showContactCategories' => str_contains($content, BuilderSubscriber::categoryListRegex),
|
||||
'showContactPreferredChannels' => str_contains($content, BuilderSubscriber::preferredchannel),
|
||||
], fn (bool $value) =>!$value);
|
||||
|
||||
$showParamsBasedOnConfiguration = array_filter($viewParameters, fn ($key) => str_starts_with($key, 'show'), ARRAY_FILTER_USE_KEY);
|
||||
|
||||
return array_merge($showParamsBasedOnConfiguration, $showParamsBasedOnContent);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user