Spaces:
No application file
No application file
namespace Mautic\PageBundle\Model; | |
use Doctrine\DBAL\Query\QueryBuilder; | |
use Doctrine\ORM\EntityManager; | |
use Mautic\CoreBundle\Helper\Chart\ChartQuery; | |
use Mautic\CoreBundle\Helper\Chart\LineChart; | |
use Mautic\CoreBundle\Helper\Chart\PieChart; | |
use Mautic\CoreBundle\Helper\CookieHelper; | |
use Mautic\CoreBundle\Helper\CoreParametersHelper; | |
use Mautic\CoreBundle\Helper\DateTimeHelper; | |
use Mautic\CoreBundle\Helper\InputHelper; | |
use Mautic\CoreBundle\Helper\IpLookupHelper; | |
use Mautic\CoreBundle\Helper\UserHelper; | |
use Mautic\CoreBundle\Model\BuilderModelTrait; | |
use Mautic\CoreBundle\Model\FormModel; | |
use Mautic\CoreBundle\Model\TranslationModelTrait; | |
use Mautic\CoreBundle\Model\VariantModelTrait; | |
use Mautic\CoreBundle\Security\Permissions\CorePermissions; | |
use Mautic\CoreBundle\Translation\Translator; | |
use Mautic\LeadBundle\DataObject\LeadManipulator; | |
use Mautic\LeadBundle\Entity\Company; | |
use Mautic\LeadBundle\Entity\Lead; | |
use Mautic\LeadBundle\Entity\UtmTag; | |
use Mautic\LeadBundle\Helper\ContactRequestHelper; | |
use Mautic\LeadBundle\Helper\IdentifyCompanyHelper; | |
use Mautic\LeadBundle\Model\CompanyModel; | |
use Mautic\LeadBundle\Model\FieldModel; | |
use Mautic\LeadBundle\Model\LeadModel; | |
use Mautic\LeadBundle\Tracker\ContactTracker; | |
use Mautic\LeadBundle\Tracker\DeviceTracker; | |
use Mautic\MessengerBundle\Message\PageHitNotification; | |
use Mautic\PageBundle\Entity\Hit; | |
use Mautic\PageBundle\Entity\Page; | |
use Mautic\PageBundle\Entity\Redirect; | |
use Mautic\PageBundle\Event\PageBuilderEvent; | |
use Mautic\PageBundle\Event\PageEvent; | |
use Mautic\PageBundle\Event\PageHitEvent; | |
use Mautic\PageBundle\Form\Type\PageType; | |
use Mautic\PageBundle\PageEvents; | |
use Psr\Log\LoggerInterface; | |
use Symfony\Component\EventDispatcher\EventDispatcherInterface; | |
use Symfony\Component\Form\FormFactoryInterface; | |
use Symfony\Component\HttpFoundation\Cookie; | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; | |
use Symfony\Component\Messenger\MessageBusInterface; | |
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; | |
use Symfony\Contracts\EventDispatcher\Event; | |
/** | |
* @extends FormModel<Page> | |
*/ | |
class PageModel extends FormModel | |
{ | |
use TranslationModelTrait; | |
use VariantModelTrait; | |
use BuilderModelTrait; | |
/** | |
* @var bool | |
*/ | |
protected $catInUrl; | |
protected DateTimeHelper $dateTimeHelper; | |
public function __construct( | |
protected CookieHelper $cookieHelper, | |
protected IpLookupHelper $ipLookupHelper, | |
protected LeadModel $leadModel, | |
protected FieldModel $leadFieldModel, | |
protected RedirectModel $pageRedirectModel, | |
protected TrackableModel $pageTrackableModel, | |
private MessageBusInterface $messageBus, | |
private CompanyModel $companyModel, | |
private DeviceTracker $deviceTracker, | |
private ContactTracker $contactTracker, | |
CoreParametersHelper $coreParametersHelper, | |
private ContactRequestHelper $contactRequestHelper, | |
EntityManager $em, | |
CorePermissions $security, | |
EventDispatcherInterface $dispatcher, | |
UrlGeneratorInterface $router, | |
Translator $translator, | |
UserHelper $userHelper, | |
LoggerInterface $mauticLogger | |
) { | |
$this->dateTimeHelper = new DateTimeHelper(); | |
parent::__construct($em, $security, $dispatcher, $router, $translator, $userHelper, $mauticLogger, $coreParametersHelper); | |
} | |
public function setCatInUrl($catInUrl): void | |
{ | |
$this->catInUrl = $catInUrl; | |
} | |
/** | |
* @return \Mautic\PageBundle\Entity\PageRepository | |
*/ | |
public function getRepository() | |
{ | |
$repo = $this->em->getRepository(Page::class); | |
$repo->setCurrentUser($this->userHelper->getUser()); | |
return $repo; | |
} | |
/** | |
* @return \Mautic\PageBundle\Entity\HitRepository | |
*/ | |
public function getHitRepository() | |
{ | |
return $this->em->getRepository(Hit::class); | |
} | |
public function getPermissionBase(): string | |
{ | |
return 'page:pages'; | |
} | |
public function getNameGetter(): string | |
{ | |
return 'getTitle'; | |
} | |
/** | |
* @param Page $entity | |
* @param bool $unlock | |
*/ | |
public function saveEntity($entity, $unlock = true): void | |
{ | |
$pageIds = $entity->getRelatedEntityIds(); | |
if (empty($this->inConversion)) { | |
$alias = $entity->getAlias(); | |
if (empty($alias)) { | |
$alias = $entity->getTitle(); | |
} | |
$alias = $this->cleanAlias($alias, '', 0, '-', ['_']); | |
// make sure alias is not already taken | |
$repo = $this->getRepository(); | |
$testAlias = $alias; | |
$count = $repo->checkPageUniqueAlias($testAlias, $pageIds); | |
$aliasTag = 1; | |
while ($count) { | |
$testAlias = $alias.$aliasTag; | |
$count = $repo->checkPageUniqueAlias($testAlias, $pageIds); | |
++$aliasTag; | |
} | |
if ($testAlias != $alias) { | |
$alias = $testAlias; | |
} | |
$entity->setAlias($alias); | |
} | |
// Set the author for new pages | |
$isNew = $entity->isNew(); | |
if (!$isNew) { | |
// increase the revision | |
$revision = $entity->getRevision(); | |
++$revision; | |
$entity->setRevision($revision); | |
} | |
// Reset a/b test if applicable | |
$variantStartDate = new \DateTime(); | |
$resetVariants = $this->preVariantSaveEntity($entity, ['setVariantHits'], $variantStartDate); | |
parent::saveEntity($entity, $unlock); | |
$this->postVariantSaveEntity($entity, $resetVariants, $pageIds, $variantStartDate); | |
$this->postTranslationEntitySave($entity); | |
} | |
/** | |
* @param Page $entity | |
*/ | |
public function deleteEntity($entity): void | |
{ | |
if ($entity->isVariant() && $entity->getIsPublished()) { | |
$this->resetVariants($entity); | |
} | |
parent::deleteEntity($entity); | |
} | |
/** | |
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException | |
*/ | |
public function createForm($entity, FormFactoryInterface $formFactory, $action = null, $options = []): \Symfony\Component\Form\FormInterface | |
{ | |
if (!$entity instanceof Page) { | |
throw new MethodNotAllowedHttpException(['Page']); | |
} | |
$formClass = PageType::class; | |
if (!empty($options['formName'])) { | |
$formClass = $options['formName']; | |
} | |
if (!empty($action)) { | |
$options['action'] = $action; | |
} | |
return $formFactory->create($formClass, $entity, $options); | |
} | |
public function getEntity($id = null): ?Page | |
{ | |
if (null === $id) { | |
$entity = new Page(); | |
$entity->setSessionId('new_'.hash('sha1', uniqid(mt_rand()))); | |
} else { | |
$entity = parent::getEntity($id); | |
if (null !== $entity) { | |
$entity->setSessionId($entity->getId()); | |
} | |
} | |
return $entity; | |
} | |
/** | |
* @throws MethodNotAllowedHttpException | |
*/ | |
protected function dispatchEvent($action, &$entity, $isNew = false, Event $event = null): ?Event | |
{ | |
if (!$entity instanceof Page) { | |
throw new MethodNotAllowedHttpException(['Page']); | |
} | |
switch ($action) { | |
case 'pre_save': | |
$name = PageEvents::PAGE_PRE_SAVE; | |
break; | |
case 'post_save': | |
$name = PageEvents::PAGE_POST_SAVE; | |
break; | |
case 'pre_delete': | |
$name = PageEvents::PAGE_PRE_DELETE; | |
break; | |
case 'post_delete': | |
$name = PageEvents::PAGE_POST_DELETE; | |
break; | |
default: | |
return null; | |
} | |
if ($this->dispatcher->hasListeners($name)) { | |
if (empty($event)) { | |
$event = new PageEvent($entity, $isNew); | |
$event->setEntityManager($this->em); | |
} | |
$this->dispatcher->dispatch($event, $name); | |
return $event; | |
} | |
return null; | |
} | |
/** | |
* Get list of entities for autopopulate fields. | |
* | |
* @param string $type | |
* @param string $filter | |
* @param int $limit | |
* | |
* @return array | |
*/ | |
public function getLookupResults($type, $filter = '', $limit = 10) | |
{ | |
$results = []; | |
switch ($type) { | |
case 'page': | |
$viewOther = $this->security->isGranted('page:pages:viewother'); | |
$repo = $this->getRepository(); | |
$repo->setCurrentUser($this->userHelper->getUser()); | |
$results = $repo->getPageList($filter, $limit, 0, $viewOther); | |
break; | |
} | |
return $results; | |
} | |
/** | |
* Generate URL for a page. | |
* | |
* @param Page $entity | |
* @param bool $absolute | |
* @param array $clickthrough | |
* | |
* @return string | |
*/ | |
public function generateUrl($entity, $absolute = true, $clickthrough = []) | |
{ | |
// If this is a variant, then get the parent's URL | |
$parent = $entity->getVariantParent(); | |
if (null != $parent) { | |
$entity = $parent; | |
} | |
$slug = $this->generateSlug($entity); | |
return $this->buildUrl('mautic_page_public', ['slug' => $slug], $absolute, $clickthrough); | |
} | |
/** | |
* Generates slug string. | |
*/ | |
public function generateSlug($entity): string | |
{ | |
$pageSlug = $entity->getAlias(); | |
// should the url include the category | |
if ($this->catInUrl) { | |
$category = $entity->getCategory(); | |
$catSlug = (!empty($category)) | |
? $category->getAlias() | |
: | |
$this->translator->trans('mautic.core.url.uncategorized'); | |
} | |
$parent = $entity->getTranslationParent(); | |
$slugs = []; | |
if ($parent) { | |
// multiple languages so tack on the language | |
$slugs[] = $entity->getLanguage(); | |
} | |
if (!empty($catSlug)) { | |
// Insert category slug | |
$slugs[] = $catSlug; | |
$slugs[] = $pageSlug; | |
} else { | |
// Insert just the page slug | |
$slugs[] = $pageSlug; | |
} | |
return implode('/', $slugs); | |
} | |
/** | |
* @return array|mixed | |
*/ | |
protected function generateClickThrough(Hit $hit) | |
{ | |
$query = $hit->getQuery(); | |
// Check for any clickthrough info | |
$clickthrough = []; | |
if (!empty($query['ct'])) { | |
$clickthrough = $query['ct']; | |
if (!is_array($clickthrough)) { | |
$clickthrough = $this->decodeArrayFromUrl($clickthrough); | |
} | |
} | |
return $clickthrough; | |
} | |
/** | |
* @param string|int $code | |
* @param array $query | |
* | |
* @throws \Exception | |
*/ | |
public function hitPage(Redirect|Page|null $page, Request $request, $code = '200', Lead $lead = null, $query = []): void | |
{ | |
// Don't skew results with user hits | |
if (!$this->security->isAnonymous()) { | |
return; | |
} | |
// Process the query | |
if (empty($query) || !is_array($query)) { | |
$query = $this->getHitQuery($request, $page); | |
} | |
// Get lead if required | |
if (null == $lead) { | |
$lead = $this->contactRequestHelper->getContactFromQuery($query); | |
// company | |
[$company, $leadAdded, $companyEntity] = IdentifyCompanyHelper::identifyLeadsCompany($query, $lead, $this->companyModel); | |
$companyChangeLog = null; | |
if ($leadAdded) { | |
$companyChangeLog = $lead->addCompanyChangeLogEntry('form', 'Identify Company', 'Lead added to the company, '.$company['companyname'], $company['id']); | |
} elseif ($companyEntity instanceof Company) { | |
$this->companyModel->setFieldValues($companyEntity, $query); | |
$this->companyModel->saveEntity($companyEntity); | |
} | |
if (!empty($company) and $companyEntity instanceof Company) { | |
// Save after the lead in for new leads created through the API and maybe other places | |
$this->companyModel->addLeadToCompany($companyEntity, $lead); | |
$this->leadModel->setPrimaryCompany($companyEntity->getId(), $lead->getId()); | |
} | |
if (null !== $companyChangeLog) { | |
$this->companyModel->getCompanyLeadRepository()->detachEntity($companyChangeLog); | |
} | |
} | |
if (!$lead || !$lead->getId()) { | |
// Lead came from a non-trackable IP so ignore | |
return; | |
} | |
$hit = new Hit(); | |
$hit->setDateHit(new \DateTime()); | |
$hit->setIpAddress($this->ipLookupHelper->getIpAddress()); | |
// Set info from request | |
$hit->setQuery($query); | |
$hit->setCode($code); | |
$trackedDevice = $this->deviceTracker->createDeviceFromUserAgent($lead, $request->server->get('HTTP_USER_AGENT')); | |
$hit->setTrackingId($trackedDevice->getTrackingId()); | |
$hit->setDeviceStat($trackedDevice); | |
// Wrap in a try/catch to prevent deadlock errors on busy servers | |
try { | |
$this->em->persist($hit); | |
$this->em->flush(); | |
} catch (\Exception $exception) { | |
if (MAUTIC_ENV === 'dev') { | |
throw $exception; | |
} else { | |
$this->logger->error( | |
$exception->getMessage(), | |
['exception' => $exception] | |
); | |
} | |
return; | |
} | |
// save hit to the cookie to use to update the exit time | |
if ($hit) { | |
$this->cookieHelper->setCookie( | |
name: 'mautic_referer_id', | |
value: $hit->getId() ?: null, | |
sameSite: Cookie::SAMESITE_NONE | |
); | |
} | |
$message = new PageHitNotification( | |
$hit->getId(), | |
$request, | |
$this->deviceTracker->wasDeviceChanged(), | |
$page instanceof Redirect, | |
$page?->getId(), | |
$lead->getId() | |
); | |
try { | |
$this->messageBus->dispatch($message); | |
} catch (\Exception $exception) { | |
$this->logger->error('Failed to dispatch a message to messenger. '.$exception->getMessage()); | |
// Fallback measure | |
$this->processPageHit($hit, $page, $request, $lead, $this->deviceTracker->wasDeviceChanged()); | |
} | |
} | |
/** | |
* Process page hit. | |
* | |
* @throws \Exception | |
*/ | |
public function processPageHit( | |
Hit $hit, | |
Redirect|Page|null $page, | |
Request $request, | |
Lead $lead, | |
bool $trackingNewlyGenerated, | |
bool $activeRequest = true, | |
\DateTimeInterface $hitDate = null | |
): void { | |
// Store Page/Redirect association | |
if ($page) { | |
if ($page instanceof Page) { | |
$hit->setPage($page); | |
} else { | |
$hit->setRedirect($page); | |
} | |
} | |
// Check for any clickthrough info | |
$clickthrough = $this->generateClickThrough($hit); | |
if (!empty($clickthrough)) { | |
if (!empty($clickthrough['channel'])) { | |
if (1 === count($clickthrough['channel'])) { | |
$channelId = reset($clickthrough['channel']); | |
$channel = key($clickthrough['channel']); | |
} else { | |
$channel = $clickthrough['channel'][0]; | |
$channelId = (int) $clickthrough['channel'][1]; | |
} | |
$hit->setSource($channel); | |
$hit->setSourceId($channelId); | |
} elseif (!empty($clickthrough['source'])) { | |
$hit->setSource($clickthrough['source'][0]); | |
$hit->setSourceId($clickthrough['source'][1]); | |
} | |
if (!empty($clickthrough['email'])) { | |
$emailRepo = $this->em->getRepository(\Mautic\EmailBundle\Entity\Email::class); | |
if ($emailEntity = $emailRepo->getEntity($clickthrough['email'])) { | |
$hit->setEmail($emailEntity); | |
} | |
} | |
} | |
$query = $hit->getQuery() ?: []; | |
if (isset($query['timezone_offset']) && !$lead->getTimezone()) { | |
// timezone_offset holds timezone offset in minutes. Multiply by 60 to get seconds. | |
// Multiply by -1 because Firgerprint2 seems to have it the other way around. | |
$timezone = (-1 * $query['timezone_offset'] * 60); | |
$lead->setTimezone($this->dateTimeHelper->guessTimezoneFromOffset($timezone)); | |
} | |
$query = $this->cleanQuery($query); | |
if (isset($query['page_referrer'])) { | |
$hit->setReferer($query['page_referrer']); | |
} | |
if (isset($query['page_language'])) { | |
$hit->setPageLanguage($query['page_language']); | |
} | |
if ($pageTitle = $query['page_title'] ?? ($page instanceof Page ? $page->getTitle() : false)) { | |
// Transliterate page titles. | |
if ($this->coreParametersHelper->get('transliterate_page_title')) { | |
$pageTitle = InputHelper::transliterate($pageTitle); | |
} | |
$query['page_title'] = $pageTitle; | |
$hit->setUrlTitle($pageTitle); | |
} | |
$hit->setQuery($query); | |
$hit->setUrl($query['page_url'] ?? $request->getRequestUri()); | |
// Add entry to contact log table | |
$this->setLeadManipulator($page, $hit, $lead); | |
// Store tracking ID | |
$hit->setLead($lead); | |
if (!$activeRequest) { | |
// Queue is consuming this hit outside of the lead's active request so this must be set in order for listeners to know who the request belongs to | |
$this->contactTracker->setSystemContact($lead); | |
} | |
$trackingId = $hit->getTrackingId(); | |
if (!$trackingNewlyGenerated) { | |
$lastHit = $request->cookies->get('mautic_referer_id'); | |
if (!empty($lastHit)) { | |
// this is not a new session so update the last hit if applicable with the date/time the user left | |
$this->getHitRepository()->updateHitDateLeft($lastHit); | |
} | |
} | |
// Check if this is a unique page hit | |
$isUnique = $this->getHitRepository()->isUniquePageHit($page, $trackingId, $lead); | |
if ($page instanceof Page) { | |
$hit->setPageLanguage($page->getLanguage()); | |
$isVariant = ($isUnique) ? $page->getVariantStartDate() : false; | |
try { | |
$this->getRepository()->upHitCount($page->getId(), 1, $isUnique, !empty($isVariant)); | |
} catch (\Exception $exception) { | |
$this->logger->error( | |
$exception->getMessage(), | |
['exception' => $exception] | |
); | |
} | |
} elseif ($page instanceof Redirect) { | |
try { | |
$this->pageRedirectModel->getRepository()->upHitCount($page->getId(), 1, $isUnique); | |
// If this is a trackable, up the trackable counts as well | |
if ($hit->getSource() && $hit->getSourceId()) { | |
$this->pageTrackableModel->getRepository()->upHitCount( | |
$page->getId(), | |
$hit->getSource(), | |
$hit->getSourceId(), | |
1, | |
$isUnique | |
); | |
} | |
} catch (\Exception $exception) { | |
if (MAUTIC_ENV === 'dev') { | |
throw $exception; | |
} else { | |
$this->logger->error( | |
$exception->getMessage(), | |
['exception' => $exception] | |
); | |
} | |
} | |
} | |
// glean info from the IP address | |
$ipAddress = $hit->getIpAddress(); | |
if ($ipAddress && $details = $ipAddress->getIpDetails()) { | |
$hit->setCountry($details['country']); | |
$hit->setRegion($details['region']); | |
$hit->setCity($details['city']); | |
$hit->setIsp($details['isp']); | |
$hit->setOrganization($details['organization']); | |
} | |
if (!$hit->getReferer()) { | |
$hit->setReferer($request->server->get('HTTP_REFERER')); | |
} | |
$hit->setUserAgent($request->server->get('HTTP_USER_AGENT')); | |
$hit->setRemoteHost($request->server->get('REMOTE_HOST')); | |
$this->setUtmTags($hit, $lead); | |
// get a list of the languages the user prefers | |
$browserLanguages = $request->server->get('HTTP_ACCEPT_LANGUAGE'); | |
if (!empty($browserLanguages)) { | |
$languages = explode(',', $browserLanguages); | |
foreach ($languages as $k => $l) { | |
if (($pos = strpos(';q=', $l)) !== false) { | |
// remove weights | |
$languages[$k] = substr($l, 0, $pos); | |
} | |
} | |
$hit->setBrowserLanguages($languages); | |
} | |
// Wrap in a try/catch to prevent deadlock errors on busy servers | |
try { | |
$this->em->persist($hit); | |
$this->em->flush(); | |
} catch (\Exception $exception) { | |
if (MAUTIC_ENV === 'dev') { | |
throw $exception; | |
} else { | |
$this->logger->error( | |
$exception->getMessage(), | |
['exception' => $exception] | |
); | |
} | |
} | |
if ($this->dispatcher->hasListeners(PageEvents::PAGE_ON_HIT)) { | |
$event = new PageHitEvent($hit, $request, $hit->getCode(), $clickthrough, $isUnique); | |
$this->dispatcher->dispatch($event, PageEvents::PAGE_ON_HIT); | |
} | |
if (null !== $hitDate) { | |
if (null === $lead->getLastActive() || $lead->getLastActive() < $hitDate) { | |
try { | |
$this->leadModel->getRepository()->updateLastActive($lead->getId(), $hitDate); | |
} catch (\Exception $e) { | |
$data = [ | |
'unique' => ($isUnique ? 'true' : 'false'), | |
'lead' => $lead->getId(), | |
'page' => $page->getId(), | |
'hit' => $hit->getId(), | |
'lastActiveOriginal' => $lead->getLastActive(), | |
'newLastActive' => $hitDate, | |
]; | |
$this->logger->error( | |
'Failed to update event time due to '.$e->getMessage(), | |
['context' => $data, 'exception' => (array) $e] | |
); | |
} | |
} | |
} | |
} | |
/** | |
* @param Redirect|Page|null $page | |
*/ | |
public function getHitQuery(Request $request, $page = null): array | |
{ | |
$get = $request->query->all(); | |
$post = $request->request->all(); | |
$query = \array_merge($get, $post); | |
// Set generated page url | |
$query['page_url'] = $this->getPageUrl($request, $page); | |
// get all params from the url (actual url or passed in as page_url) | |
if (!empty($query['page_url'])) { | |
$queryUrl = $this->getQueryFromUrl($query['page_url']); | |
$query = \array_merge($queryUrl, $query); | |
} | |
// Process clickthrough if applicable | |
if (!empty($query['ct'])) { | |
$query['ct'] = $this->decodeArrayFromUrl($query['ct']); | |
} | |
return $query; | |
} | |
/** | |
* Get array of page builder tokens from bundles subscribed PageEvents::PAGE_ON_BUILD. | |
* | |
* @param array|string $requestedComponents all | tokens | abTestWinnerCriteria | |
* | |
* @return array | |
*/ | |
public function getBuilderComponents(Page $page = null, $requestedComponents = 'all', string $tokenFilter = '') | |
{ | |
$event = new PageBuilderEvent($this->translator, $page, $requestedComponents, $tokenFilter); | |
$this->dispatcher->dispatch($event, PageEvents::PAGE_ON_BUILD); | |
return $this->getCommonBuilderComponents($requestedComponents, $event); | |
} | |
/** | |
* Get number of page bounces. | |
* | |
* @return mixed[] | |
*/ | |
public function getBounces(Page $page, \DateTime $fromDate = null): array | |
{ | |
return $this->getHitRepository()->getBounces($page->getId(), $fromDate); | |
} | |
/** | |
* Joins the page table and limits created_by to currently logged in user. | |
*/ | |
public function limitQueryToCreator(QueryBuilder &$q): void | |
{ | |
$q->join('t', MAUTIC_TABLE_PREFIX.'pages', 'p', 'p.id = t.page_id') | |
->andWhere('p.created_by = :userId') | |
->setParameter('userId', $this->userHelper->getUser()->getId()); | |
} | |
/** | |
* Get line chart data of hits. | |
* | |
* @param char $unit {@link php.net/manual/en/function.date.php#refsect1-function.date-parameters} | |
* @param string $dateFormat | |
* @param array $filter | |
* @param bool $canViewOthers | |
*/ | |
public function getHitsLineChartData($unit, \DateTime $dateFrom, \DateTime $dateTo, $dateFormat = null, $filter = [], $canViewOthers = true): array | |
{ | |
$flag = null; | |
if (isset($filter['flag'])) { | |
$flag = $filter['flag']; | |
unset($filter['flag']); | |
} | |
$chart = new LineChart($unit, $dateFrom, $dateTo, $dateFormat); | |
$query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo); | |
if (!$flag || 'total_and_unique' == $flag) { | |
$q = $query->prepareTimeDataQuery('page_hits', 'date_hit', $filter); | |
if (!$canViewOthers) { | |
$this->limitQueryToCreator($q); | |
} | |
$data = $query->loadAndBuildTimeData($q); | |
$chart->setDataset($this->translator->trans('mautic.page.show.total.visits'), $data); | |
} | |
if ('unique' == $flag || 'total_and_unique' == $flag) { | |
$q = $query->prepareTimeDataQuery( | |
'page_hits', | |
'date_hit', | |
$filter, | |
'distinct(t.lead_id)', | |
true, | |
false | |
); | |
if (!$canViewOthers) { | |
$this->limitQueryToCreator($q); | |
} | |
$data = $query->loadAndBuildTimeData($q); | |
$chart->setDataset($this->translator->trans('mautic.page.show.unique.visits'), $data); | |
} | |
return $chart->render(); | |
} | |
/** | |
* @deprecated Use getUniqueVsReturningPieChartData() instead. | |
* | |
* Get data for pie chart showing new vs returning leads. | |
* Returning leads are even leads who visit 2 different page once. | |
* | |
* @param \DateTime $dateFrom | |
* @param \DateTime $dateTo | |
* @param array $filters | |
* @param bool $canViewOthers | |
*/ | |
public function getNewVsReturningPieChartData($dateFrom, $dateTo, $filters = [], $canViewOthers = true): array | |
{ | |
$chart = new PieChart(); | |
$query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo); | |
$allQ = $query->getCountQuery('page_hits', 'id', 'date_hit', $filters); | |
$filters['lead_id'] = [ | |
'expression' => 'isNull', | |
]; | |
$returnQ = $query->getCountQuery('page_hits', 'id', 'date_hit', $filters); | |
if (!$canViewOthers) { | |
$this->limitQueryToCreator($allQ); | |
$this->limitQueryToCreator($returnQ); | |
} | |
$all = $query->fetchCount($allQ); | |
$returning = $query->fetchCount($returnQ); | |
$unique = $all - $returning; | |
$chart->setDataset($this->translator->trans('mautic.page.unique'), $unique); | |
$chart->setDataset($this->translator->trans('mautic.page.graph.pie.new.vs.returning.returning'), $returning); | |
return $chart->render(); | |
} | |
/** | |
* Get data for pie chart showing new vs returning leads. | |
* Returning leads are even leads who visits 2 different page once. | |
* | |
* @return array<string, array<int|string>> | |
*/ | |
public function getUniqueVsReturningPieChartData(\DateTime $dateFrom, \DateTime $dateTo, bool $canViewOthers = true): array | |
{ | |
$chart = new PieChart(); | |
$query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo); | |
$filters = [ | |
'lead_id' => [ | |
'expression' => 'isNotNull', | |
], | |
'date_left' => [ | |
'expression' => 'isNull', | |
], | |
'redirect_id' => [ | |
'expression' => 'isNull', | |
], | |
'email_id' => [ | |
'expression' => 'isNull', | |
], | |
]; | |
$allQ = $query->getCountQuery('page_hits', 'id', 'date_hit', $filters); | |
if (!$canViewOthers) { | |
$this->limitQueryToCreator($allQ); | |
} | |
$allQ->resetQueryPart('select')->select('t.lead_id'); | |
$allQ->groupBy('t.lead_id'); | |
// fetch all group by lead_id | |
$q = $this->em->getConnection()->createQueryBuilder(); | |
$q->select('COUNT(*) as count') | |
->from( | |
sprintf('(%s)', $allQ->getSQL()), 'tt' | |
); | |
$q->setParameters($allQ->getParameters()); | |
$all = $query->fetchCount($q); | |
// date_left is NULL more like 1 mean returned visitor | |
$allQ->having('COUNT(t.id) > 1'); | |
$q = $this->em->getConnection()->createQueryBuilder(); | |
$q->select('COUNT(*) as count') | |
->from( | |
sprintf('(%s)', $allQ->getSQL()), 'tt' | |
); | |
$q->setParameters($allQ->getParameters()); | |
$returning = $query->fetchCount($q); | |
$unique = $all - $returning; | |
$chart->setDataset($this->translator->trans('mautic.page.unique'), $unique); | |
$chart->setDataset($this->translator->trans('mautic.page.graph.pie.new.vs.returning.returning'), $returning); | |
return $chart->render(); | |
} | |
/** | |
* Get pie chart data of dwell times. | |
* | |
* @param array $filters | |
* @param bool $canViewOthers | |
*/ | |
public function getDwellTimesPieChartData(\DateTime $dateFrom, \DateTime $dateTo, $filters = [], $canViewOthers = true): array | |
{ | |
$timesOnSite = $this->getHitRepository()->getDwellTimeLabels(); | |
$chart = new PieChart(); | |
$query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo); | |
foreach ($timesOnSite as $time) { | |
$q = $query->getCountDateDiffQuery('page_hits', 'date_hit', 'date_left', $time['from'], $time['till'], $filters); | |
if (!$canViewOthers) { | |
$this->limitQueryToCreator($q); | |
} | |
$data = $query->fetchCountDateDiff($q); | |
$chart->setDataset($time['label'], $data); | |
} | |
return $chart->render(); | |
} | |
/** | |
* Get bar chart data of hits. | |
*/ | |
public function getDeviceGranularityData(\DateTime $dateFrom, \DateTime $dateTo, $filters = [], $canViewOthers = true): array | |
{ | |
$q = $this->em->getConnection()->createQueryBuilder(); | |
$q->select('count(h.id) as count, ds.device as device') | |
->from(MAUTIC_TABLE_PREFIX.'page_hits', 'h') | |
->join('h', MAUTIC_TABLE_PREFIX.'lead_devices', 'ds', 'ds.id=h.device_id') | |
->orderBy('device', 'DESC') | |
->andWhere($q->expr()->gte('h.date_hit', ':date_from')) | |
->setParameter('date_from', $dateFrom->format('Y-m-d')) | |
->andWhere($q->expr()->lte('h.date_hit', ':date_to')) | |
->setParameter('date_to', $dateTo->format('Y-m-d 23:59:59')); | |
$q->groupBy('ds.device'); | |
$results = $q->executeQuery()->fetchAllAssociative(); | |
$chart = new PieChart(); | |
if (empty($results)) { | |
$results[] = [ | |
'device' => $this->translator->trans('mautic.report.report.noresults'), | |
'count' => 0, | |
]; | |
} | |
foreach ($results as $result) { | |
$label = empty($result['device']) ? $this->translator->trans('mautic.core.no.info') : $result['device']; | |
$chart->setDataset($label, $result['count']); | |
} | |
return $chart->render(); | |
} | |
/** | |
* Get a list of popular (by hits) pages. | |
* | |
* @param int $limit | |
* @param array $filters | |
* @param bool $canViewOthers | |
* | |
* @return array | |
*/ | |
public function getPopularPages($limit = 10, \DateTime $dateFrom = null, \DateTime $dateTo = null, $filters = [], $canViewOthers = true) | |
{ | |
$q = $this->em->getConnection()->createQueryBuilder(); | |
$q->select('COUNT(DISTINCT t.id) AS hits, p.id, p.title, p.alias') | |
->from(MAUTIC_TABLE_PREFIX.'page_hits', 't') | |
->join('t', MAUTIC_TABLE_PREFIX.'pages', 'p', 'p.id = t.page_id') | |
->orderBy('hits', 'DESC') | |
->groupBy('p.id') | |
->setMaxResults($limit); | |
if (!$canViewOthers) { | |
$q->andWhere('p.created_by = :userId') | |
->setParameter('userId', $this->userHelper->getUser()->getId()); | |
} | |
$chartQuery = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo); | |
$chartQuery->applyFilters($q, $filters); | |
$chartQuery->applyDateFilters($q, 'date_hit'); | |
return $q->execute()->fetchAllAssociative(); | |
} | |
/** | |
* Get a list of pages created in a date range. | |
* | |
* @param int $limit | |
* @param array $filters | |
* @param bool $canViewOthers | |
* | |
* @return array | |
*/ | |
public function getPageList($limit = 10, \DateTime $dateFrom = null, \DateTime $dateTo = null, $filters = [], $canViewOthers = true) | |
{ | |
$q = $this->em->getConnection()->createQueryBuilder(); | |
$q->select('t.id, t.title AS name, t.date_added, t.date_modified') | |
->from(MAUTIC_TABLE_PREFIX.'pages', 't') | |
->setMaxResults($limit); | |
if (!$canViewOthers) { | |
$q->andWhere('t.created_by = :userId') | |
->setParameter('userId', $this->userHelper->getUser()->getId()); | |
} | |
$chartQuery = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo); | |
$chartQuery->applyFilters($q, $filters); | |
$chartQuery->applyDateFilters($q, 'date_added'); | |
return $q->execute()->fetchAllAssociative(); | |
} | |
/** | |
* Get all params (e.g. UTM tags) from a url. | |
*/ | |
private function getQueryFromUrl(string $pageUrl): array | |
{ | |
$query = []; | |
$urlQuery = parse_url($pageUrl, PHP_URL_QUERY); | |
if (is_string($urlQuery)) { | |
parse_str($urlQuery, $urlQueryArray); | |
foreach ($urlQueryArray as $key => $value) { | |
if (is_string($value)) { | |
$key = strtolower($key); | |
$query[$key] = urldecode($value); | |
} | |
} | |
} | |
return $query; | |
} | |
/** | |
* Set UTM Tags based on the query of a page hit. | |
*/ | |
private function setUtmTags(Hit $hit, Lead $lead): void | |
{ | |
// Add UTM tags entry if a UTM tag exist | |
$queryHasUtmTags = false; | |
$query = $hit->getQuery(); | |
foreach ($query as $key => $value) { | |
if (str_contains($key, 'utm_')) { | |
$queryHasUtmTags = true; | |
break; | |
} | |
} | |
if ($queryHasUtmTags && $lead) { | |
$utmTags = new UtmTag(); | |
$utmTags->setDateAdded($hit->getDateHit()); | |
$utmTags->setUrl($hit->getUrl()); | |
$utmTags->setReferer($hit->getReferer()); | |
$utmTags->setQuery($hit->getQuery()); | |
$utmTags->setUserAgent($hit->getUserAgent()); | |
$utmTags->setRemoteHost($hit->getRemoteHost()); | |
$utmTags->setLead($lead); | |
if (array_key_exists('utm_campaign', $query)) { | |
$utmTags->setUtmCampaign($query['utm_campaign']); | |
} | |
if (array_key_exists('utm_term', $query)) { | |
$utmTags->setUtmTerm($query['utm_term']); | |
} | |
if (array_key_exists('utm_content', $query)) { | |
$utmTags->setUtmContent($query['utm_content']); | |
} | |
if (array_key_exists('utm_medium', $query)) { | |
$utmTags->setUtmMedium($query['utm_medium']); | |
} | |
if (array_key_exists('utm_source', $query)) { | |
$utmTags->setUtmSource($query['utm_source']); | |
} | |
$repo = $this->em->getRepository(UtmTag::class); | |
$repo->saveEntity($utmTags); | |
$this->leadModel->setUtmTags($lead, $utmTags); | |
} | |
} | |
private function setLeadManipulator($page, Hit $hit, Lead $lead): void | |
{ | |
// Only save the lead and dispatch events if needed | |
$source = 'hit'; | |
$sourceId = $hit->getId(); | |
if ($page) { | |
$source = $page instanceof Page ? 'page' : 'redirect'; | |
$sourceId = $page->getId(); | |
} | |
$lead->setManipulator( | |
new LeadManipulator( | |
'page', | |
$source, | |
$sourceId, | |
$hit->getUrl() | |
) | |
); | |
$this->leadModel->saveEntity($lead); | |
} | |
/** | |
* @return mixed|string | |
*/ | |
private function getPageUrl(Request $request, $page) | |
{ | |
// Default to page_url set in the query from tracking pixel and/or contactfield token | |
if ($pageURL = $request->get('page_url')) { | |
return $pageURL; | |
} | |
if ($page instanceof Redirect) { | |
// use the configured redirect URL | |
return $page->getUrl(); | |
} | |
// Use the current URL | |
$isPageEvent = false; | |
if (str_contains($request->server->get('REQUEST_URI'), $this->router->generate('mautic_page_tracker'))) { | |
// Tracking pixel is used | |
if ($request->server->get('QUERY_STRING')) { | |
parse_str($request->server->get('QUERY_STRING'), $query); | |
$isPageEvent = true; | |
} | |
} elseif (str_contains($request->server->get('REQUEST_URI'), $this->router->generate('mautic_page_tracker_cors'))) { | |
$query = $request->request->all(); | |
$isPageEvent = true; | |
} | |
if ($isPageEvent) { | |
$pageURL = $request->server->get('HTTP_REFERER'); | |
// if additional data were sent with the tracking pixel | |
if (isset($query)) { | |
// URL attr 'd' is encoded so let's decode it first. | |
$decoded = false; | |
if (isset($query['d'])) { | |
// parse_str auto urldecodes | |
$query = $this->decodeArrayFromUrl($query['d'], false); | |
$decoded = true; | |
} | |
if (is_array($query) && !empty($query)) { | |
if (isset($query['page_url'])) { | |
$pageURL = $query['page_url']; | |
if (!$decoded) { | |
$pageURL = urldecode($pageURL); | |
} | |
} | |
if (isset($query['page_referrer'])) { | |
if (!$decoded) { | |
$query['page_referrer'] = urldecode($query['page_referrer']); | |
} | |
} | |
if (isset($query['page_language'])) { | |
if (!$decoded) { | |
$query['page_language'] = urldecode($query['page_language']); | |
} | |
} | |
if (isset($query['page_title'])) { | |
if (!$decoded) { | |
$query['page_title'] = urldecode($query['page_title']); | |
} | |
} | |
if (isset($query['tags'])) { | |
if (!$decoded) { | |
$query['tags'] = urldecode($query['tags']); | |
} | |
} | |
} | |
} | |
return $pageURL; | |
} | |
$pageURL = 'http'; | |
if ('on' == $request->server->get('HTTPS')) { | |
$pageURL .= 's'; | |
} | |
$pageURL .= '://'; | |
if (!in_array((int) $request->server->get('SERVER_PORT', 80), [80, 8080, 443])) { | |
return $pageURL.$request->server->get('SERVER_NAME').':'.$request->server->get('SERVER_PORT'). | |
$request->server->get('REQUEST_URI'); | |
} | |
return $pageURL.$request->server->get('SERVER_NAME').$request->server->get('REQUEST_URI'); | |
} | |
/* | |
* Cleans query params saving url values. | |
* | |
* @param $query array | |
* | |
* @return array | |
*/ | |
private function cleanQuery(array $query): array | |
{ | |
foreach ($query as $key => $value) { | |
if (filter_var($value, FILTER_VALIDATE_URL)) { | |
$query[$key] = InputHelper::url($value); | |
} else { | |
$query[$key] = InputHelper::clean($value); | |
} | |
} | |
return $query; | |
} | |
} | |