mautic / app /bundles /PageBundle /Controller /PublicController.php
chrisbryan17's picture
Upload folder using huggingface_hub
d2897cd verified
<?php
namespace Mautic\PageBundle\Controller;
use Mautic\CoreBundle\Controller\AbstractFormController;
use Mautic\CoreBundle\Exception\InvalidDecodedStringException;
use Mautic\CoreBundle\Helper\CookieHelper;
use Mautic\CoreBundle\Helper\IpLookupHelper;
use Mautic\CoreBundle\Helper\TrackingPixelHelper;
use Mautic\CoreBundle\Helper\UrlHelper;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\CoreBundle\Twig\Helper\AnalyticsHelper;
use Mautic\CoreBundle\Twig\Helper\AssetsHelper;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Helper\ContactRequestHelper;
use Mautic\LeadBundle\Helper\PrimaryCompanyHelper;
use Mautic\LeadBundle\Helper\TokenHelper;
use Mautic\LeadBundle\Model\LeadModel;
use Mautic\LeadBundle\Tracker\ContactTracker;
use Mautic\LeadBundle\Tracker\Service\DeviceTrackingService\DeviceTrackingServiceInterface;
use Mautic\PageBundle\Entity\Page;
use Mautic\PageBundle\Event\PageDisplayEvent;
use Mautic\PageBundle\Event\TrackingEvent;
use Mautic\PageBundle\Helper\TrackingHelper;
use Mautic\PageBundle\Model\PageModel;
use Mautic\PageBundle\Model\Tracking404Model;
use Mautic\PageBundle\Model\VideoModel;
use Mautic\PageBundle\PageEvents;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
class PublicController extends AbstractFormController
{
/**
* @return Response
*
* @throws \Exception
* @throws \Mautic\CoreBundle\Exception\FileNotFoundException
*/
public function indexAction(
Request $request,
ContactRequestHelper $contactRequestHelper,
CookieHelper $cookieHelper,
AnalyticsHelper $analyticsHelper,
AssetsHelper $assetsHelper,
Tracking404Model $tracking404Model,
RouterInterface $router,
$slug)
{
/** @var PageModel $model */
$model = $this->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());
return $this->redirect($entity->getRedirectUrl(), (int) $entity->getRedirectType());
} 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 ($variantCookie !== $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
);
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
$slots = $this->factory->getTheme($template)->getSlots('page');
$content = $entity->getContent();
$this->processSlots($slots, $entity);
// Add the GA code to the template assets
if (!empty($analytics)) {
$this->factory->getHelper('template.assets')->addCustomDeclaration($analytics);
}
$logicalName = $this->factory->getHelper('theme')->checkForTwigTemplate('@themes/'.$template.'/html/page.html.twig');
$response = $this->render(
$logicalName,
[
'slots' => $slots,
'content' => $content,
'page' => $entity,
'template' => $template,
'public' => true,
]
);
$content = $response->getContent();
} else {
if (!empty($analytics)) {
$content = str_replace('</head>', $analytics."\n</head>", $content);
}
if ($entity->getNoIndex()) {
$content = str_replace('</head>', "<meta name=\"robots\" content=\"noindex\">\n</head>", $content);
}
}
$assetsHelper->addScript($router->generate('mautic_js', [], UrlGeneratorInterface::ABSOLUTE_URL),
'onPageDisplay_headClose',
true,
'mautic_js'
);
$event = new PageDisplayEvent($content, $entity);
$this->dispatcher->dispatch($event, PageEvents::PAGE_ON_DISPLAY);
$content = $event->getContent();
$model->hitPage($entity, $request, 200, $lead, $query);
return new Response($content);
}
if (false !== $entity && $tracking404Model->isTrackable()) {
$tracking404Model->hitPage($entity, $request);
}
return $this->notFound();
}
/**
* @return Response|\Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*
* @throws \Exception
* @throws \Mautic\CoreBundle\Exception\FileNotFoundException
*/
public function previewAction(Request $request, CorePermissions $security, int $id)
{
$contactId = (int) $request->query->get('contactId');
if ($contactId) {
/** @var LeadModel $leadModel */
$leadModel = $this->getModel('lead.lead');
/** @var Lead $contact */
$contact = $leadModel->getEntity($contactId);
}
/** @var PageModel $model */
$model = $this->getModel('page');
/** @var Page $page */
$page = $model->getEntity($id);
if (!$page->getId()) {
return $this->notFound();
}
$analytics = $this->factory->getHelper('twig.analytics')->getCode();
$BCcontent = $page->getContent();
$content = $page->getCustomHtml();
if (!$security->isAdmin()
&& (
(!$page->isPublished())
|| (!$security->hasEntityAccess(
'email:emails:viewown',
'email:emails:viewother',
$page->getCreatedBy()
)))
) {
return $this->accessDenied();
}
if ($contactId && (
!$security->isAdmin()
|| !$security->hasEntityAccess('lead:leads:viewown', 'lead:leads:viewother')
)
) {
return $this->accessDenied();
}
if (empty($content) && !empty($BCcontent)) {
$template = $page->getTemplate();
// all the checks pass so display the content
$slots = $this->factory->getTheme($template)->getSlots('page');
$content = $page->getContent();
$this->processSlots($slots, $page);
// Add the GA code to the template assets
if (!empty($analytics)) {
$this->factory->getHelper('template.assets')->addCustomDeclaration($analytics);
}
$logicalName = $this->factory->getHelper('theme')->checkForTwigTemplate('@themes/'.$template.'/html/page.html.twig');
$response = $this->render(
$logicalName,
[
'slots' => $slots,
'content' => $content,
'page' => $page,
'template' => $template,
'public' => true, // @deprecated Remove in 2.0
]
);
$content = $response->getContent();
} else {
$content = str_replace('</head>', $analytics.$this->renderView('@MauticPage/Page/preview_header.html.twig')."\n</head>", $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
): \Symfony\Component\HttpFoundation\RedirectResponse {
$logger->debug('Attempting to load redirect with tracking_id of: '.$redirectId);
/** @var \Mautic\PageBundle\Model\RedirectModel $redirectModel */
$redirectModel = $this->getModel('page.redirect');
$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 = $query['ct'] ?? null;
// 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();
if ($ct) {
if ($ipAddress->isTrackable()) {
// Search replace lead fields in the URL
/** @var PageModel $pageModel */
$pageModel = $this->getModel('page');
try {
$lead = $contactRequestHelper->getContactFromQuery(['ct' => $ct]);
$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->set('ct', '');
$request->query->set('ct', '');
$lead = $contactRequestHelper->getContactFromQuery();
$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 (strpos($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]));
}
return $this->redirect($url);
}
/**
* PreProcess page slots for public view.
*
* @deprecated - to be removed in 3.0
*
* @param array $slots
* @param Page $entity
*/
private function processSlots($slots, $entity): void
{
/** @var AssetsHelper $assetsHelper */
$assetsHelper = $this->factory->getHelper('template.assets');
/** @var \Mautic\CoreBundle\Twig\Helper\SlotsHelper $slotsHelper */
$slotsHelper = $this->factory->getHelper('template.slots');
$content = $entity->getContent();
foreach ($slots as $slot => $slotConfig) {
// backward compatibility - if slotConfig array does not exist
if (is_numeric($slot)) {
$slot = $slotConfig;
$slotConfig = [];
}
if (isset($slotConfig['type']) && 'slideshow' == $slotConfig['type']) {
if (isset($content[$slot])) {
$options = json_decode($content[$slot], true);
} else {
$options = [
'width' => '100%',
'height' => '250px',
'background_color' => 'transparent',
'arrow_navigation' => false,
'dot_navigation' => true,
'interval' => 5000,
'pause' => 'hover',
'wrap' => true,
'keyboard' => true,
];
}
// Create sample slides for first time or if all slides were deleted
if (empty($options['slides'])) {
$options['slides'] = [
[
'order' => 0,
'background-image' => $assetsHelper->getOverridableUrl('images/mautic_logo_lb200.png'),
'captionheader' => 'Caption 1',
],
[
'order' => 1,
'background-image' => $assetsHelper->getOverridableUrl('images/mautic_logo_db200.png'),
'captionheader' => 'Caption 2',
],
];
}
// Order slides
usort(
$options['slides'],
fn ($a, $b): int => strcmp($a['order'], $b['order'])
);
$options['slot'] = $slot;
$options['public'] = true;
} elseif (isset($slotConfig['type']) && 'textarea' == $slotConfig['type']) {
$value = isset($content[$slot]) ? nl2br($content[$slot]) : '';
$slotsHelper->set($slot, $value);
} else {
// Fallback for other types like html, text, textarea and all unknown
$value = $content[$slot] ?? '';
$slotsHelper->set($slot, $value);
}
}
$parentVariant = $entity->getVariantParent();
$title = (!empty($parentVariant)) ? $parentVariant->getTitle() : $entity->getTitle();
$slotsHelper->set('pageTitle', $title);
}
/**
* Track video views.
*/
public function hitVideoAction(Request $request)
{
// 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<string,bool>
*/
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'),
];
}
}