getModel('page'); $security = $this->security; /** @var Page|bool $entity */ $entity = $model->getEntityBySlugs($slug); // Do not hit preference center pages if (!empty($entity) && !$entity->getIsPreferenceCenter()) { $userAccess = $security->hasEntityAccess('page:pages:viewown', 'page:pages:viewother', $entity->getCreatedBy()); $published = $entity->isPublished(); // Make sure the page is published or deny access if not if (!$published && !$userAccess) { // If the page has a redirect type, handle it if (null != $entity->getRedirectType()) { $model->hitPage($entity, $request, $entity->getRedirectType()); if ($entity->getRedirectUrl()) { return $this->redirect($entity->getRedirectUrl(), (int) $entity->getRedirectType()); } else { return $this->notFound(); } } else { $model->hitPage($entity, $request, 401); return $this->accessDenied(); } } $lead = null; $query = null; if (!$userAccess) { // Extract the lead from the request so it can be used to determine language if applicable $query = $model->getHitQuery($request, $entity); $lead = $contactRequestHelper->getContactFromQuery($query); } // Correct the URL if it doesn't match up if (!$request->attributes->get('ignore_mismatch', 0)) { // Make sure URLs match up $url = $model->generateUrl($entity, false); $requestUri = $request->getRequestUri(); // Remove query when comparing $query = $request->getQueryString(); if (!empty($query)) { $requestUri = str_replace("?{$query}", '', $url); } // Redirect if they don't match if ($requestUri != $url) { $model->hitPage($entity, $request, 301, $lead, $query); return $this->redirect($url, 301); } } // Check for variants [$parentVariant, $childrenVariants] = $entity->getVariants(); // Is this a variant of another? If so, the parent URL should be used unless a user is logged in and previewing if ($parentVariant != $entity && !$userAccess) { $model->hitPage($entity, $request, 301, $lead, $query); $url = $model->generateUrl($parentVariant, false); return $this->redirect($url, 301); } // First determine the A/B test to display if applicable if (!$userAccess) { // Check to see if a variant should be shown versus the parent but ignore if a user is previewing if (count($childrenVariants)) { $variants = []; $variantWeight = 0; $totalHits = $entity->getVariantHits(); foreach ($childrenVariants as $id => $child) { if ($child->isPublished()) { $variantSettings = $child->getVariantSettings(); $variants[$id] = [ 'weight' => ($variantSettings['weight'] / 100), 'hits' => $child->getVariantHits(), ]; $variantWeight += $variantSettings['weight']; // Count translations for this variant as well $translations = $child->getTranslations(true); /** @var Page $translation */ foreach ($translations as $translation) { if ($translation->isPublished()) { $variants[$id]['hits'] += (int) $translation->getVariantHits(); } } $totalHits += $variants[$id]['hits']; } } if (count($variants)) { // check to see if this user has already been displayed a specific variant $variantCookie = $request->cookies->get('mautic_page_'.$entity->getId()); if (!empty($variantCookie)) { if (isset($variants[$variantCookie])) { // if not the parent, show the specific variant already displayed to the visitor if ((string) $variantCookie !== (string) $entity->getId()) { $entity = $childrenVariants[$variantCookie]; } // otherwise proceed with displaying parent } } else { // Add parent weight $variants[$entity->getId()] = [ 'weight' => ((100 - $variantWeight) / 100), 'hits' => $entity->getVariantHits(), ]; // Count translations for the parent as well $translations = $entity->getTranslations(true); /** @var Page $translation */ foreach ($translations as $translation) { if ($translation->isPublished()) { $variants[$entity->getId()]['hits'] += (int) $translation->getVariantHits(); } } $totalHits += $variants[$id]['hits']; // determine variant to show foreach ($variants as &$variant) { $variant['weight_deficit'] = ($totalHits) ? $variant['weight'] - ($variant['hits'] / $totalHits) : $variant['weight']; } // Reorder according to send_weight so that campaigns which currently send one at a time alternate uasort( $variants, function ($a, $b): int { if ($a['weight_deficit'] === $b['weight_deficit']) { if ($a['hits'] === $b['hits']) { return 0; } // if weight is the same - sort by least number displayed return ($a['hits'] < $b['hits']) ? -1 : 1; } // sort by the one with the greatest deficit first return ($a['weight_deficit'] > $b['weight_deficit']) ? -1 : 1; } ); // find the one with the most difference from weight $useId = array_key_first($variants); // set the cookie - 14 days $cookieHelper->setCookie( 'mautic_page_'.$entity->getId(), $useId, 3600 * 24 * 14 ); if ($useId != $entity->getId()) { $entity = $childrenVariants[$useId]; } } } } // Now show the translation for the page or a/b test - only fetch a translation if a slug was not used if ($entity->isTranslation() && empty($entity->languageSlug)) { [$translationParent, $translatedEntity] = $model->getTranslatedEntity( $entity, $lead, $request ); \assert($translatedEntity instanceof Page); if ($translationParent && $translatedEntity !== $entity) { if (!$request->get('ntrd', 0)) { $url = $model->generateUrl($translatedEntity, false); $model->hitPage($entity, $request, 302, $lead, $query); return $this->redirect($url, 302); } } } } // Generate contents $analytics = $analyticsHelper->getCode(); $BCcontent = $entity->getContent(); $content = $entity->getCustomHtml(); // This condition remains so the Mautic v1 themes would display the content if (empty($content) && !empty($BCcontent)) { /** * @deprecated BC support to be removed in 3.0 */ $template = $entity->getTemplate(); // all the checks pass so display the content $content = $entity->getContent(); // Add the GA code to the template assets if (!empty($analytics)) { $assetsHelper->addCustomDeclaration($analytics); } $logicalName = $themeHelper->checkForTwigTemplate('@themes/'.$template.'/html/page.html.twig'); $response = $this->render( $logicalName, [ 'content' => $content, 'page' => $entity, 'template' => $template, 'public' => true, ] ); $content = $response->getContent(); } else { if (!empty($analytics)) { $content = str_replace('', $analytics."\n", $content); } if ($entity->getNoIndex()) { $content = str_replace('', "\n", $content); } } $assetsHelper->addScript( $router->generate('mautic_js', [], UrlGeneratorInterface::ABSOLUTE_URL), 'onPageDisplay_headClose', true, 'mautic_js' ); $event = new PageDisplayEvent((string) $content, $entity); $this->dispatcher->dispatch($event, PageEvents::PAGE_ON_DISPLAY); $content = $event->getContent(); $model->hitPage($entity, $request, Response::HTTP_OK, $lead, $query); $response = new Response($content); if ($request->cookies->has('Blocked-Tracking')) { $deviceTrackingService->clearTrackingCookies(); } return $response; } if (false !== $entity && $tracking404Model->isTrackable()) { $tracking404Model->hitPage($entity, $request); } return $this->notFound(); } /** * @return mixed[]|JsonResponse|RedirectResponse|Response * * @throws FileNotFoundException */ public function previewAction(Request $request, PageConfig $pageConfig, CorePermissions $security, AnalyticsHelper $analyticsHelper, AssetsHelper $assetsHelper, ThemeHelper $themeHelper, int $id, ?string $objectType = null) { /** @var PageModel $model */ $model = $this->getModel('page'); /** @var Page $page */ $page = $model->getEntity($id); if (!$page || !$page->getId()) { return $this->notFound(); } $contactId = (int) $request->query->get('contactId'); if ($contactId) { /** @var LeadModel $leadModel */ $leadModel = $this->getModel('lead.lead'); $contact = $leadModel->getEntity($contactId); } $draftEnabled = $pageConfig->isDraftEnabled(); $analytics = $analyticsHelper->getCode(); $BCcontent = $page->getContent(); $content = $page->getCustomHtml(); $publicPreview = $page->isPublicPreview(); if ('draft' === $objectType && $draftEnabled && $page->hasDraft()) { $content = $page->getDraftContent(); $publicPreview = $page->getDraft()->isPublicPreview(); } if (($security->isAnonymous() && (!$page->isPublished() || !$publicPreview)) || (!$security->isAnonymous( ) && !$security->hasEntityAccess( 'page:pages:viewown', 'page:pages:viewother', $page->getCreatedBy() ))) { return $this->accessDenied(); } if ($contactId && (!$security->isAdmin() || !$security->hasEntityAccess( 'lead:leads:viewown', 'lead:leads:viewother' ))) { // disallow displaying contact information return $this->accessDenied(); } if (empty($content) && !empty($BCcontent)) { $template = $page->getTemplate(); // all the checks pass so display the content $content = $page->getContent(); // Add the GA code to the template assets if (!empty($analytics)) { $assetsHelper->addCustomDeclaration($analytics); } $logicalName = $themeHelper->checkForTwigTemplate('@themes/'.$template.'/html/page.html.twig'); $response = $this->render( $logicalName, [ 'content' => $content, 'page' => $page, 'template' => $template, 'public' => true, // @deprecated Remove in 2.0 ] ); $content = $response->getContent(); } else { $content = str_replace('', $analytics."\n", $content); } if ($this->dispatcher->hasListeners(PageEvents::PAGE_ON_DISPLAY)) { $event = new PageDisplayEvent($content, $page, $this->getPreferenceCenterConfig()); if (isset($contact) && $contact instanceof Lead) { $event->setLead($contact); } $this->dispatcher->dispatch($event, PageEvents::PAGE_ON_DISPLAY); $content = $event->getContent(); } return new Response($content); } /** * @return Response * * @throws \Exception */ public function trackingImageAction(Request $request) { /** @var PageModel $model */ $model = $this->getModel('page'); $model->hitPage(null, $request); return TrackingPixelHelper::getResponse($request); } /** * @return JsonResponse * * @throws \Exception */ public function trackingAction( Request $request, DeviceTrackingServiceInterface $deviceTrackingService, TrackingHelper $trackingHelper, ContactTracker $contactTracker, ) { $notSuccessResponse = new JsonResponse( [ 'success' => 0, ] ); if (!$this->security->isAnonymous()) { return $notSuccessResponse; } /** @var PageModel $model */ $model = $this->getModel('page'); try { $model->hitPage(null, $request); } catch (InvalidDecodedStringException) { // do not track invalid ct return $notSuccessResponse; } $lead = $contactTracker->getContact(); $trackedDevice = $deviceTrackingService->getTrackedDevice(); $trackingId = (null === $trackedDevice ? null : $trackedDevice->getTrackingId()); $sessionValue = $trackingHelper->getCacheItem(true); $event = new TrackingEvent($lead, $request, $sessionValue); $this->dispatcher->dispatch($event, PageEvents::ON_CONTACT_TRACKED); return new JsonResponse( [ 'success' => 1, 'id' => ($lead) ? $lead->getId() : null, 'sid' => $trackingId, 'device_id' => $trackingId, 'events' => $event->getResponse()->all(), ] ); } /** * @throws \Exception */ public function redirectAction( Request $request, ContactRequestHelper $contactRequestHelper, PrimaryCompanyHelper $primaryCompanyHelper, IpLookupHelper $ipLookupHelper, LoggerInterface $logger, $redirectId, ): RedirectResponse { $logger->debug('Attempting to load redirect with tracking_id of: '.$redirectId); /** @var RedirectModel $redirectModel */ $redirectModel = $this->getModel(RedirectModel::class); $redirect = $redirectModel->getRedirectById($redirectId); $logger->debug('Executing Redirect: '.$redirect); if (null === $redirect || !$redirect->isPublished(false)) { $logger->debug('Redirect with tracking_id of '.$redirectId.' not found'); $url = ($redirect) ? $redirect->getUrl() : 'n/a'; throw $this->createNotFoundException($this->translator->trans('mautic.core.url.error.404', ['%url%' => $url])); } // Ensure the URL does not have encoded ampersands $url = UrlHelper::decodeAmpersands($redirect->getUrl()); // Get query string $query = $request->query->all(); $ct = null; // Remove click-trough parameter, so it won't be duplicated later. if (isset($query['ct'])) { $ct = $query['ct']; unset($query['ct']); } // Tak on anything left to the URL if (count($query)) { $url = UrlHelper::appendQueryToUrl($url, http_build_query($query)); } // 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 // This prevents simulated clicks from 3rd party services such as URL shorteners from simulating clicks $ipAddress = $ipLookupHelper->getIpAddress(); $isHitTrackable = false; if (null !== $ct && '' !== $ct) { if ($ipAddress->isTrackable()) { // Search replace lead fields in the URL /** @var PageModel $pageModel */ $pageModel = $this->getModel(PageModel::class); try { $lead = $contactRequestHelper->getContactFromQuery(['ct' => $ct]); $isHitTrackable = $pageModel->hitPage($redirect, $request, 200, $lead); } catch (InvalidDecodedStringException $e) { // Invalid ct value so we must unset it // and process the request without it $logger->error(sprintf('Invalid clickthrough value: %s', $ct), ['exception' => $e]); $request->request->remove('ct'); $request->query->remove('ct'); $lead = $contactRequestHelper->getContactFromQuery(); $isHitTrackable = $pageModel->hitPage($redirect, $request, 200, $lead); } $leadArray = ($lead) ? $primaryCompanyHelper->getProfileFieldsWithPrimaryCompany($lead) : []; $url = TokenHelper::findLeadTokens($url, $leadArray, true); } if (str_contains($url, $this->generateUrl('mautic_asset_download'))) { if (str_contains($url, '?')) { $url .= '&ct='.$ct; } else { $url .= '?ct='.$ct; } } } $url = UrlHelper::sanitizeAbsoluteUrl($url); if (!UrlHelper::isValidUrl($url)) { throw $this->createNotFoundException($this->translator->trans('mautic.core.url.error.404', ['%url%' => $url])); } $response = $this->redirect($url); $response->headers->setCookie(new Cookie('Blocked-Tracking', (string) !$isHitTrackable, strtotime('now + 15 seconds'))); return $response; } /** * Track video views. */ public function hitVideoAction(Request $request): JsonResponse|Response { // Only track XMLHttpRequests, because the hit should only come from there if ($request->isXmlHttpRequest()) { /** @var VideoModel $model */ $model = $this->getModel('page.video'); try { $model->hitVideo($request); } catch (\Exception) { return new JsonResponse(['success' => false]); } return new JsonResponse(['success' => true]); } return new Response(); } /** * Get the ID of the currently tracked Contact. */ public function getContactIdAction(DeviceTrackingServiceInterface $trackedDeviceService, ContactTracker $contactTracker): JsonResponse { $data = []; if ($this->security->isAnonymous()) { $lead = $contactTracker->getContact(); $trackedDevice = $trackedDeviceService->getTrackedDevice(); $trackingId = (null === $trackedDevice ? null : $trackedDevice->getTrackingId()); $data = [ 'id' => ($lead) ? $lead->getId() : null, 'sid' => $trackingId, 'device_id' => $trackingId, ]; } return new JsonResponse($data); } /** * @return array */ private function getPreferenceCenterConfig(): array { return [ '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'), ]; } }