Issues (3627)

bundles/PageBundle/Controller/PublicController.php (2 issues)

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
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);
0 ignored issues
show
Deprecated Code introduced by
The function Mautic\PageBundle\Contro...troller::processSlots() has been deprecated: - to be removed in 3.0 ( Ignorable by Annotation )

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

246
                /** @scrutinizer ignore-deprecated */ $this->processSlots($slots, $entity);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
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);
0 ignored issues
show
Deprecated Code introduced by
The function Mautic\PageBundle\Contro...troller::processSlots() has been deprecated: - to be removed in 3.0 ( Ignorable by Annotation )

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

324
            /** @scrutinizer ignore-deprecated */ $this->processSlots($slots, $entity);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
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