Issues (3627)

bundles/PageBundle/Controller/PublicController.php (1 issue)

1
<?php
2
3
/*
4
 * @copyright   2014 Mautic Contributors. All rights reserved
5
 * @author      Mautic
6
 *
7
 * @link        http://mautic.org
8
 *
9
 * @license     GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html
10
 */
11
12
namespace Mautic\PageBundle\Controller;
13
14
use Mautic\CoreBundle\Controller\FormController as CommonFormController;
15
use Mautic\CoreBundle\Exception\InvalidDecodedStringException;
16
use Mautic\CoreBundle\Helper\TrackingPixelHelper;
17
use Mautic\CoreBundle\Helper\UrlHelper;
18
use Mautic\LeadBundle\Helper\PrimaryCompanyHelper;
19
use Mautic\LeadBundle\Helper\TokenHelper;
20
use Mautic\LeadBundle\Model\LeadModel;
21
use Mautic\LeadBundle\Tracker\ContactTracker;
22
use Mautic\LeadBundle\Tracker\Service\DeviceTrackingService\DeviceTrackingServiceInterface;
23
use Mautic\PageBundle\Entity\Page;
24
use Mautic\PageBundle\Event\PageDisplayEvent;
25
use Mautic\PageBundle\Helper\TrackingHelper;
26
use Mautic\PageBundle\Model\PageModel;
27
use Mautic\PageBundle\Model\VideoModel;
28
use Mautic\PageBundle\PageEvents;
29
use Symfony\Component\HttpFoundation\JsonResponse;
30
use Symfony\Component\HttpFoundation\Request;
31
use Symfony\Component\HttpFoundation\Response;
32
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
33
34
class PublicController extends CommonFormController
0 ignored issues
show
Deprecated Code introduced by
The class Mautic\CoreBundle\Controller\FormController has been deprecated: 2.3 - to be removed in 3.0; use AbstractFormController instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

