src/Controller/CampaignsController.php line 300

Open in your IDE?
  1. <?php
  2. namespace App\Controller;
  3. use App\Entity\Campaign;
  4. use App\Entity\CampaignContact;
  5. use App\Entity\CampaignLink;
  6. use App\Entity\CampaignLinkHit;
  7. use App\Entity\CustomFields;
  8. use App\Entity\RecurringCampaign;
  9. use App\Entity\SendEvent;
  10. use App\Entity\ThreadSafe\ThreadSafeEntityManager;
  11. use App\Entity\User;
  12. use App\Event\CampaignContactUpdateEvent;
  13. use App\Form\CampaignType;
  14. use App\Service\Campaign\CampaignManager;
  15. use App\Service\DataTables\SSP;
  16. use App\Service\Stats;
  17. use Doctrine\DBAL\LockMode;
  18. use Doctrine\ORM\EntityManagerInterface;
  19. use Psr\Log\LoggerInterface;
  20. use Symfony\Component\Form\Extension\Core\Type\SubmitType;
  21. use Symfony\Component\HttpFoundation\JsonResponse;
  22. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  23. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  24. use Symfony\Component\Form\FormError;
  25. use Symfony\Component\HttpFoundation\RedirectResponse;
  26. use Symfony\Component\Routing\RouterInterface;
  27. use Symfony\Component\Security\Core\Security;
  28. use Symfony\Component\HttpFoundation\Request;
  29. use Symfony\Component\HttpFoundation\Response;
  30. use Symfony\Component\Validator\Validator\ValidatorInterface;
  31. use Symfony\Contracts\Translation\TranslatorInterface;
  32. use Twig\Markup;
  33. class CampaignsController extends UserPageController {
  34. const PAGE_TITLE = 'page.title.campaigns';
  35. protected $page;
  36. protected $eventDispatcher;
  37. /** @var User */
  38. protected $user;
  39. protected $em;
  40. protected $stats;
  41. protected $router;
  42. protected $logger;
  43. public function __construct(Security $security, TranslatorInterface $translator, EntityManagerInterface $entityManager, Stats $stats, RouterInterface $router, EventDispatcherInterface $eventDispatcher, LoggerInterface $logger)
  44. {
  45. $this->em = $entityManager;
  46. $this->eventDispatcher = $eventDispatcher;
  47. $this->stats = $stats;
  48. $this->user = $security->getUser();
  49. $this->router = $router;
  50. $this->logger = $logger;
  51. $this->addBreadcrumb('Dashboard', $this->router->generate('utmailer_dashboard'));
  52. $this->addBreadcrumb($translator->trans('entity.campaign', ['num' => 2]), $this->router->generate('utmailer_campaigns'));
  53. }
  54. public function index() : Response
  55. {
  56. // Only access if user profile setup is complete
  57. if(!$this->isGranted('setup-complete', $this->user) || !$this->isGranted('domain-auth-complete', $this->user)){
  58. $this->addFlash('warning', $this->translator->trans('user.complete_profile_setup', ['setup_url' => $this->generateUrl('utmailer_user_setup_1')]));
  59. return $this->redirectToRoute('utmailer_user_setup_1', [], 302);
  60. }
  61. $cRepo = $this->getDoctrine()->getRepository(Campaign::class);
  62. $campaigns = $cRepo->findBy(array(
  63. 'user' => $this->user
  64. ), [ 'publish_at' => 'DESC' ]);
  65. $campaignsList = array();
  66. foreach ($campaigns as $campaign) {
  67. $campaignsList[] = [
  68. 'campaign' => $campaign,
  69. 'stats' => $this->stats->getCampaignsStatsFromTotalCounts($this->user, $campaign, [
  70. 'contacts',
  71. SendEvent::TYPE_DELIVERED,
  72. SendEvent::TYPE_BOUNCED
  73. ])
  74. ];
  75. }
  76. $params = array(
  77. 'page' => $this->page,
  78. 'campaigns' => $campaignsList
  79. );
  80. return $this->render('campaigns-list.html.twig', $params);
  81. }
  82. public function view($id, Request $request) : Response
  83. {
  84. $cRepo = $this->getDoctrine()->getRepository(Campaign::class);
  85. $campaign = $cRepo->find($id);
  86. $this->addBreadcrumb($campaign->getName(), $this->router->generate('utmailer_campaign_view', ['id' => $id]));
  87. // Is campaign not owned by user?
  88. if(!$this->user->getCampaigns()->contains($campaign)){
  89. return $this->redirect($this->generateUrl('utmailer_campaigns', array()), 301);
  90. }
  91. $campaignStats = $this->stats->getCampaignsStatsFromTotalCounts($this->user, $campaign, [
  92. 'contacts',
  93. SendEvent::TYPE_DELIVERED,
  94. SendEvent::TYPE_OPENED,
  95. SendEvent::TYPE_CLICKED,
  96. SendEvent::TYPE_BOUNCED
  97. ]);
  98. $campaignTimeline = $this->stats->getCampaignTimeline($campaign, 15);
  99. $labels = array();
  100. $delivered = array();
  101. $opened = array();
  102. $bounced = array();
  103. foreach ($campaignTimeline as $date => $stats){
  104. $labels[] = $date;
  105. $delivered[] = $stats['delivered'];
  106. $opened[] = $stats['opened'];
  107. $clicked[] = $stats['clicked'];
  108. $bounced[] = $stats['bounced'];
  109. }
  110. $campaignChart = [
  111. 'labels' => $labels,
  112. 'datasets' => [
  113. [
  114. 'label' => 'Delivered',
  115. 'data' => $delivered,
  116. 'style' => 'info'
  117. ],
  118. [
  119. 'label' => 'Opened',
  120. 'data' => $opened,
  121. 'style' => 'primary'
  122. ],
  123. [
  124. 'label' => 'Clicked',
  125. 'data' => $clicked,
  126. 'style' => 'success'
  127. ],
  128. [
  129. 'label' => 'Bounced',
  130. 'data' => $bounced,
  131. 'style' => 'warning'
  132. ]
  133. ]
  134. ];
  135. $cLinksRepo = $this->em->getRepository(CampaignLink::class);
  136. $cLinks = $cLinksRepo->createQueryBuilder('l')
  137. ->addSelect('count(h.id) AS HIDDEN hitsNum')
  138. ->leftJoin('l.hits', 'h')
  139. ->where('l.campaign = :campaign')
  140. ->setParameter('campaign', $campaign)
  141. ->orderBy('hitsNum', 'DESC')
  142. ->groupBy('l.id')
  143. ->getQuery()
  144. ->getResult();
  145. $links = array();
  146. $cLinkHitsRepo = $this->em->getRepository(CampaignLinkHit::class);
  147. foreach ($cLinks as $link){
  148. $total = $cLinkHitsRepo->countTotalForLink($link);
  149. $unique = $cLinkHitsRepo->countUniqueForLink($link);
  150. $links[] = [
  151. 'link' => $link,
  152. 'hits' => $total,
  153. 'uniqueHits' => $unique
  154. ];
  155. }
  156. return $this->render('campaigns-view.html.twig', [
  157. 'page' => $this->page,
  158. 'campaign' => $campaign,
  159. 'links' => $links,
  160. 'campaign_stats' => $campaignStats,
  161. 'campaign_chart' => $campaignChart
  162. ]);
  163. }
  164. public function create(bool $recurring, Request $request, ValidatorInterface $validator, TranslatorInterface $translator, CampaignManager $cm) : Response
  165. {
  166. $this->addBreadcrumb($translator->trans('entity.actions.create', ['entity' => $translator->trans('entity.campaign', ['num'=>1])]), $this->router->generate('utmailer_campaign_create'));
  167. // Only access if user profile setup is complete
  168. if(!$this->isGranted('setup-complete', $this->user) || !$this->isGranted('domain-auth-complete', $this->user)){
  169. $this->addFlash('warning', $this->translator->trans('user.complete_profile_setup', ['setup_url' => $this->generateUrl('utmailer_user_setup_1')]));
  170. return $this->redirectToRoute('utmailer_user_setup_1', [], 302);
  171. }
  172. $form = $this->createForm(CampaignType::class, null, [
  173. 'recurring' => $recurring,
  174. ]);
  175. $form->handleRequest($request);
  176. if($form->isSubmitted() && $form->isValid()){
  177. /** @var Campaign $campaign */
  178. $campaign = $form->getData();
  179. $campaign->setUser($this->user);
  180. $errors = $validator->validate($campaign);
  181. if (count($errors) > 0) {
  182. foreach ($errors as $error) {
  183. $form->addError(new FormError($error->getMessage()));
  184. }
  185. } else {
  186. // If send button clicked, send now!
  187. if(isset($request->request->get('campaign')['send']) && $this->user->getStatus() !== User::STATUS_SUSPENDED){
  188. $campaign->setSent(true);
  189. $campaign->setActive(true);
  190. $campaign->setPublishAt(new \DateTime());
  191. $task = $cm->makeSendTask($campaign);
  192. $entityManager = $this->getDoctrine()->getManager();
  193. $entityManager->persist($campaign);
  194. $entityManager->flush();
  195. $flash = new Markup($this->renderView('flashes/campaign-task-created.html.twig'), 'UTF-8');
  196. $this->addFlash('success', $flash);
  197. return new RedirectResponse($this->generateUrl('utmailer_campaign_view', ['id' => $campaign->getId()]), Response::HTTP_MOVED_PERMANENTLY);
  198. } else {
  199. if ($this->user->getStatus() === User::STATUS_SUSPENDED){
  200. $campaign->setActive(false);
  201. }
  202. $campaign->setSent(false);
  203. $entityManager = $this->getDoctrine()->getManager();
  204. if ($recurring) {
  205. $repeat = $form['repeat']->getData();
  206. $contactGroups = [];
  207. foreach ($form['contactGroups']->getData() as $cg) {
  208. $contactGroups[] = $cg->getId();
  209. };
  210. $contactFilters = json_decode($form['contactFilters']->getData());
  211. $rules = [
  212. 'contact_groups' => $contactGroups,
  213. 'contact_filters' => $contactFilters,
  214. 'repeat' => $repeat,
  215. ];
  216. $recurringCampaign = new RecurringCampaign();
  217. $recurringCampaign->setName($form['name']->getData());
  218. $recurringCampaign->setStatus(($form['active']->getData()) ? 'active' : 'inactive');
  219. $recurringCampaign->setRules($rules);
  220. $recurringCampaign->addCampaign($campaign);
  221. $entityManager->persist($recurringCampaign);
  222. }
  223. $entityManager->persist($campaign);
  224. $entityManager->flush();
  225. $this->addFlash('success', $translator->trans('campaign.created'));
  226. return new RedirectResponse($this->generateUrl('utmailer_campaign_edit', ['id' => $campaign->getId()]));
  227. }
  228. }
  229. }
  230. $customFields = array();
  231. if (!$user_custom_fields = $this->user->getCustomFields()) {
  232. $user_custom_fields = new CustomFields();
  233. $this->user->setCustomFields($user_custom_fields);
  234. $this->em->persist($this->user);
  235. $this->em->flush();
  236. }
  237. foreach ($user_custom_fields->getCustomFields() as $customFieldNum => $customField) {
  238. if ($customField) {
  239. $customFields[] = [$customFieldNum => $customField];
  240. }
  241. }
  242. if ($recurring) {
  243. return $this->render('recurring-campaigns-create.html.twig', [
  244. 'page' => $this->page,
  245. 'campaign_form' => $form->createView(),
  246. 'customFields' => $customFields
  247. ]);
  248. }
  249. return $this->render('campaigns-create.html.twig', [
  250. 'page' => $this->page,
  251. 'campaign_form' => $form->createView(),
  252. 'customFields' => $customFields
  253. ]);
  254. }
  255. public function edit($id, bool $recurring = false, Request $request, ValidatorInterface $validator, TranslatorInterface $translator, CampaignManager $cm) : Response
  256. {
  257. $cRepo = $this->getDoctrine()->getRepository(Campaign::class);
  258. $campaign = $cRepo->find($id);
  259. // Campaigns already sent cannot be modified
  260. if($campaign->isSent()){
  261. $this->addFlash('warning', $translator->trans('flashes.cannot_modify_sent_campaign'));
  262. return new RedirectResponse($this->generateUrl('utmailer_campaign_view', ['id' => $campaign->getId()]), Response::HTTP_FOUND);
  263. }
  264. $this->addBreadcrumb($campaign->getName(), $this->router->generate('utmailer_campaign_edit', ['id' => $id]));
  265. // Is campaign not owned by user?
  266. if(!$this->user->getCampaigns()->contains($campaign)){
  267. return $this->redirect($this->generateUrl('utmailer_campaigns', array()), 301);
  268. }
  269. $form = $this->createForm(CampaignType::class, $campaign, array('user' => $this->user));
  270. $form->handleRequest($request);
  271. if($form->isSubmitted() && $form->isValid()){
  272. $campaign = $form->getData();
  273. $campaign->setUser($this->user);
  274. $errors = $validator->validate($campaign);
  275. if (count($errors) > 0) {
  276. foreach ($errors as $error) {
  277. $form->addError(new FormError($error->getMessage()));
  278. }
  279. } else {
  280. // If send button clicked, send now!
  281. if(isset($request->request->get('campaign')['send']) && $this->user->getStatus() !== User::STATUS_SUSPENDED){
  282. $campaign->setSent(true);
  283. $campaign->setActive(true);
  284. $campaign->setPublishAt(new \DateTime());
  285. $task = $cm->makeSendTask($campaign);
  286. $entityManager = $this->getDoctrine()->getManager();
  287. $entityManager->persist($campaign);
  288. $entityManager->flush();
  289. $flash = new Markup($this->renderView('flashes/campaign-task-created.html.twig'), 'UTF-8');
  290. $this->addFlash('success', $flash);
  291. return new RedirectResponse($this->generateUrl('utmailer_campaign_view', ['id' => $campaign->getId()]), Response::HTTP_MOVED_PERMANENTLY);
  292. } else {
  293. if ($this->user->getStatus() === User::STATUS_SUSPENDED){
  294. $campaign->setActive(false);
  295. }
  296. $entityManager = $this->getDoctrine()->getManager();
  297. $entityManager->persist($campaign);
  298. $entityManager->flush();
  299. $this->addFlash('success', $translator->trans('campaign.saved'));
  300. return new RedirectResponse($this->generateUrl('utmailer_campaign_edit', ['id' => $campaign->getId()]));
  301. }
  302. }
  303. }
  304. $customFields = array();
  305. if (!$user_custom_fields = $this->user->getCustomFields()) {
  306. $user_custom_fields = new CustomFields();
  307. $this->user->setCustomFields($user_custom_fields);
  308. $this->em->persist($this->user);
  309. $this->em->flush();
  310. }
  311. foreach ($user_custom_fields->getCustomFields() as $customFieldNum => $customField) {
  312. if ($customField) {
  313. $customFields[] = [$customFieldNum => $customField];
  314. }
  315. }
  316. return $this->render('campaigns-edit.html.twig', [
  317. 'page' => $this->page,
  318. 'campaign_form' => $form->createView(),
  319. 'campaign' => $campaign,
  320. 'customFields' => $customFields,
  321. ]);
  322. }
  323. public function delete(Request $request, int $id, TranslatorInterface $translator) : Response
  324. {
  325. $cRepo = $this->em->getRepository(Campaign::class);
  326. // Check campaign is owned by user
  327. if($campaign = $cRepo->findOneBy(['user' => $this->user, 'id' => $id])){
  328. $this->em->remove($campaign);
  329. $this->em->flush();
  330. }
  331. $referer = $request->headers->get('referer');
  332. if ($referer == NULL) {
  333. $referer = $this->generateUrl('utmailer_campaigns');
  334. }
  335. $this->addFlash('success', $translator->trans('Campaign') . ' ' . $translator->trans('deleted', ['gender' => 'female', 'num' => '1']));
  336. return new RedirectResponse($referer);
  337. }
  338. public function getBody($id) : Response
  339. {
  340. $cRepo = $this->getDoctrine()->getRepository(Email::class);
  341. $email = $cRepo->find($id);
  342. return new Response($email->getBody(), 200, ['Content-Type' => 'application/json']);
  343. }
  344. public function campaignLink($token, ThreadSafeEntityManager $em) : Response
  345. {
  346. $tok = explode('.', $token);
  347. // check token parts
  348. if (count($tok) !== 3) {
  349. // not valid token
  350. throw $this->createNotFoundException('Resource not found');
  351. }
  352. // check link id segment
  353. if (!isset($tok[0]) || !filter_var($tok[0], FILTER_VALIDATE_INT)) {
  354. // not valid id
  355. throw $this->createNotFoundException('Resource not found');
  356. }
  357. // check campaign contact id segment
  358. if (!isset($tok[1]) || !filter_var($tok[1], FILTER_VALIDATE_INT)) {
  359. // not valid id
  360. throw $this->createNotFoundException('Resource not found');
  361. }
  362. $linkId = $tok[0];
  363. $campaignContactId = $tok[1];
  364. $sRepo = $this->em->getRepository(SendEvent::class);
  365. if(($link = $this->em->getRepository(CampaignLink::class)->find($linkId)) && ($campaignContact = $this->em->getRepository(CampaignContact::class)->find($campaignContactId))){
  366. if ($tok[2] !== hash_hmac('sha256', $link->getUrl().$campaignContact->getId(), $this->getParameter('app.secret'))) {
  367. // failed verification
  368. throw $this->createNotFoundException('Resource not found');
  369. } else {
  370. // Record link hit
  371. $linkHit = new CampaignLinkHit();
  372. $linkHit->setCampaignContact($campaignContact);
  373. $linkHit->setCampaignLink($link);
  374. $linkHit->setDateCreated(new \DateTime());
  375. $this->em->persist($linkHit);
  376. $this->em->flush();
  377. // Save Clicked sendEvent only if not already exists
  378. if(!$sRepo->findOneBy(['campaign_contact' => $campaignContact->getId(), 'event' => 'clicked'])){
  379. // Record clicked SendEvent, campaign contact status will be updated by event listener
  380. $sendEvent = new SendEvent();
  381. $sendEvent->setCampaignContact($campaignContact);
  382. $sendEvent->setCreatedAt(new \DateTime());
  383. $sendEvent->setEvent('clicked');
  384. $this->em->persist($sendEvent);
  385. $this->em->flush();
  386. // Update campaign contact and campaign totals with thread safety
  387. $callback = function() use ($em, $campaignContactId) {
  388. // Get repository inside callable to make sure EntityManager is valid
  389. $ccRepo = $em->getRepository(CampaignContact::class);
  390. // Fetch account with FOR UPDATE write lock
  391. $campaignContact = $ccRepo->find(
  392. $campaignContactId,
  393. LockMode::PESSIMISTIC_WRITE
  394. );
  395. if(in_array($campaignContact->getStatus(), [
  396. CampaignContact::STATUS_PENDING,
  397. CampaignContact::STATUS_PROCESSED,
  398. CampaignContact::STATUS_DEFERRED,
  399. CampaignContact::STATUS_DELIVERED,
  400. CampaignContact::STATUS_OPENED
  401. ])){
  402. $campaignContact->setStatus(CampaignContact::STATUS_CLICKED);
  403. $campaign = $em->getRepository(Campaign::class)->find($campaignContact->getCampaign()->getId(), LockMode::PESSIMISTIC_WRITE);
  404. $campaign->addDailyCount(SendEvent::TYPE_CLICKED);
  405. }
  406. return true;
  407. };
  408. $em->transactional($callback);
  409. }
  410. return new RedirectResponse($link->getUrl(), 301);
  411. }
  412. } else {
  413. throw $this->createNotFoundException('Resource not found');
  414. }
  415. }
  416. public function unsubscribe(Request $request, $token) : Response
  417. {
  418. $tok = explode('.', $token);
  419. // check token parts
  420. if (count($tok) !== 2) {
  421. // not valid token
  422. throw $this->createNotFoundException('Resource not found');
  423. }
  424. // check id segment
  425. if (!isset($tok[0]) || !filter_var($tok[0], FILTER_VALIDATE_INT)) {
  426. // not valid id
  427. throw $this->createNotFoundException('Resource not found');
  428. }
  429. $campaignContactId = $tok[0];
  430. if(!$campaignContact = $this->em->getRepository(CampaignContact::class)->find($campaignContactId)) {
  431. throw $this->createNotFoundException('Resource not found');
  432. }
  433. $user = $campaignContact->getCampaign()->getUser();
  434. $unsubForm = $this->createFormBuilder()->add('confirm', SubmitType::class, [
  435. 'label' => 'Confirm'
  436. ]);
  437. if ($tok[1] !== hash_hmac('sha256', $campaignContact->getContact()->getId(), $this->getParameter('app.secret'))) {
  438. // failed verification
  439. throw $this->createNotFoundException('Resource not found');
  440. }
  441. $unsubForm = $this->createFormBuilder()->add('confirm', SubmitType::class, [
  442. 'label' => 'Confirm'
  443. ])->getForm();
  444. $unsubForm->handleRequest($request);
  445. if($unsubForm->isSubmitted() && $unsubForm->isValid()){
  446. $sendEvent = new SendEvent();
  447. $sendEvent->setCampaignContact($campaignContact);
  448. $sendEvent->setCreatedAt(new \DateTime('@'.time()));
  449. $sendEvent->setEvent('unsubscribed');
  450. $event = new CampaignContactUpdateEvent($sendEvent);
  451. $this->eventDispatcher->dispatch($event, $event::NAME);
  452. $sendEventResult = $event->getSendEvent();
  453. $this->em->persist($sendEventResult);
  454. $this->em->flush();
  455. $this->addFlash('success', 'You unsubscribed successfully');
  456. return $this->render('unsubscribe-success.html.twig', [
  457. 'user' => $user,
  458. 'page' => $this->page,
  459. ]);
  460. }
  461. return $this->render('unsubscribe.html.twig', [
  462. 'page' => $this->page,
  463. 'user' => $user,
  464. 'unsubscribe_form' => $unsubForm->createView()
  465. ]);
  466. }
  467. public function getContactsJson(Request $request, SSP $ssp, int $id) : JsonResponse
  468. {
  469. $output = $this->getOutput($request, $ssp, [
  470. ['campaign', 'eq', $id]
  471. //['status', 'neq', 'deleted']
  472. ]);
  473. return new JsonResponse($output);
  474. }
  475. private function getOutput(Request $request, SSP $ssp, array $fixed_filters = []) : array
  476. {
  477. $columns = array();
  478. foreach ($request->query->get('columns') as $dtColumn){
  479. if($dtColumn['data']=='' || is_numeric($dtColumn['data'])){
  480. $column = [
  481. 'dt' => null,
  482. 'db' => null,
  483. 'searchable' => false
  484. ];
  485. } else {
  486. $column = [
  487. 'dt' => $dtColumn['data'],
  488. 'db' => $dtColumn['name'],
  489. 'searchable' => ($dtColumn['searchable'] === 'true') ? true : false,
  490. 'search' => $dtColumn['search']
  491. ];
  492. if($dtColumn['name'] == 'createdAt'){
  493. $column['formatter'] = function ($d, $row){
  494. return $d->format('d/m/Y');
  495. };
  496. }
  497. if($dtColumn['name'] == 'send_events'){
  498. $column['formatter'] = function ($d, $row){
  499. $sendEvents = array();
  500. foreach (json_decode($d) as $eventId){
  501. $sendEvent = $this->em->find(SendEvent::class, $eventId);
  502. $sendEvents[] = [
  503. 'id' => $sendEvent->getId(),
  504. 'event' => $sendEvent->getEvent(),
  505. 'created_at' => $sendEvent->getCreatedAt()->format('d/m/Y H:i'),
  506. 'message' => $sendEvent->getMessage()
  507. ];
  508. }
  509. usort($sendEvents, function ($a, $b){
  510. $aDate = \DateTime::createFromFormat('d/m/Y H:i', $a['created_at']);
  511. $bDate = \DateTime::createFromFormat('d/m/Y H:i', $b['created_at']);
  512. return ($aDate > $bDate) ? -1 : 1;
  513. });
  514. return $sendEvents;
  515. };
  516. }
  517. }
  518. $columns[] = $column;
  519. }
  520. $orders = [
  521. //['updated_at', 'desc']
  522. ];
  523. return $ssp->utsimple($this->getUser(), $request->query, $columns, CampaignContact::class, $orders, $fixed_filters);
  524. }
  525. public function webView(Request $request, string $token) : void
  526. {
  527. try {
  528. $campaignContact = $this->parseCampaignContactToken($token);
  529. } catch (\Exception $e) {
  530. throw $this->createNotFoundException('Resource not found');
  531. }
  532. $campaign = $campaignContact->getCampaign();
  533. $campaignData = $campaign->getCampaignData();
  534. $substitutions = [];
  535. $contact = $campaignContact->getContact();
  536. // unsubscribe_url merge tag, defaults to # if not a campaign email (e.g. sending tests)
  537. $unsubUrl = '#';
  538. // Set recipient specific substitutions
  539. if(strlen($contact->getFirstName()) != 0){
  540. $substitutions['{{first_name}}'] = $contact->getFirstName();
  541. $substitutions['{{contact_first_name}}'] = $contact->getFirstName();
  542. }
  543. if(strlen($contact->getLastName()) != 0){
  544. $substitutions['{{last_name}}'] = $contact->getLastName();
  545. $substitutions['{{contact_last_name}}'] = $contact->getLastName();
  546. }
  547. if(strlen($contact->getCompanyName()) != 0){
  548. $substitutions['{{contact_company_name}}'] = $contact->getCompanyName();
  549. }
  550. $substitutions['{{company_name}}'] = $campaign->getUser()->getCompany();
  551. $substitutions['{{unsubscribe_url}}'] = $unsubUrl;
  552. // unsubscribe_url merge tag for campaign contact
  553. $unsubToken = $campaignContact->getId().'.'.hash_hmac('sha256', $contact->getId(), $this->getParameter('app.secret'));
  554. $unsubUrl = $this->router->generate('utmailer_unsubscribe', ['token' => $unsubToken], UrlGeneratorInterface::ABSOLUTE_URL);
  555. $substitutions['{{unsubscribe_url}}'] = $unsubUrl;
  556. // Add links substitutions
  557. foreach ($campaign->getCampaignLinksAll() as $link){
  558. if ($link->getUrl()){
  559. $linkToken = $link->getId() . '.' . $campaignContact->getId() . '.'.hash_hmac('sha256', $link->getUrl().$campaignContact->getId(), $this->getParameter('app.secret'));
  560. $linkUrl = $this->router->generate('utmailer_campaign_link', ['token' => $linkToken], UrlGeneratorInterface::ABSOLUTE_URL);
  561. } else {
  562. $linkUrl = '';
  563. }
  564. $substitutions['{{EMAIL_LINK_'.$link->getLinkId().'}}'] = $linkUrl;
  565. }
  566. echo str_replace(array_keys($substitutions), array_values($substitutions), $campaignData['html']);
  567. exit;
  568. }
  569. private function parseCampaignContactToken(string $token) : CampaignContact
  570. {
  571. $tok = explode('.', $token);
  572. // check token parts
  573. if (count($tok) !== 2) {
  574. // not valid token
  575. throw $this->createNotFoundException('Resource not found');
  576. }
  577. // check id segment
  578. if (!isset($tok[0]) || !filter_var($tok[0], FILTER_VALIDATE_INT)) {
  579. // not valid id
  580. throw $this->createNotFoundException('Resource not found');
  581. }
  582. $campaignContactId = $tok[0];
  583. /** @var CampaignContact $campaignContact */
  584. if(!$campaignContact = $this->em->getRepository(CampaignContact::class)->find($campaignContactId)) {
  585. throw $this->createNotFoundException('Resource not found');
  586. }
  587. if ($tok[1] !== hash_hmac('sha256', $campaignContact->getContact()->getId(), $this->getParameter('app.secret'))) {
  588. // failed verification
  589. throw $this->createNotFoundException('Resource not found');
  590. }
  591. return $campaignContact;
  592. }
  593. }