34
class PublicController extends /** @scrutinizer ignore-deprecated */ CommonFormController
Loading history...
35
{
36
    /**
37
     * @param $slug
38
     *
39
     * @return Response
40
     *
41
     * @throws \Exception
42
     * @throws \Mautic\CoreBundle\Exception\FileNotFoundException
43
     */
44
    public function indexAction($slug, Request $request)
45
    {
46
        /** @var \Mautic\PageBundle\Model\PageModel $model */
47
        $model    = $this->getModel('page');
48
        $security = $this->get('mautic.security');
49
        /** @var Page $entity */
50
        $entity = $model->getEntityBySlugs($slug);
51
52
        // Do not hit preference center pages
53
        if (!empty($entity) && !$entity->getIsPreferenceCenter()) {
54
            $userAccess = $security->hasEntityAccess('page:pages:viewown', 'page:pages:viewother', $entity->getCreatedBy());
55
            $published  = $entity->isPublished();
56
57
            // Make sure the page is published or deny access if not
58
            if (!$published && !$userAccess) {
59
                // If the page has a redirect type, handle it
60
                if (null != $entity->getRedirectType()) {
61
                    $model->hitPage($entity, $this->request, $entity->getRedirectType());
62
63
                    return $this->redirect($entity->getRedirectUrl(), $entity->getRedirectType());
64
                } else {
65
                    $model->hitPage($entity, $this->request, 401);
66
67
                    return $this->accessDenied();
68
                }
69
            }
70
71
            $lead  = null;
72
            $query = null;
73
            if (!$userAccess) {
74
                /** @var LeadModel $leadModel */
75
                $leadModel = $this->getModel('lead');
76
                // Extract the lead from the request so it can be used to determine language if applicable
77
                $query = $model->getHitQuery($this->request, $entity);
78
                $lead  = $leadModel->getContactFromRequest($query);
79
            }
80
81
            // Correct the URL if it doesn't match up
82
            if (!$request->attributes->get('ignore_mismatch', 0)) {
83
                // Make sure URLs match up
84
                $url        = $model->generateUrl($entity, false);
85
                $requestUri = $this->request->getRequestUri();
86
87
                // Remove query when comparing
88
                $query = $this->request->getQueryString();
89
                if (!empty($query)) {
90
                    $requestUri = str_replace("?{$query}", '', $url);
91
                }
92
93
                // Redirect if they don't match
94
                if ($requestUri != $url) {
95
                    $model->hitPage($entity, $this->request, 301, $lead, $query);
96
97
                    return $this->redirect($url, 301);
98
                }
99
            }
100
101
            // Check for variants
102
            list($parentVariant, $childrenVariants) = $entity->getVariants();
103
104
            // Is this a variant of another? If so, the parent URL should be used unless a user is logged in and previewing
105
            if ($parentVariant != $entity && !$userAccess) {
106
                $model->hitPage($entity, $this->request, 301, $lead, $query);
107
                $url = $model->generateUrl($parentVariant, false);
108
109
                return $this->redirect($url, 301);
110
            }
111
112
            // First determine the A/B test to display if applicable
113
            if (!$userAccess) {
114
                // Check to see if a variant should be shown versus the parent but ignore if a user is previewing
115
                if (count($childrenVariants)) {
116
                    $variants      = [];
117
                    $variantWeight = 0;
118
                    $totalHits     = $entity->getVariantHits();
119
120
                    foreach ($childrenVariants as $id => $child) {
121
                        if ($child->isPublished()) {
122
                            $variantSettings = $child->getVariantSettings();
123
                            $variants[$id]   = [
124
                                'weight' => ($variantSettings['weight'] / 100),
125
                                'hits'   => $child->getVariantHits(),
126
                            ];
127
                            $variantWeight += $variantSettings['weight'];
128
129
                            // Count translations for this variant as well
130
                            $translations = $child->getTranslations(true);
131
                            /** @var Page $translation */
132
                            foreach ($translations as $translation) {
133
                                if ($translation->isPublished()) {
134
                                    $variants[$id]['hits'] += (int) $translation->getVariantHits();
135
                                }
136
                            }
137
138
                            $totalHits += $variants[$id]['hits'];
139
                        }
140
                    }
141
142
                    if (count($variants)) {
143
                        //check to see if this user has already been displayed a specific variant
144
                        $variantCookie = $this->request->cookies->get('mautic_page_'.$entity->getId());
145
146
                        if (!empty($variantCookie)) {
147
                            if (isset($variants[$variantCookie])) {
148
                                //if not the parent, show the specific variant already displayed to the visitor
149
                                if ($variantCookie !== $entity->getId()) {
150
                                    $entity = $childrenVariants[$variantCookie];
151
                                } //otherwise proceed with displaying parent
152
                            }
153
                        } else {
154
                            // Add parent weight
155
                            $variants[$entity->getId()] = [
156
                                'weight' => ((100 - $variantWeight) / 100),
157
                                'hits'   => $entity->getVariantHits(),
158
                            ];
159
160
                            // Count translations for the parent as well
161
                            $translations = $entity->getTranslations(true);
162
                            /** @var Page $translation */
163
                            foreach ($translations as $translation) {
164
                                if ($translation->isPublished()) {
165
                                    $variants[$entity->getId()]['hits'] += (int) $translation->getVariantHits();
166
                                }
167
                            }
168
                            $totalHits += $variants[$id]['hits'];
169
170
                            //determine variant to show
171
                            foreach ($variants as &$variant) {
172
                                $variant['weight_deficit'] = ($totalHits) ? $variant['weight'] - ($variant['hits'] / $totalHits) : $variant['weight'];
173
                            }
174
175
                            // Reorder according to send_weight so that campaigns which currently send one at a time alternate
176
                            uasort(
177
                                $variants,
178
                                function ($a, $b) {
179
                                    if ($a['weight_deficit'] === $b['weight_deficit']) {
180
                                        if ($a['hits'] === $b['hits']) {
181
                                            return 0;
182
                                        }
183
184
                                        // if weight is the same - sort by least number displayed
185
                                        return ($a['hits'] < $b['hits']) ? -1 : 1;
186
                                    }
187
188
                                    // sort by the one with the greatest deficit first
189
                                    return ($a['weight_deficit'] > $b['weight_deficit']) ? -1 : 1;
190
                                }
191
                            );
192
193
                            //find the one with the most difference from weight
194
195
                            reset($variants);
196
                            $useId = key($variants);
197
198
                            //set the cookie - 14 days
199
                            $this->get('mautic.helper.cookie')->setCookie(
200
                                'mautic_page_'.$entity->getId(),
201
                                $useId,
202
                                3600 * 24 * 14
203
                            );
204
205
                            if ($useId != $entity->getId()) {
206
                                $entity = $childrenVariants[$useId];
207
                            }
208
                        }
209
                    }
210
                }
211
212
                // Now show the translation for the page or a/b test - only fetch a translation if a slug was not used
213
                if ($entity->isTranslation() && empty($entity->languageSlug)) {
214
                    list($translationParent, $translatedEntity) = $model->getTranslatedEntity(
215
                        $entity,
216
                        $lead,
217
                        $this->request
218
                    );
219
220
                    if ($translationParent && $translatedEntity !== $entity) {
221
                        if (!$this->request->get('ntrd', 0)) {
222
                            $url = $model->generateUrl($translatedEntity, false);
223
                            $model->hitPage($entity, $this->request, 302, $lead, $query);
224
225
                            return $this->redirect($url, 302);
226
                        }
227
                    }
228
                }
229
            }
230
231
            // Generate contents
232
            $analytics = $this->get('mautic.helper.template.analytics')->getCode();
233
234
            $BCcontent = $entity->getContent();
235
            $content   = $entity->getCustomHtml();
236
            // This condition remains so the Mautic v1 themes would display the content
237
            if (empty($content) && !empty($BCcontent)) {
238
                /**
239
                 * @deprecated  BC support to be removed in 3.0
240
                 */
241
                $template = $entity->getTemplate();
242
                //all the checks pass so display the content
243
                $slots   = $this->factory->getTheme($template)->getSlots('page');
244
                $content = $entity->getContent();
245
246
                $this->processSlots($slots, $entity);
247
248
                // Add the GA code to the template assets
249
                if (!empty($analytics)) {
250
                    $this->factory->getHelper('template.assets')->addCustomDeclaration($analytics);
251
                }
252
253
                $logicalName = $this->factory->getHelper('theme')->checkForTwigTemplate(':'.$template.':page.html.php');
254
255
                $response = $this->render(
256
                    $logicalName,
257
                    [
258
                        'slots'    => $slots,
259
                        'content'  => $content,
260
                        'page'     => $entity,
261
                        'template' => $template,
262
                        'public'   => true,
263
                    ]
264
                );
265
266
                $content = $response->getContent();
267
            } else {
268
                if (!empty($analytics)) {
269
                    $content = str_replace('</head>', $analytics."\n</head>", $content);
270
                }
271
                if ($entity->getNoIndex()) {
272
                    $content = str_replace('</head>', "<meta name=\"robots\" content=\"noindex\">\n</head>", $content);
273
                }
274
            }
275
276
            $this->get('templating.helper.assets')->addScript(
277
                $this->get('router')->generate('mautic_js', [], UrlGeneratorInterface::ABSOLUTE_URL),
278
                'onPageDisplay_headClose',
279
                true,
280
                'mautic_js'
281
            );
282
283
            $event = new PageDisplayEvent($content, $entity);
284
            $this->get('event_dispatcher')->dispatch(PageEvents::PAGE_ON_DISPLAY, $event);
285
            $content = $event->getContent();
286
287
            $model->hitPage($entity, $this->request, 200, $lead, $query);
288
289
            return new Response($content);
290
        }
291
292
        $model->hitPage($entity, $this->request, 404);
293
294
        return $this->notFound();
295
    }
296
297
    /**
298
     * @param $id
299
     *
300
     * @return Response|\Symfony\Component\HttpKernel\Exception\NotFoundHttpException
301
     *
302
     * @throws \Exception
303
     * @throws \Mautic\CoreBundle\Exception\FileNotFoundException
304
     */
305
    public function previewAction($id)
306
    {
307
        $model  = $this->getModel('page');
308
        $entity = $model->getEntity($id);
309
310
        if (null === $entity) {
311
            return $this->notFound();
312
        }
313
314
        $analytics = $this->factory->getHelper('template.analytics')->getCode();
315
316
        $BCcontent = $entity->getContent();
317
        $content   = $entity->getCustomHtml();
318
        if (empty($content) && !empty($BCcontent)) {
319
            $template = $entity->getTemplate();
320
            //all the checks pass so display the content
321
            $slots   = $this->factory->getTheme($template)->getSlots('page');
322
            $content = $entity->getContent();
323
324
            $this->processSlots($slots, $entity);
325
326
            // Add the GA code to the template assets
327
            if (!empty($analytics)) {
328
                $this->factory->getHelper('template.assets')->addCustomDeclaration($analytics);
329
            }
330
331
            $logicalName = $this->factory->getHelper('theme')->checkForTwigTemplate(':'.$template.':page.html.php');
332
333
            $response = $this->render(
334
                $logicalName,
335
                [
336
                    'slots'    => $slots,
337
                    'content'  => $content,
338
                    'page'     => $entity,
339
                    'template' => $template,
340
                    'public'   => true, // @deprecated Remove in 2.0
341
                ]
342
            );
343
344
            $content = $response->getContent();
345
        } else {
346
            if (!empty($analytics)) {
347
                $content = str_replace('</head>', $analytics."\n</head>", $content);
348
            }
349
        }
350
351
        $dispatcher = $this->get('event_dispatcher');
352
        if ($dispatcher->hasListeners(PageEvents::PAGE_ON_DISPLAY)) {
353
            $event = new PageDisplayEvent($content, $entity);
354
            $dispatcher->dispatch(PageEvents::PAGE_ON_DISPLAY, $event);
355
            $content = $event->getContent();
356
        }
357
358
        return new Response($content);
359
    }
360
361
    /**
362
     * @return Response
363
     *
364
     * @throws \Exception
365
     */
366
    public function trackingImageAction()
367
    {
368
        /** @var \Mautic\PageBundle\Model\PageModel $model */
369
        $model = $this->getModel('page');
370
        $model->hitPage(null, $this->request);
371
372
        return TrackingPixelHelper::getResponse($this->request);
373
    }
374
375
    /**
376
     * @return JsonResponse
377
     *
378
     * @throws \Exception
379
     */
380
    public function trackingAction()
381
    {
382
        if (!$this->get('mautic.security')->isAnonymous()) {
383
            return new JsonResponse(
384
                [
385
                    'success' => 0,
386
                ]
387
            );
388
        }
389
390
        /** @var \Mautic\PageBundle\Model\PageModel $model */
391
        $model = $this->getModel('page');
392
        $model->hitPage(null, $this->request);
393
394
        /** @var ContactTracker $contactTracker */
395
        $contactTracker = $this->get(ContactTracker::class);
396
397
        $lead = $contactTracker->getContact();
398
        /** @var DeviceTrackingServiceInterface $trackedDevice */
399
        $trackedDevice = $this->get('mautic.lead.service.device_tracking_service')->getTrackedDevice();
400
        $trackingId    = (null === $trackedDevice ? null : $trackedDevice->getTrackingId());
401
402
        /** @var TrackingHelper $trackingHelper */
403
        $trackingHelper = $this->get('mautic.page.helper.tracking');
404
        $sessionValue   = $trackingHelper->getSession(true);
405
406
        return new JsonResponse(
407
            [
408
                'success'   => 1,
409
                'id'        => ($lead) ? $lead->getId() : null,
410
                'sid'       => $trackingId,
411
                'device_id' => $trackingId,
412
                'events'    => $sessionValue,
413
            ]
414
        );
415
    }
416
417
    /**
418
     * @param $redirectId
419
     *
420
     * @return \Symfony\Component\HttpFoundation\RedirectResponse
421
     *
422
     * @throws \Exception
423
     */
424
    public function redirectAction($redirectId)
425
    {
426
        $logger = $this->container->get('monolog.logger.mautic');
427
428
        $logger->debug('Attempting to load redirect with tracking_id of: '.$redirectId);
429
430
        /** @var \Mautic\PageBundle\Model\RedirectModel $redirectModel */
431
        $redirectModel = $this->getModel('page.redirect');
432
        $redirect      = $redirectModel->getRedirectById($redirectId);
433
434
        $logger->debug('Executing Redirect: '.$redirect);
435
436
        if (null === $redirect || !$redirect->isPublished(false)) {
437
            $logger->debug('Redirect with tracking_id of '.$redirectId.' not found');
438
439
            $url = ($redirect) ? $redirect->getUrl() : 'n/a';
440
441
            throw $this->createNotFoundException($this->translator->trans('mautic.core.url.error.404', ['%url%' => $url]));
442
        }
443
444
        // Ensure the URL does not have encoded ampersands
445
        $url = str_replace('&amp;', '&', $redirect->getUrl());
446
447
        // Get query string
448
        $query = $this->request->query->all();
449
450
        // Unset the clickthrough from the URL query
451
        $ct = $query['ct'];
452
        unset($query['ct']);
453
454
        // Tak on anything left to the URL
455
        if (count($query)) {
456
            $url = UrlHelper::appendQueryToUrl($url, http_build_query($query));
457
        }
458
459
        // If the IP address is not trackable, it means it came form a configured "do not track" IP or a "do not track" user agent
460
        // This prevents simulated clicks from 3rd party services such as URL shorteners from simulating clicks
461
        $ipAddress = $this->container->get('mautic.helper.ip_lookup')->getIpAddress();
462
        if ($ipAddress->isTrackable()) {
463
            // Search replace lead fields in the URL
464
            /** @var \Mautic\LeadBundle\Model\LeadModel $leadModel */
465
            $leadModel = $this->getModel('lead');
466
467
            /** @var PageModel $pageModel */
468
            $pageModel = $this->getModel('page');
469
470
            try {
471
                $lead = $leadModel->getContactFromRequest(['ct' => $ct]);
472
                $pageModel->hitPage($redirect, $this->request, 200, $lead);
473
            } catch (InvalidDecodedStringException $e) {
474
                // Invalid ct value so we must unset it
475
                // and process the request without it
476
477
                $logger->error(sprintf('Invalid clickthrough value: %s', $ct), ['exception' => $e]);
478
479
                $this->request->request->set('ct', '');
480
                $this->request->query->set('ct', '');
481
                $lead = $leadModel->getContactFromRequest();
482
                $pageModel->hitPage($redirect, $this->request, 200, $lead);
483
            }
484
485
            /** @var PrimaryCompanyHelper $primaryCompanyHelper */
486
            $primaryCompanyHelper = $this->get('mautic.lead.helper.primary_company');
487
            $leadArray            = ($lead) ? $primaryCompanyHelper->getProfileFieldsWithPrimaryCompany($lead) : [];
488
489
            $url = TokenHelper::findLeadTokens($url, $leadArray, true);
490
        }
491
492
        $url = UrlHelper::sanitizeAbsoluteUrl($url);
493
494
        if (!UrlHelper::isValidUrl($url)) {
495
            throw $this->createNotFoundException($this->translator->trans('mautic.core.url.error.404', ['%url%' => $url]));
496
        }
497
498
        return $this->redirect($url);
499
    }
500
501
    /**
502
     * PreProcess page slots for public view.
503
     *
504
     * @deprecated - to be removed in 3.0
505
     *
506
     * @param array $slots
507
     * @param Page  $entity
508
     */
509
    private function processSlots($slots, $entity)
510
    {
511
        /** @var \Mautic\CoreBundle\Templating\Helper\AssetsHelper $assetsHelper */
512
        $assetsHelper = $this->factory->getHelper('template.assets');
513
        /** @var \Mautic\CoreBundle\Templating\Helper\SlotsHelper $slotsHelper */
514
        $slotsHelper = $this->factory->getHelper('template.slots');
515
516
        $content = $entity->getContent();
517
518
        foreach ($slots as $slot => $slotConfig) {
519
            // backward compatibility - if slotConfig array does not exist
520
            if (is_numeric($slot)) {
521
                $slot       = $slotConfig;
522
                $slotConfig = [];
523
            }
524
525
            if (isset($slotConfig['type']) && 'slideshow' == $slotConfig['type']) {
526
                if (isset($content[$slot])) {
527
                    $options = json_decode($content[$slot], true);
528
                } else {
529
                    $options = [
530
                        'width'            => '100%',
531
                        'height'           => '250px',
532
                        'background_color' => 'transparent',
533
                        'arrow_navigation' => false,
534
                        'dot_navigation'   => true,
535
                        'interval'         => 5000,
536
                        'pause'            => 'hover',
537
                        'wrap'             => true,
538
                        'keyboard'         => true,
539
                    ];
540
                }
541
542
                // Create sample slides for first time or if all slides were deleted
543
                if (empty($options['slides'])) {
544
                    $options['slides'] = [
545
                        [
546
                            'order'            => 0,
547
                            'background-image' => $assetsHelper->getUrl('media/images/mautic_logo_lb200.png'),
548
                            'captionheader'    => 'Caption 1',
549
                        ],
550
                        [
551
                            'order'            => 1,
552
                            'background-image' => $assetsHelper->getUrl('media/images/mautic_logo_db200.png'),
553
                            'captionheader'    => 'Caption 2',
554
                        ],
555
                    ];
556
                }
557
558
                // Order slides
559
                usort(
560
                    $options['slides'],
561
                    function ($a, $b) {
562
                        return strcmp($a['order'], $b['order']);
563
                    }
564
                );
565
566
                $options['slot']   = $slot;
567
                $options['public'] = true;
568
569
                $renderingEngine = $this->container->get('templating')->getEngine('MauticPageBundle:Page:Slots/slideshow.html.php');
570
                $slotsHelper->set($slot, $renderingEngine->render('MauticPageBundle:Page:Slots/slideshow.html.php', $options));
571
            } elseif (isset($slotConfig['type']) && 'textarea' == $slotConfig['type']) {
572
                $value = isset($content[$slot]) ? nl2br($content[$slot]) : '';
573
                $slotsHelper->set($slot, $value);
574
            } else {
575
                // Fallback for other types like html, text, textarea and all unknown
576
                $value = isset($content[$slot]) ? $content[$slot] : '';
577
                $slotsHelper->set($slot, $value);
578
            }
579
        }
580
581
        $parentVariant = $entity->getVariantParent();
582
        $title         = (!empty($parentVariant)) ? $parentVariant->getTitle() : $entity->getTitle();
583
        $slotsHelper->set('pageTitle', $title);
584
    }
585
586
    /**
587
     * Track video views.
588
     */
589
    public function hitVideoAction()
590
    {
591
        // Only track XMLHttpRequests, because the hit should only come from there
592
        if ($this->request->isXmlHttpRequest()) {
593
            /** @var VideoModel $model */
594
            $model = $this->getModel('page.video');
595
596
            try {
597
                $model->hitVideo($this->request);
598
            } catch (\Exception $e) {
599
                return new JsonResponse(['success' => false]);
600
            }
601
602
            return new JsonResponse(['success' => true]);
603
        }
604
605
        return new Response();
606
    }
607
608
    /**
609
     * Get the ID of the currently tracked Contact.
610
     *
611
     * @return JsonResponse
612
     */
613
    public function getContactIdAction()
614
    {
615
        $data = [];
616
        if ($this->get('mautic.security')->isAnonymous()) {
617
            /** @var ContactTracker $contactTracker */
618
            $contactTracker = $this->get(ContactTracker::class);
619
620
            $lead = $contactTracker->getContact();
621
            /** @var DeviceTrackingServiceInterface $trackedDevice */
622
            $trackedDevice = $this->get('mautic.lead.service.device_tracking_service')->getTrackedDevice();
623
            $trackingId    = (null === $trackedDevice ? null : $trackedDevice->getTrackingId());
624
            $data          = [
625
                'id'        => ($lead) ? $lead->getId() : null,
626
                'sid'       => $trackingId,
627
                'device_id' => $trackingId,
628
            ];
629
        }
630
631
        return new JsonResponse($data);
632
    }
633
}
634